35 Commits

Author SHA1 Message Date
Tony 20642e5ec3 fix(gateway): update DALI configuration parameters for improved performance and reliability
This commit add comat parameter for some old DALI-1 control gear.

Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-21 17:03:35 +08:00
Tony 0827befb06 feat(gateway_cache): enhance DALI state management and caching
- Increased flush interval to 10 seconds and added a refresh interval of 120 seconds in GatewayCacheConfig.
- Introduced a new boolean `stale` in GatewayCacheDaliRuntimeStatus to track stale states.
- Added methods for setting actual DALI levels and persisting DALI address states.
- Implemented functions to build and apply DALI state payloads, including handling scene levels.
- Enhanced the GatewayCache class to manage DALI states more effectively, including loading and persisting states.
- Updated GatewayController to support cache refresh operations, including handling cache commands and reporting cache status.
- Added mechanisms for periodic cache refresh based on idle time and configured intervals.

Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-21 15:34:26 +08:00
Tony 2b8ef31263 Add GatewayKnxTpIpRouter implementation for handling KNXnet/IP services
- Implemented handleUdpDatagram to process incoming UDP datagrams and route them to appropriate handlers based on service type.
- Added methods for handling various KNXnet/IP requests including search, description, tunneling, device configuration, connection state, and disconnect requests.
- Introduced TunnelClient management for handling multiple tunnel connections, including allocation, resetting, and pruning stale clients.
- Implemented secure service handling with appropriate logging for unsupported secure sessions.
- Enhanced logging for better traceability of incoming requests and responses.

Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-21 14:12:46 +08:00
Tony 8e80fd05b4 fix(gateway): enhance DALI logging with query packet logging and configuration
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-21 13:39:31 +08:00
Tony 7ab1855295 fix(gateway): enhance DALI logging and error handling in gateway_knx
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-21 13:22:38 +08:00
Tony e8bf8c56cd fix(gateway): update subproject commit reference for knx module
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-21 11:59:24 +08:00
Tony 416320604b fix(gateway): update subproject commit reference for knx module
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-20 06:33:10 +08:00
Tony e489a2de36 fix(gateway): update subproject commit reference for knx module
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-20 06:18:30 +08:00
Tony b73e0c6138 fix(gateway): update subproject commit reference for knx module
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-20 05:54:48 +08:00
Tony 3165afe8a4 fix(gateway): update subproject commit reference for knx module
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-20 04:57:25 +08:00
Tony 57950e7b0b feat(gateway): implement commissioning scan functionality with options for new, randomize, delete, and assign
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-19 10:47:44 +08:00
Tony a3f03719f9 feat(gateway): add extended function property handlers and diagnostics support for OpenKNX
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-19 10:02:57 +08:00
Tony e091b4301e fix(gateway): enable KNX security device endpoints and enhance tunnel frame handling
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-19 08:16:28 +08:00
Tony 226855362b feat(gateway): integrate bridge service into gateway controller for transport handling
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-19 05:52:36 +08:00
Tony 3bc5355041 feat(gateway): enable full IP forwarding for KNX TP and enhance group value write handling
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-19 04:18:53 +08:00
Tony 3af2995b40 feat(gateway): add support for full IP forwarding of KNX TP telegrams and enhance tunnel frame handling
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-19 03:14:03 +08:00
Tony b447da5bfc feat(gateway): enhance handling of local routing and tunnel frames in KNX integration
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-19 00:09:17 +08:00
Tony f2b7dee8bd fix(gateway): improve TP-UART configuration logging and handling for ETS device states
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-16 10:25:20 +08:00
Tony 77fe8c1b02 feat(gateway): enhance DALI and KNX integration with tunnel confirmation handling and improved message routing
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-16 09:34:14 +08:00
Tony 82142dd46c feat(gateway): add support for DALI scene handling and relative brightness adjustments
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-16 04:38:15 +08:00
Tony 323ff24c04 feat(gateway): add KNX TP UART startup timeout configuration and enhance initialization logic
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-16 03:40:45 +08:00
Tony 277379abd7 feat(gateway): add DALI bus ID configuration and enhance group object write handling
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-16 03:31:12 +08:00
Tony e79223c87e Refactor KNX DALI Gateway Configuration and Update Device Parameters
- Introduced configuration macros for OEM manufacturer ID, application number, and application version in knxprod.h.
- Updated product identity definitions to use the new configuration macros.
- Modified device type enumeration to include additional device types.
- Adjusted color space enumeration values for consistency.
- Defined generated group object layout constants for memory offsets and block sizes.
- Enhanced knx_dali_gw.cpp to utilize the new configuration macros for manufacturer ID and program version.
- Updated the device initialization logic to reflect the new hardware and program version structures.
- Removed obsolete knx_dali_gw subproject and updated related submodules.

Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-16 01:51:01 +08:00
Tony 2a3808c1e4 feat(gateway): enhance OpenKNX integration with new DIB construction methods and improve BLE configuration
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-16 01:50:26 +08:00
Tony 449a3a801a 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 <tonylu@tony-cloud.com>
2026-05-15 12:34:13 +08:00
Tony 3f15cd7f3f fix(gateway): update DALI query response timeout and activity wait parameters
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-15 10:33:22 +08:00
Tony f005d2bc09 feat(gateway): update BLE connection parameters and enhance DALI timeout configurations
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-15 08:42:20 +08:00
Tony 70ae1ae6cf feat(dali): enhance DALI timing configurations and add new timeout parameters
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-15 03:17:35 +08:00
Tony f2ffb45ca6 feat(gateway): implement LegacyRawPayload function for DALI and USB components
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-15 02:35:37 +08:00
Tony 0b2d00472e Enhance DALI Component Configuration and Functionality
- Updated README.md to include new configuration options for native timing values, TX/RX polarity, power-down polling, and logging levels.
- Introduced new default values for query response timeout and double-send delay in dali.c.
- Implemented a function to drain stale RX frames from the queue to improve query handling.
- Enhanced DALI HAL implementation in dali_hal_idf5.c with additional configuration options for timer resolution and bus power check intervals.
- Added logging capabilities to track bus states and message transmissions in the DALI HAL.
- Improved error handling and message response mechanisms in dali_domain.cpp and gateway_usb_setup.cpp for better communication reliability.
- Refactored GPIO handling to support configurable TX/RX active states in dali_hal.h.
- Introduced legacy query response handling for backward compatibility in the DALI domain.

Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-15 01:26:13 +08:00
Tony 4553ed32e7 feat(gateway): add support for KNX TP UART 9-bit mode and enhance UART pin configuration
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-14 22:39:14 +08:00
Tony 39ef630608 feat: Enhance EtsDeviceRuntime constructor and multicast handling
- Updated EtsDeviceRuntime constructor to accept an optional tunnel_client_address parameter with a default value of 0.
- Modified EspIdfPlatform::setupMultiCast to use IPPROTO_UDP for socket creation and improved multicast interface selection based on the current IP address.
- Ensured that the multicast interface is set only if a valid local address is available.
- Adjusted the client address assignment in EtsDeviceRuntime to use the provided tunnel_client_address if valid, falling back to the default otherwise.

Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-13 12:47:37 +08:00
Tony b74367e5a0 Refactor GatewayModbus and GatewayNetwork components
- Updated GatewayModbusConfig to allow uart_port and pin values to be -1, indicating an unconfigured state.
- Enhanced GatewayNetworkService to support an additional setup AP button with configurable GPIO and active low settings.
- Refactored boot button configuration logic to reduce redundancy and improve clarity.
- Introduced a new method for handling GPIO input configuration.
- Improved boot button task loop to handle both boot and setup AP buttons more effectively.
- Added programming mode functionality to EtsDeviceRuntime, allowing toggling and querying of the programming state.
- Implemented memory checks to avoid unnecessary reads in EtsDeviceRuntime.
- Enhanced security storage to derive factory FDSK from the device's serial number and store it in NVS.
- Updated factory FDSK loading logic to ensure proper key generation and storage.

Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-13 12:36:16 +08:00
Tony df1dd472cc feat(gateway): implement KNX security features including secure session handling and factory certificate management
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-12 21:29:40 +08:00
Tony 888d021343 feat(gateway): update partition sizes and GPIO configurations for W5500 Ethernet
Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-12 21:29:00 +08:00
82 changed files with 14996 additions and 4129 deletions
-4
View File
@@ -5,10 +5,6 @@
path = knx
url = https://git.tonycloud.org/knx/knx.git
branch = v1
[submodule "knx_dali_gw"]
path = knx_dali_gw
url = https://git.tonycloud.org/knx/GW-REG1-Dali.git
branch = tonycloud-dev
[submodule "tpuart"]
path = tpuart
url = https://git.tonycloud.org/knx/tpuart.git
+49 -3
View File
@@ -4,6 +4,8 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
## Layout
This is the list of top-level directories and their purposes,
update as the project evolves:
- `apps/`: standard ESP-IDF applications for each firmware role.
- `apps/gateway/main/Kconfig.projbuild`: project-visible gateway-role settings such as per-channel native/serial PHY selection, gateway ids, pin mapping, and startup transport policy.
- `components/`: reusable components shared by all gateway applications.
@@ -12,19 +14,63 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
- `dali_domain/`: native DALI domain facade over `dali_cpp` and raw frame sinks.
- `gateway_cache/`: DALI scene/group/settings/runtime cache used by controller reconciliation and protocol bridges.
- `gateway_bridge/`: per-channel bridge provisioning, command execution, protocol startup, and HTTP bridge actions.
- `openknx_idf/`: ESP-IDF port layer for the OpenKNX `gateway/knx` and `gateway/tpuart` submodules, including NVS-backed OpenKNX memory, ETS cEMI programming support, UDP multicast/unicast plumbing, and a native TP-UART interface without the Arduino framework.
- `openknx_idf/`: ESP-IDF port layer for the OpenKNX `gateway/knx` and `gateway/tpuart` submodules, including NVS-backed OpenKNX memory, development KNX security storage, ETS cEMI programming support, UDP multicast/unicast plumbing, and a native TP-UART interface without the Arduino framework.
- `gateway_modbus/`: gateway-owned Modbus TCP/RTU/ASCII config, generated DALI point tables, and provisioned Modbus model override dispatch.
- `gateway_bacnet/`: BACnet/IP server adapter backed by bacnet-stack, including the gateway-owned BACnet bridge model adapter.
- `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`, including raw DALI notifications.
- `gateway_controller/`: Lua-compatible gateway command dispatcher, internal scene/group state, and notification fan-out.
- `gateway_network/`: HTTP `/info`, `/dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP port `2020` command/notify routing, Wi-Fi STA lifecycle, W5500 SPI Ethernet startup/teardown, ESP-Touch smartconfig, setup AP mode, ESP-NOW setup ingress, and BOOT-button Wi-Fi reset for the native gateway.
- `gateway_network/`: HTTP `/info`, `/dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP port `2020` command/notify routing, Wi-Fi STA lifecycle, W5500 SPI Ethernet startup/teardown, ESP-Touch smartconfig, setup AP mode, ESP-NOW setup ingress, setup AP GPIO handling, and optional Wi-Fi reset GPIO handling for the native gateway.
- `gateway_runtime/`: persistent runtime state, command queueing, and device info services.
- `gateway_485_control/`: optional 485 Lua control bridge for framed `0x28 0x01` commands and `0x22 ... checksum` notifications at `9600 8N1`; disabled by default because UART0 must be moved off the ESP-IDF console first.
- `gateway_usb_setup/`: optional USB Serial/JTAG setup bridge; disabled by default so USB remains available for debug at boot.
- `knx`: The forked OpenKNX cEMI programming support, ESP-IDF port, and KNX security storage used by the gateway. You can edit this code when necessary to support missing ETS programming features or to implement the secure-session transport path.
- `knx_dali_gw`: The forked OpenKNX DALI-GW function-property support used by the gateway. You can edit this code when necessary to support missing DALI-GW features or to fix bugs.
## Gateway DALI cache
The gateway owns the shared DALI cache for multi-user deployments. App, BLE,
IP, Modbus, BACnet, KNX, and local control paths should treat the gateway cache
as the shared read surface instead of maintaining separate app-side device-state
caches. Transparent/setup raw forwarding paths remain bypass-oriented, but raw
DALI bus observation still feeds the passive decoder when frames are visible to
the gateway.
`gateway_cache` stores internal scene/group data and per-short-address DALI
state. Device settings, group masks, scene levels, known flags, and the last
runtime status are batched to NVS using `GATEWAY_CACHE_FLUSH_INTERVAL_MS`, which
defaults to 10000 ms. Runtime status loaded from disk is marked stale until the
gateway observes a bus command or the background refresher verifies it again.
`GATEWAY_CACHE_REFRESH_INTERVAL_MS` defaults to 120000 ms. When nonzero, the
controller maintenance loop refreshes direct short-address actual levels one
small step at a time. The refresh is low priority: it yields to queued gateway
commands, address allocation, live management reads, bridge traffic, and any raw
DALI bus activity from another master until the bus has been idle. Group and
broadcast targets are never queried for refresh.
Gateway feature opcode `0x06` advertises cache support with bit `0x40`. Gateway
opcode `0x39` returns cache summary and target snapshots so frontend clients can
read cached state without issuing live DALI queries on supported gateways.
## Current status
The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port with raw receive fan-out, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app also includes a `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, W5500 SPI Ethernet with DHCP, Wi-Fi STA lifecycle, ESP-Touch smartconfig credential provisioning, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, and BOOT-button Wi-Fi credential clearing, and an optional `gateway_485_control` bridge that claims UART0 for Lua-compatible framed command ingress plus `0x22` notification egress when the console is moved off UART0. Startup behavior is configured in `main/Kconfig.projbuild`: BLE and wired Ethernet are enabled by default, W5500 initialization and startup probe failures are ignored by default for boards without populated Ethernet hardware by fully disabling Ethernet for that boot, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected, and the UART0 control bridge stays disabled unless the deployment explicitly repurposes UART0 away from the ESP-IDF console. Runtime settings and internal scene/group data are cached in RAM after load, skip unchanged flash writes, and batch Wi-Fi credential commits to reduce flash stalls on ESP32-S3 boards where flash and PSRAM share the SPI bus. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. The checked-in `sdkconfig` is aligned with the app's custom 16 MB partition table so the Wi-Fi/BLE/network-enabled image fits the OTA app slots.
The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port with raw receive fan-out, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app also includes a `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, W5500 SPI Ethernet with DHCP, Wi-Fi STA lifecycle, ESP-Touch smartconfig credential provisioning, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, setup AP GPIO entry, and optional Wi-Fi credential reset GPIO handling, and an optional `gateway_485_control` bridge that claims UART0 for Lua-compatible framed command ingress plus `0x22` notification egress when the console is moved off UART0. Startup behavior is configured in `main/Kconfig.projbuild`: BLE and wired Ethernet are enabled by default, W5500 initialization and startup probe failures are ignored by default for boards without populated Ethernet hardware by fully disabling Ethernet for that boot, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected, and the UART0 control bridge stays disabled unless the deployment explicitly repurposes UART0 away from the ESP-IDF console. Runtime settings and internal scene/group data are cached in RAM after load, skip unchanged flash writes, and batch Wi-Fi credential commits to reduce flash stalls on ESP32-S3 boards where flash and PSRAM share the SPI bus. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. The checked-in `sdkconfig` is aligned with the app's custom 16 MB partition table so the Wi-Fi/BLE/network-enabled image fits the OTA app slots.
## KNX Security
KNX Data Secure and KNXnet/IP Secure support are controlled by `GATEWAY_KNX_DATA_SECURE_SUPPORTED` and `GATEWAY_KNX_IP_SECURE_SUPPORTED`. The current KNXnet/IP Secure flag reserves and reports secure service capability, while runtime secure-session transport is still reported as not implemented until that path is wired. The gateway derives its KNX serial identity from the ESP base MAC, and the development factory setup key is deterministically derived from that KNX serial so the same board keeps the same FDSK across NVS erases.
The KNXnet/IP tunnel can start from the built-in default configuration before any ETS download. KNX TP-UART is enabled only when `GATEWAY_KNX_TP_UART_PORT` is `0`, `1`, or `2`; set that UART port to `-1` for IP-only operation. UART TX/RX GPIO values of `-1` mean use the ESP-IDF target default pins for that UART, not disabled. `GATEWAY_KNX_TP_UART_9BIT_MODE` enables the NCN5120/OpenKNX-style 9-bit host frame on the wire, represented on ESP-IDF as 8 data bits plus even parity. Enable `GATEWAY_KNX_TP_FULL_IP_FORWARD` when the gateway must mirror all physical TP telegrams back out to KNXnet/IP tunnelling and multicast so ETS can monitor or download other TP devices through the gateway's IP endpoint. Non-UART GPIO options use `-1` as disabled, including the KNX programming button, KNX programming LED, setup AP button, Wi-Fi reset button, and status LED.
When no KNX bridge config or ETS application data has been downloaded, the KNXnet/IP router starts in commissioning mode: OpenKNX receives tunnel programming traffic from ETS, while DALI group routing and REG1-Dali function-property actions stay inactive until ETS reports a configured application.
The bridge service exposes one shared KNXnet/IP endpoint per physical gateway on the configured UDP port. Per-channel DALI/KNX bridge runtimes keep their own group-address mappings behind that endpoint, and incoming group writes are dispatched to the matching channel instead of starting one UDP socket per DALI channel.
KNX programming mode can be controlled locally with `GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO`, and `GATEWAY_KNX_PROGRAMMING_LED_GPIO` mirrors the current programming-mode state. The setup AP entry button is configured separately with `GATEWAY_SETUP_AP_BUTTON_GPIO`; Wi-Fi credential reset remains a separate long-press function on `GATEWAY_BOOT_BUTTON_GPIO` when enabled.
When `GATEWAY_KNX_SECURITY_DEV_ENDPOINTS` is enabled, the bridge HTTP action surface exposes development-only operations for reading, writing, generating, and resetting the factory setup key, exporting the factory certificate payload, and clearing local KNX security failure diagnostics. These endpoints require explicit confirmation fields in the JSON body and should stay disabled in production builds. The default development storage mode is plain NVS via `GATEWAY_KNX_SECURITY_PLAIN_NVS`; production builds should replace that with encrypted NVS, flash encryption, and secure boot before handling real commissioning keys.
The normal bridge status response includes a `knx.security` object with compile-time capability flags, storage mode, factory setup key metadata, factory certificate metadata, and security failure counters/log entries. Secret FDSK strings are returned only by the explicit development actions, not by passive status polling.
## Modbus
+1
View File
@@ -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"
)
+198 -24
View File
@@ -71,18 +71,20 @@ config GATEWAY_CHANNEL1_NATIVE_BAUDRATE
config GATEWAY_CHANNEL1_SERIAL_TX_PIN
int "Serial PHY TX pin"
depends on GATEWAY_CHANNEL1_PHY_UART1 || GATEWAY_CHANNEL1_PHY_UART2
range 0 48
range -1 48
default 1
help
ESP32-S3 GPIO used by the channel 1 serial PHY transmit pin.
ESP32-S3 GPIO used by the channel 1 serial PHY transmit pin. Set to -1
to keep the UART driver's default TX routing.
config GATEWAY_CHANNEL1_SERIAL_RX_PIN
int "Serial PHY RX pin"
depends on GATEWAY_CHANNEL1_PHY_UART1 || GATEWAY_CHANNEL1_PHY_UART2
range 0 48
range -1 48
default 2
help
ESP32-S3 GPIO used by the channel 1 serial PHY receive pin.
ESP32-S3 GPIO used by the channel 1 serial PHY receive pin. Set to -1
to keep the UART driver's default RX routing.
config GATEWAY_CHANNEL1_SERIAL_BAUDRATE
int "Serial PHY baudrate"
@@ -178,18 +180,20 @@ config GATEWAY_CHANNEL2_NATIVE_BAUDRATE
config GATEWAY_CHANNEL2_SERIAL_TX_PIN
int "Serial PHY TX pin"
depends on GATEWAY_CHANNEL_COUNT >= 2 && (GATEWAY_CHANNEL2_PHY_UART1 || GATEWAY_CHANNEL2_PHY_UART2)
range 0 48
range -1 48
default 6
help
ESP32-S3 GPIO used by the channel 2 serial PHY transmit pin.
ESP32-S3 GPIO used by the channel 2 serial PHY transmit pin. Set to -1
to keep the UART driver's default TX routing.
config GATEWAY_CHANNEL2_SERIAL_RX_PIN
int "Serial PHY RX pin"
depends on GATEWAY_CHANNEL_COUNT >= 2 && (GATEWAY_CHANNEL2_PHY_UART1 || GATEWAY_CHANNEL2_PHY_UART2)
range 0 48
range -1 48
default 7
help
ESP32-S3 GPIO used by the channel 2 serial PHY receive pin.
ESP32-S3 GPIO used by the channel 2 serial PHY receive pin. Set to -1
to keep the UART driver's default RX routing.
config GATEWAY_CHANNEL2_SERIAL_BAUDRATE
int "Serial PHY baudrate"
@@ -255,9 +259,18 @@ config GATEWAY_CACHE_FLUSH_INTERVAL_MS
int "Cache flush interval ms"
depends on GATEWAY_CACHE_SUPPORTED && GATEWAY_CACHE_START_ENABLED
range 100 600000
default 5000
default 10000
help
Interval used to batch scene and group cache writes to flash.
Interval used to batch gateway cache writes to flash.
config GATEWAY_CACHE_REFRESH_INTERVAL_MS
int "Cache status refresh interval ms"
depends on GATEWAY_CACHE_SUPPORTED && GATEWAY_CACHE_START_ENABLED
range 0 3600000
default 120000
help
Interval used by the low-priority DALI status refresh scheduler. Set to
0 to disable scheduled refresh while keeping command and bus mirroring.
choice GATEWAY_CACHE_CONFLICT_PRIORITY
prompt "Cache conflict priority default"
@@ -542,8 +555,11 @@ config GATEWAY_MODBUS_UNIT_ID
config GATEWAY_MODBUS_SERIAL_UART_PORT
int "Default Modbus serial UART port"
depends on GATEWAY_MODBUS_BRIDGE_SUPPORTED && (GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU || GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII)
range 0 2
range -1 2
default 1
help
UART used by the default Modbus serial server. Set to -1 to disable the
default serial UART runtime for this function.
config GATEWAY_MODBUS_ALLOW_UART0
bool "Allow Modbus/setup to claim UART0"
@@ -656,6 +672,47 @@ config GATEWAY_KNX_SECURITY_PLAIN_NVS
during bring-up, but production builds should replace it with encrypted
NVS, flash encryption, and secure boot before exposing real keys.
config GATEWAY_KNX_OEM_MANUFACTURER_ID
hex "KNX OEM manufacturer ID"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 0x0000 0xffff
default 0x00A4
help
Manufacturer ID advertised by the ETS-programmable KNX-DALI gateway
application. This value must match the manufacturer ID used by the
Kaenx Creator generated KNX product database.
config GATEWAY_KNX_OEM_HARDWARE_ID
hex "KNX OEM hardware ID"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 0x0000 0xffff
default 0xA401
help
Hardware ID encoded into the OpenKNX Device Object hardware type as
0000HHHHVV00. This must match the hardware identifier from the KNX
product database, for example Hardware Id or SerialNumber 0xA401 in
the generated Hardware.xml.
config GATEWAY_KNX_OEM_APPLICATION_NUMBER
hex "KNX OEM application number"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 0x0000 0xffff
default 0x0001
help
Application number advertised by the ETS-programmable KNX-DALI gateway
application. Keep this in sync with MAIN_ApplicationNumber from the
generated knxprod.h.
config GATEWAY_KNX_OEM_APPLICATION_VERSION
hex "KNX OEM application version"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 0x00 0xff
default 0x08
help
Application version advertised by the ETS-programmable KNX-DALI gateway
application. Keep this in sync with MAIN_ApplicationVersion from the
generated knxprod.h.
config GATEWAY_KNX_MAIN_GROUP
int "KNX DALI main group"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
@@ -665,6 +722,24 @@ config GATEWAY_KNX_MAIN_GROUP
Main group used by the built-in KNX to DALI router. Middle groups select
the data type and subgroups select broadcast, short-address, or group targets.
config GATEWAY_KNX_DALI_BUS_ID
int "KNX database target DALI bus id"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 0 15
default 0
help
Selects the native DALI HAL bus targeted by the ETS KNX product database.
The current KNX-DALI application supports one ETS-controlled DALI bus.
config GATEWAY_KNX_DEBUG_DUMP_MEMORY
bool "Dump full OpenKNX memory for debugging"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
default n
help
Prints the complete OpenKNX non-volatile memory image when it is restored
or committed. Enable only while debugging ETS download and association
table issues, because the log is large and may include KNX configuration data.
config GATEWAY_KNX_TUNNEL_ENABLED
bool "Enable KNXnet/IP tunneling mode"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
@@ -686,32 +761,78 @@ config GATEWAY_KNX_MULTICAST_ADDRESS
depends on GATEWAY_KNX_BRIDGE_SUPPORTED && GATEWAY_KNX_MULTICAST_ENABLED
default "224.0.23.12"
config GATEWAY_KNX_INDIVIDUAL_ADDRESS
int "KNX individual address raw value"
config GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS
int "KNXnet/IP interface individual address raw value"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 0 65535
default 4353
default 65281
help
Raw 16-bit individual address advertised to KNXnet/IP tunnel clients.
The default 4353 is 1.1.1.
Raw 16-bit individual address advertised by the KNXnet/IP interface.
The default 65281 is 15.15.1.
config GATEWAY_KNX_INDIVIDUAL_ADDRESS
int "Logical KNX-DALI gateway individual address raw value"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 0 65535
default 65534
help
Raw 16-bit individual address used by the ETS-programmable KNX-DALI gateway device.
The default 65534 is 15.15.254, used as the unprogrammed logical device address.
config GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO
int "KNX programming button GPIO"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range -1 48
default -1
help
GPIO used to toggle KNX programming mode. Set to -1 to disable the local
KNX programming button.
config GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW
bool "KNX programming button is active low"
depends on GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO >= 0
default y
config GATEWAY_KNX_PROGRAMMING_LED_GPIO
int "KNX programming LED GPIO"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range -1 48
default -1
help
GPIO used to show KNX programming mode. Set to -1 to disable the local
KNX programming LED.
config GATEWAY_KNX_PROGRAMMING_LED_ACTIVE_HIGH
bool "KNX programming LED is active high"
depends on GATEWAY_KNX_PROGRAMMING_LED_GPIO >= 0
default y
config GATEWAY_KNX_TP_UART_PORT
int "KNX TP UART port"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 0 2
default 1
range -1 2
default -1
help
UART used by the KNX TP-UART interface. Set to -1 to disable TP-UART
while keeping KNXnet/IP tunnelling and routing available.
config GATEWAY_KNX_TP_TX_PIN
int "KNX TP UART TX pin"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range -1 48
default -1
help
GPIO used by the KNX TP-UART TX pin. Set to -1 to keep the UART driver's
default TX routing.
config GATEWAY_KNX_TP_RX_PIN
int "KNX TP UART RX pin"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range -1 48
default -1
help
GPIO used by the KNX TP-UART RX pin. Set to -1 to keep the UART driver's
default RX routing.
config GATEWAY_KNX_TP_BAUDRATE
int "KNX TP UART baudrate"
@@ -719,11 +840,43 @@ config GATEWAY_KNX_TP_BAUDRATE
range 1200 921600
default 19200
config GATEWAY_KNX_TP_STARTUP_TIMEOUT_MS
int "KNX TP UART startup timeout (ms)"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 0 10000
default 2000
help
How long the gateway should keep retrying TP-UART startup before
giving up. This covers transient UART/GPIO ownership races that can
happen immediately after an ETS-triggered reboot. Set to 0 to keep the
previous single-shot startup behavior.
config GATEWAY_KNX_TP_UART_9BIT_MODE
bool "KNX TP UART 9-bit mode"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
default y
help
Enable the NCN5120/OpenKNX-style 9-bit UART frame on the wire. ESP-IDF
exposes this as 8 data bits plus even parity, matching the TP-UART host
mode commonly described as 19200 baud 9-bit UART. Disable only for
hardware wired for 8N1 host UART mode.
config GATEWAY_KNX_TP_FULL_IP_FORWARD
bool "Mirror all physical KNX TP telegrams to KNXnet/IP"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED && GATEWAY_KNX_TP_UART_PORT >= 0
default n
help
Mirrors physical KNX TP telegrams received from the TP-UART line back
out through KNXnet/IP tunnelling and multicast even when the gateway
runs the single-interface ETS device runtime. Enable this when ETS must
monitor or download other TP devices through the gateway's IP endpoint.
Leave it disabled to preserve the narrower default forwarding behavior.
config GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE
int "KNX/IP bridge task stack bytes"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
range 6144 24576
default 8192
default 12288
config GATEWAY_BRIDGE_KNX_TASK_PRIORITY
int "KNX/IP bridge task priority"
@@ -916,23 +1069,44 @@ config GATEWAY_STATUS_LED_ACTIVE_HIGH
default y
config GATEWAY_BOOT_BUTTON_GPIO
int "BOOT button GPIO"
int "Wi-Fi reset button GPIO"
range -1 48
default 0
default -1
help
GPIO used for Lua-compatible setup entry and Wi-Fi credential clearing. Set to -1 to disable.
GPIO used for long-press Wi-Fi credential clearing. Set to -1 to disable.
config GATEWAY_BOOT_BUTTON_ACTIVE_LOW
bool "BOOT button is active low"
bool "Wi-Fi reset button is active low"
depends on GATEWAY_BOOT_BUTTON_GPIO >= 0
default y
config GATEWAY_BOOT_BUTTON_LONG_PRESS_MS
int "BOOT button long press ms"
int "Wi-Fi reset button long press ms"
depends on GATEWAY_BOOT_BUTTON_GPIO >= 0
range 500 10000
default 3000
config GATEWAY_SETUP_AP_BUTTON_GPIO
int "Setup AP button GPIO"
range -1 48
default 0
help
GPIO used for entering setup AP mode. Set to -1 to disable local setup
AP entry by GPIO.
config GATEWAY_SETUP_AP_BUTTON_ACTIVE_LOW
bool "Setup AP button is active low"
depends on GATEWAY_SETUP_AP_BUTTON_GPIO >= 0
default y
config GATEWAY_BUTTON_TASK_STACK_SIZE
int "Gateway button task stack bytes"
depends on GATEWAY_BOOT_BUTTON_GPIO >= 0 || GATEWAY_SETUP_AP_BUTTON_GPIO >= 0
range 3072 12288
default 8192
help
Stack used by the GPIO button task and one-shot setup AP task.
endmenu
endmenu
+100 -8
View File
@@ -40,6 +40,21 @@
#define CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS 3000
#endif
#ifndef CONFIG_GATEWAY_SETUP_AP_BUTTON_GPIO
#ifdef CONFIG_GATEWAY_BOOT_BUTTON_GPIO
#define CONFIG_GATEWAY_SETUP_AP_BUTTON_GPIO CONFIG_GATEWAY_BOOT_BUTTON_GPIO
#ifdef CONFIG_GATEWAY_BOOT_BUTTON_ACTIVE_LOW
#define CONFIG_GATEWAY_SETUP_AP_BUTTON_ACTIVE_LOW 1
#endif
#else
#define CONFIG_GATEWAY_SETUP_AP_BUTTON_GPIO -1
#endif
#endif
#ifndef CONFIG_GATEWAY_BUTTON_TASK_STACK_SIZE
#define CONFIG_GATEWAY_BUTTON_TASK_STACK_SIZE 8192
#endif
#ifndef CONFIG_GATEWAY_USB_SETUP_CHANNEL_INDEX
#define CONFIG_GATEWAY_USB_SETUP_CHANNEL_INDEX 0
#endif
@@ -92,6 +107,10 @@
#define CONFIG_GATEWAY_485_CONTROL_TASK_PRIORITY 4
#endif
#ifndef CONFIG_GATEWAY_KNX_TP_STARTUP_TIMEOUT_MS
#define CONFIG_GATEWAY_KNX_TP_STARTUP_TIMEOUT_MS 2000
#endif
#ifndef CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC
#define CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC 60
#endif
@@ -193,7 +212,7 @@
#endif
#ifndef CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE
#define CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE 8192
#define CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE 12288
#endif
#ifndef CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY
@@ -204,6 +223,10 @@
#define CONFIG_GATEWAY_KNX_MAIN_GROUP 0
#endif
#ifndef CONFIG_GATEWAY_KNX_DALI_BUS_ID
#define CONFIG_GATEWAY_KNX_DALI_BUS_ID 0
#endif
#ifndef CONFIG_GATEWAY_KNX_UDP_PORT
#define CONFIG_GATEWAY_KNX_UDP_PORT 3671
#endif
@@ -213,11 +236,23 @@
#endif
#ifndef CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS
#define CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS 4353
#define CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS 65534
#endif
#ifndef CONFIG_GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS
#define CONFIG_GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS 65281
#endif
#ifndef CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO
#define CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO -1
#endif
#ifndef CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO
#define CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO -1
#endif
#ifndef CONFIG_GATEWAY_KNX_TP_UART_PORT
#define CONFIG_GATEWAY_KNX_TP_UART_PORT 1
#define CONFIG_GATEWAY_KNX_TP_UART_PORT -1
#endif
#ifndef CONFIG_GATEWAY_KNX_TP_TX_PIN
@@ -233,7 +268,11 @@
#endif
#ifndef CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS
#define CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS 5000
#define CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS 10000
#endif
#ifndef CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS
#define CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS 120000
#endif
namespace {
@@ -579,21 +618,38 @@ bool ValidateChannelBindings() {
if (kKnxBridgeSupported) {
const int knx_uart = CONFIG_GATEWAY_KNX_TP_UART_PORT;
if (k485ControlEnabled && knx_uart == 0) {
if (kKnxBridgeStartupEnabled) {
const uint8_t knx_dali_bus_id = static_cast<uint8_t>(CONFIG_GATEWAY_KNX_DALI_BUS_ID);
int matches = 0;
for (int i = 0; i < CONFIG_GATEWAY_CHANNEL_COUNT; ++i) {
if (channels[i].enabled && channels[i].native_phy &&
channels[i].native_bus_id == knx_dali_bus_id) {
++matches;
}
}
if (matches != 1) {
ESP_LOGE(kTag,
"KNX DALI bus id %u must match exactly one enabled native DALI channel",
knx_dali_bus_id);
return false;
}
}
if (knx_uart >= 0 && k485ControlEnabled && knx_uart == 0) {
ESP_LOGE(kTag, "KNX TP UART0 conflicts with the UART0 control bridge");
return false;
}
if (knx_uart == 0 && kConsoleOnUart0) {
if (knx_uart >= 0 && knx_uart == 0 && kConsoleOnUart0) {
ESP_LOGE(kTag, "KNX TP-UART on UART0 requires moving the ESP-IDF console off UART0");
return false;
}
if (kModbusBridgeSupported && kModbusDefaultSerialTransport &&
if (knx_uart >= 0 && kModbusBridgeSupported && kModbusDefaultSerialTransport &&
knx_uart == CONFIG_GATEWAY_MODBUS_SERIAL_UART_PORT) {
ESP_LOGE(kTag, "KNX TP UART%d conflicts with default Modbus serial UART", knx_uart);
return false;
}
for (int i = 0; i < CONFIG_GATEWAY_CHANNEL_COUNT; ++i) {
if (channels[i].enabled && channels[i].serial_phy && channels[i].uart_port == knx_uart) {
if (knx_uart >= 0 && channels[i].enabled && channels[i].serial_phy &&
channels[i].uart_port == knx_uart) {
ESP_LOGE(kTag, "KNX TP UART%d conflicts with DALI channel %d serial PHY", knx_uart,
i + 1);
return false;
@@ -733,6 +789,8 @@ extern "C" void app_main(void) {
cache_config.full_state_mirror_enabled = cache_config.reconciliation_enabled &&
kCacheFullStateMirrorEnabled;
cache_config.flush_interval_ms = static_cast<uint32_t>(CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS);
cache_config.refresh_interval_ms =
static_cast<uint32_t>(CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS);
cache_config.default_priority_mode = kCachePriorityMode;
s_cache = std::make_unique<gateway::GatewayCache>(cache_config);
ESP_ERROR_CHECK(s_cache->start());
@@ -745,6 +803,9 @@ extern "C" void app_main(void) {
controller_config.ip_router_supported = network_transport_supported;
controller_config.internal_scene_supported = true;
controller_config.internal_group_supported = true;
controller_config.cache_supported = kCacheSupported;
controller_config.cache_refresh_interval_ms =
static_cast<uint32_t>(CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS);
s_controller = std::make_unique<gateway::GatewayController>(*s_runtime, *s_dali_domain,
*s_cache,
@@ -836,14 +897,36 @@ extern "C" void app_main(void) {
default_knx.tunnel_enabled = kKnxTunnelEnabled;
default_knx.multicast_enabled = kKnxMulticastEnabled;
default_knx.main_group = static_cast<uint8_t>(CONFIG_GATEWAY_KNX_MAIN_GROUP);
default_knx.dali_bus_id = static_cast<uint8_t>(CONFIG_GATEWAY_KNX_DALI_BUS_ID);
default_knx.udp_port = static_cast<uint16_t>(CONFIG_GATEWAY_KNX_UDP_PORT);
default_knx.multicast_address = CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS;
default_knx.ip_interface_individual_address =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS);
default_knx.individual_address =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS);
default_knx.programming_button_gpio = CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO;
default_knx.programming_led_gpio = CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO;
#ifdef CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW
default_knx.programming_button_active_low = true;
#else
default_knx.programming_button_active_low = false;
#endif
#ifdef CONFIG_GATEWAY_KNX_PROGRAMMING_LED_ACTIVE_HIGH
default_knx.programming_led_active_high = true;
#else
default_knx.programming_led_active_high = false;
#endif
default_knx.tp_uart.uart_port = CONFIG_GATEWAY_KNX_TP_UART_PORT;
default_knx.tp_uart.tx_pin = CONFIG_GATEWAY_KNX_TP_TX_PIN;
default_knx.tp_uart.rx_pin = CONFIG_GATEWAY_KNX_TP_RX_PIN;
default_knx.tp_uart.baudrate = static_cast<uint32_t>(CONFIG_GATEWAY_KNX_TP_BAUDRATE);
default_knx.tp_uart.startup_timeout_ms =
static_cast<uint32_t>(CONFIG_GATEWAY_KNX_TP_STARTUP_TIMEOUT_MS);
#ifdef CONFIG_GATEWAY_KNX_TP_UART_9BIT_MODE
default_knx.tp_uart.nine_bit_mode = true;
#else
default_knx.tp_uart.nine_bit_mode = false;
#endif
bridge_config.default_knx_config = default_knx;
}
bridge_config.knx_task_stack_size =
@@ -852,6 +935,7 @@ extern "C" void app_main(void) {
static_cast<UBaseType_t>(CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY);
s_bridge = std::make_unique<gateway::GatewayBridgeService>(*s_dali_domain, *s_cache,
bridge_config);
s_controller->setBridgeService(s_bridge.get());
}
if (profile.enable_wifi || profile.enable_eth) {
@@ -898,7 +982,10 @@ extern "C" void app_main(void) {
static_cast<uint32_t>(CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE);
network_config.status_led_gpio = CONFIG_GATEWAY_STATUS_LED_GPIO;
network_config.boot_button_gpio = CONFIG_GATEWAY_BOOT_BUTTON_GPIO;
network_config.setup_ap_button_gpio = CONFIG_GATEWAY_SETUP_AP_BUTTON_GPIO;
network_config.boot_button_long_press_ms = CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS;
network_config.boot_button_task_stack_size =
static_cast<uint32_t>(CONFIG_GATEWAY_BUTTON_TASK_STACK_SIZE);
#ifdef CONFIG_GATEWAY_STATUS_LED_ACTIVE_HIGH
network_config.status_led_active_high = true;
#else
@@ -908,6 +995,11 @@ extern "C" void app_main(void) {
network_config.boot_button_active_low = true;
#else
network_config.boot_button_active_low = false;
#endif
#ifdef CONFIG_GATEWAY_SETUP_AP_BUTTON_ACTIVE_LOW
network_config.setup_ap_button_active_low = true;
#else
network_config.setup_ap_button_active_low = false;
#endif
s_network = std::make_unique<gateway::GatewayNetworkService>(*s_controller, *s_runtime,
*s_dali_domain, network_config,
+6 -5
View File
@@ -1,6 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xf000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
factory, app, factory, 0x20000, 0x200000,
storage, data, spiffs, 0x220000, 0x180000,
nvs, data, nvs, 0x9000, 0x14000,
otadata, data, ota, 0x1D000, 0x2000,
phy_init, data, phy, 0x1F000, 0x1000,
factory, app, factory, 0x20000, 0x300000,
knxprops, data, 0x40, 0x320000, 0x60000,
storage, data, spiffs, 0x380000, 0x80000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x6000 0x14000
3 otadata data ota 0xf000 0x1D000 0x2000
4 phy_init data phy 0x11000 0x1F000 0x1000
5 factory app factory 0x20000 0x200000 0x300000
6 storage knxprops data spiffs 0x40 0x220000 0x320000 0x180000 0x60000
7 storage data spiffs 0x380000 0x80000
+115 -87
View File
@@ -596,38 +596,25 @@ CONFIG_PARTITION_TABLE_MD5=y
#
# Gateway App
#
CONFIG_GATEWAY_CHANNEL_COUNT=2
CONFIG_GATEWAY_CHANNEL_COUNT=1
#
# Gateway Channel 1
#
CONFIG_GATEWAY_CHANNEL1_GW_ID=3
# CONFIG_GATEWAY_CHANNEL1_PHY_DISABLED is not set
# CONFIG_GATEWAY_CHANNEL1_PHY_NATIVE is not set
CONFIG_GATEWAY_CHANNEL1_PHY_UART1=y
CONFIG_GATEWAY_CHANNEL1_PHY_NATIVE=y
# CONFIG_GATEWAY_CHANNEL1_PHY_UART1 is not set
# CONFIG_GATEWAY_CHANNEL1_PHY_UART2 is not set
CONFIG_GATEWAY_CHANNEL1_SERIAL_TX_PIN=1
CONFIG_GATEWAY_CHANNEL1_SERIAL_RX_PIN=2
CONFIG_GATEWAY_CHANNEL1_SERIAL_BAUDRATE=9600
CONFIG_GATEWAY_CHANNEL1_SERIAL_RX_BUFFER=512
CONFIG_GATEWAY_CHANNEL1_SERIAL_TX_BUFFER=512
CONFIG_GATEWAY_CHANNEL1_SERIAL_QUERY_TIMEOUT_MS=100
CONFIG_GATEWAY_CHANNEL1_NATIVE_BUS_ID=0
CONFIG_GATEWAY_CHANNEL1_NATIVE_TX_PIN=2
CONFIG_GATEWAY_CHANNEL1_NATIVE_RX_PIN=1
CONFIG_GATEWAY_CHANNEL1_NATIVE_BAUDRATE=1200
# end of Gateway Channel 1
#
# Gateway Channel 2
#
CONFIG_GATEWAY_CHANNEL2_GW_ID=4
# CONFIG_GATEWAY_CHANNEL2_PHY_DISABLED is not set
# CONFIG_GATEWAY_CHANNEL2_PHY_NATIVE is not set
# CONFIG_GATEWAY_CHANNEL2_PHY_UART1 is not set
CONFIG_GATEWAY_CHANNEL2_PHY_UART2=y
CONFIG_GATEWAY_CHANNEL2_SERIAL_TX_PIN=6
CONFIG_GATEWAY_CHANNEL2_SERIAL_RX_PIN=7
CONFIG_GATEWAY_CHANNEL2_SERIAL_BAUDRATE=9600
CONFIG_GATEWAY_CHANNEL2_SERIAL_RX_BUFFER=512
CONFIG_GATEWAY_CHANNEL2_SERIAL_TX_BUFFER=512
CONFIG_GATEWAY_CHANNEL2_SERIAL_QUERY_TIMEOUT_MS=100
# end of Gateway Channel 2
#
@@ -635,11 +622,9 @@ CONFIG_GATEWAY_CHANNEL2_SERIAL_QUERY_TIMEOUT_MS=100
#
CONFIG_GATEWAY_CACHE_SUPPORTED=y
CONFIG_GATEWAY_CACHE_START_ENABLED=y
CONFIG_GATEWAY_CACHE_RECONCILIATION_ENABLED=y
# CONFIG_GATEWAY_CACHE_FULL_STATE_MIRROR is not set
CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS=5000
CONFIG_GATEWAY_CACHE_OUTSIDE_BUS_FIRST=y
# CONFIG_GATEWAY_CACHE_LOCAL_GATEWAY_FIRST is not set
# CONFIG_GATEWAY_CACHE_RECONCILIATION_ENABLED is not set
CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS=10000
CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS=120000
# end of Gateway Cache
# CONFIG_GATEWAY_ENABLE_DALI_BUS is not set
@@ -663,17 +648,17 @@ CONFIG_GATEWAY_ETHERNET_IGNORE_INIT_FAILURE=y
#
# Gateway Wired Ethernet
#
CONFIG_GATEWAY_ETHERNET_W5500_SPI_HOST=1
CONFIG_GATEWAY_ETHERNET_W5500_SCLK_GPIO=14
CONFIG_GATEWAY_ETHERNET_W5500_MOSI_GPIO=13
CONFIG_GATEWAY_ETHERNET_W5500_MISO_GPIO=12
CONFIG_GATEWAY_ETHERNET_W5500_CS_GPIO=15
CONFIG_GATEWAY_ETHERNET_W5500_INT_GPIO=4
CONFIG_GATEWAY_ETHERNET_W5500_SPI_HOST=2
CONFIG_GATEWAY_ETHERNET_W5500_SCLK_GPIO=48
CONFIG_GATEWAY_ETHERNET_W5500_MOSI_GPIO=47
CONFIG_GATEWAY_ETHERNET_W5500_MISO_GPIO=33
CONFIG_GATEWAY_ETHERNET_W5500_CS_GPIO=34
CONFIG_GATEWAY_ETHERNET_W5500_INT_GPIO=36
CONFIG_GATEWAY_ETHERNET_W5500_POLL_PERIOD_MS=0
CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=36
CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO=5
CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=40
CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO=-1
CONFIG_GATEWAY_ETHERNET_PHY_ADDR=1
CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE=3072
CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE=4096
# end of Gateway Wired Ethernet
CONFIG_GATEWAY_BRIDGE_SUPPORTED=y
@@ -690,19 +675,33 @@ CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED=y
CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED=y
CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED=y
# CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED is not set
# CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS is not set
CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS=y
CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS=y
CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID=0x01e5
CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID=0xa401
CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER=0x0001
CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION=0x08
CONFIG_GATEWAY_KNX_MAIN_GROUP=0
CONFIG_GATEWAY_KNX_DALI_BUS_ID=0
# CONFIG_GATEWAY_KNX_DEBUG_DUMP_MEMORY is not set
CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y
CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y
CONFIG_GATEWAY_KNX_UDP_PORT=3671
CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS="224.0.23.12"
CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=4353
CONFIG_GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS=65281
CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=65534
CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO=0
CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW=y
CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO=10
# CONFIG_GATEWAY_KNX_PROGRAMMING_LED_ACTIVE_HIGH is not set
CONFIG_GATEWAY_KNX_TP_UART_PORT=0
CONFIG_GATEWAY_KNX_TP_TX_PIN=-1
CONFIG_GATEWAY_KNX_TP_RX_PIN=-1
CONFIG_GATEWAY_KNX_TP_BAUDRATE=19200
CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE=8192
CONFIG_GATEWAY_KNX_TP_STARTUP_TIMEOUT_MS=2000
CONFIG_GATEWAY_KNX_TP_UART_9BIT_MODE=y
CONFIG_GATEWAY_KNX_TP_FULL_IP_FORWARD=y
CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE=12288
CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY=5
CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set
@@ -726,6 +725,8 @@ CONFIG_GATEWAY_STATUS_LED_GPIO=-1
CONFIG_GATEWAY_BOOT_BUTTON_GPIO=0
CONFIG_GATEWAY_BOOT_BUTTON_ACTIVE_LOW=y
CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS=3000
CONFIG_GATEWAY_SETUP_AP_BUTTON_GPIO=-1
CONFIG_GATEWAY_BUTTON_TASK_STACK_SIZE=8192
# end of Gateway Network Services
# end of Gateway App
@@ -799,12 +800,12 @@ CONFIG_BT_CONTROLLER_ENABLED=y
#
# General
#
CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_INTERNAL=y
# CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL is not set
# CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_INTERNAL is not set
CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL=y
# CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_DEFAULT is not set
CONFIG_BT_NIMBLE_PINNED_TO_CORE=0
CONFIG_BT_NIMBLE_PINNED_TO_CORE_0=y
# CONFIG_BT_NIMBLE_PINNED_TO_CORE_1 is not set
CONFIG_BT_NIMBLE_PINNED_TO_CORE=1
# CONFIG_BT_NIMBLE_PINNED_TO_CORE_0 is not set
CONFIG_BT_NIMBLE_PINNED_TO_CORE_1=y
CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=4096
CONFIG_BT_NIMBLE_LEGACY_VHCI_ENABLE=y
# end of General
@@ -846,7 +847,7 @@ CONFIG_BT_NIMBLE_MAX_CONN_REATTEMPT=3
CONFIG_BT_NIMBLE_HS_PVCY=y
# CONFIG_BT_NIMBLE_HOST_ALLOW_CONNECT_WITH_SCAN is not set
# CONFIG_BT_NIMBLE_HOST_QUEUE_CONG_CHECK is not set
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=3
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=4
CONFIG_BT_NIMBLE_MAX_CCCDS=8
CONFIG_BT_NIMBLE_CRYPTO_STACK_MBEDTLS=y
CONFIG_BT_NIMBLE_HS_STOP_TIMEOUT_MS=2000
@@ -1009,10 +1010,10 @@ CONFIG_BT_NIMBLE_HCI_UART_CTS_PIN=23
# CONFIG_BT_NIMBLE_MEM_DEBUG is not set
# CONFIG_BT_NIMBLE_LOG_LEVEL_NONE is not set
# CONFIG_BT_NIMBLE_LOG_LEVEL_ERROR is not set
# CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING is not set
CONFIG_BT_NIMBLE_LOG_LEVEL_INFO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
# CONFIG_BT_NIMBLE_LOG_LEVEL_INFO is not set
# CONFIG_BT_NIMBLE_LOG_LEVEL_DEBUG is not set
CONFIG_BT_NIMBLE_LOG_LEVEL=1
CONFIG_BT_NIMBLE_LOG_LEVEL=2
CONFIG_BT_NIMBLE_PRINT_ERR_NAME=y
# CONFIG_BT_NIMBLE_DEBUG is not set
# CONFIG_BT_NIMBLE_TEST_THROUGHPUT_TEST is not set
@@ -1041,9 +1042,9 @@ CONFIG_BT_CTRL_MODE_EFF=1
CONFIG_BT_CTRL_BLE_MAX_ACT=6
CONFIG_BT_CTRL_BLE_MAX_ACT_EFF=6
CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB=0
CONFIG_BT_CTRL_PINNED_TO_CORE_0=y
# CONFIG_BT_CTRL_PINNED_TO_CORE_1 is not set
CONFIG_BT_CTRL_PINNED_TO_CORE=0
# CONFIG_BT_CTRL_PINNED_TO_CORE_0 is not set
CONFIG_BT_CTRL_PINNED_TO_CORE_1=y
CONFIG_BT_CTRL_PINNED_TO_CORE=1
CONFIG_BT_CTRL_HCI_MODE_VHCI=y
# CONFIG_BT_CTRL_HCI_MODE_UART_H4 is not set
CONFIG_BT_CTRL_HCI_TL=1
@@ -1306,15 +1307,15 @@ CONFIG_ESP_ERR_TO_NAME_LOOKUP=y
#
# ESP-Driver:GPIO Configurations
#
# CONFIG_GPIO_CTRL_FUNC_IN_IRAM is not set
CONFIG_GPIO_CTRL_FUNC_IN_IRAM=y
# end of ESP-Driver:GPIO Configurations
#
# ESP-Driver:GPTimer Configurations
#
CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM=y
# CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM is not set
# CONFIG_GPTIMER_ISR_CACHE_SAFE is not set
CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM=y
CONFIG_GPTIMER_ISR_CACHE_SAFE=y
CONFIG_GPTIMER_OBJ_CACHE_SAFE=y
# CONFIG_GPTIMER_ENABLE_DEBUG_LOG is not set
# end of ESP-Driver:GPTimer Configurations
@@ -1750,9 +1751,9 @@ CONFIG_ESP_ROM_PRINT_IN_IRAM=y
# ESP System Settings
#
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80 is not set
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240 is not set
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160 is not set
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240
#
# Cache config
@@ -1798,7 +1799,7 @@ CONFIG_ESP_SYSTEM_IN_IRAM=y
CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT=y
# CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT is not set
# CONFIG_ESP_SYSTEM_PANIC_GDBSTUB is not set
CONFIG_ESP_SYSTEM_PANIC_REBOOT_DELAY_SECONDS=0
CONFIG_ESP_SYSTEM_PANIC_REBOOT_DELAY_SECONDS=2
CONFIG_ESP_SYSTEM_RTC_FAST_MEM_AS_HEAP_DEPCHECK=y
CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP=y
@@ -1811,7 +1812,7 @@ CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=y
CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304
CONFIG_ESP_MAIN_TASK_STACK_SIZE=3584
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y
# CONFIG_ESP_MAIN_TASK_AFFINITY_CPU1 is not set
# CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set
@@ -1878,9 +1879,9 @@ CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER=y
CONFIG_ESP_WIFI_TX_BUFFER_TYPE=1
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32
CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER=y
# CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUFFER is not set
CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=0
# CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER is not set
CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUFFER=y
CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=1
CONFIG_ESP_WIFI_RX_MGMT_BUF_NUM_DEF=5
# CONFIG_ESP_WIFI_CSI_ENABLED is not set
CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y
@@ -1888,8 +1889,8 @@ CONFIG_ESP_WIFI_TX_BA_WIN=6
CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y
CONFIG_ESP_WIFI_RX_BA_WIN=6
CONFIG_ESP_WIFI_NVS_ENABLED=y
CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_0=y
# CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1 is not set
# CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_0 is not set
CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1=y
CONFIG_ESP_WIFI_SOFTAP_BEACON_MAX_LEN=752
CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32
CONFIG_ESP_WIFI_IRAM_OPT=y
@@ -2006,12 +2007,12 @@ CONFIG_FATFS_DONT_TRUST_LAST_ALLOC=0
#
# CONFIG_FREERTOS_SMP is not set
# CONFIG_FREERTOS_UNICORE is not set
CONFIG_FREERTOS_HZ=100
CONFIG_FREERTOS_HZ=1000
# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE is not set
# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL is not set
CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y
CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=1
CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1536
CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=2048
# CONFIG_FREERTOS_USE_IDLE_HOOK is not set
# CONFIG_FREERTOS_USE_TICK_HOOK is not set
CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16
@@ -2184,8 +2185,7 @@ CONFIG_LWIP_GARP_TMR_INTERVAL=60
CONFIG_LWIP_ESP_MLDV6_REPORT=y
CONFIG_LWIP_MLDV6_TMR_INTERVAL=40
CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32
CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y
# CONFIG_LWIP_DHCP_DOES_ACD_CHECK is not set
CONFIG_LWIP_DHCP_DOES_ACD_CHECK=y
# CONFIG_LWIP_DHCP_DOES_NOT_CHECK_OFFERED_IP is not set
# CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set
CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y
@@ -2204,13 +2204,16 @@ CONFIG_LWIP_DHCPS_STATIC_ENTRIES=y
CONFIG_LWIP_DHCPS_ADD_DNS=y
# end of DHCP server
# CONFIG_LWIP_AUTOIP is not set
CONFIG_LWIP_AUTOIP=y
CONFIG_LWIP_AUTOIP_TRIES=2
CONFIG_LWIP_AUTOIP_MAX_CONFLICTS=9
CONFIG_LWIP_AUTOIP_RATE_LIMIT_INTERVAL=20
CONFIG_LWIP_IPV4=y
CONFIG_LWIP_IPV6=y
# CONFIG_LWIP_IPV6_AUTOCONFIG is not set
CONFIG_LWIP_IPV6_NUM_ADDRESSES=3
# CONFIG_LWIP_IPV6_FORWARD is not set
# CONFIG_LWIP_NETIF_STATUS_CALLBACK is not set
CONFIG_LWIP_NETIF_STATUS_CALLBACK=y
CONFIG_LWIP_NETIF_LOOPBACK=y
CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8
@@ -2774,13 +2777,36 @@ CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y
#
CONFIG_DALI_PHY_COUNT=16
CONFIG_DALI_DEFAULT_BAUDRATE=1200
CONFIG_DALI_API_QUEUE_LEN=10
CONFIG_DALI_TX_QUEUE_LEN=1
CONFIG_DALI_TIMER_RESOLUTION_HZ=3636363
CONFIG_DALI_CUSTOM_HALF_BIT_TIME_X100_US=0
CONFIG_DALI_TX_STOP_CONDITION_US=0
CONFIG_DALI_RX_STOP_CONDITION_US=0
CONFIG_DALI_QUERY_RESPONSE_TIMEOUT_MS=30
CONFIG_DALI_DOUBLE_SEND_DELAY_MS=20
CONFIG_DALI_FORWARD_ACTIVITY_WAIT_MS=20
CONFIG_DALI_FORWARD_AFTER_BACKWARD_WAIT_MS=10
CONFIG_DALI_FORWARD_MAX_WAIT_MS=50
CONFIG_DALI_BACKWARD_IDLE_TIMEOUT_MS=10
CONFIG_DALI_BUS_POWER_CHECK_INTERVAL_MS=500
CONFIG_DALI_BUS_ABNORMAL_REPORT_INTERVAL_MS=3000
# CONFIG_DALI_LOG_LEVEL_NONE is not set
# CONFIG_DALI_LOG_LEVEL_ERROR is not set
# CONFIG_DALI_LOG_LEVEL_WARN is not set
# CONFIG_DALI_LOG_LEVEL_INFO is not set
CONFIG_DALI_LOG_LEVEL_DEBUG=y
# CONFIG_DALI_LOG_LEVEL_VERBOSE is not set
CONFIG_DALI_LOG_LEVEL=4
CONFIG_DALI_TX_ACTIVE_LOW=y
# CONFIG_DALI_TX_ACTIVE_HIGH is not set
CONFIG_DALI_RX_ACTIVE_LOW=y
# CONFIG_DALI_RX_ACTIVE_HIGH is not set
CONFIG_DALI_API_QUEUE_LEN=64
CONFIG_DALI_TX_QUEUE_LEN=4
CONFIG_DALI_TX_REPLY_QUEUE_LEN=4
CONFIG_DALI_RX_QUEUE_LEN=50
CONFIG_DALI_DEBUG_QUEUE_LEN=100
# CONFIG_DALI_ENABLE_DEBUG_TASK is not set
CONFIG_DALI_DALI_TASK_STACK_SIZE=2048
CONFIG_DALI_ENABLE_DEBUG_TASK=y
CONFIG_DALI_DALI_TASK_STACK_SIZE=8192
CONFIG_DALI_DALI_TASK_PRIORITY=2
CONFIG_DALI_DEBUG_TASK_STACK_SIZE=2048
CONFIG_DALI_DEBUG_TASK_PRIORITY=1
@@ -2802,10 +2828,12 @@ CONFIG_MQTT_TRANSPORT_SSL=y
CONFIG_MQTT_TRANSPORT_WEBSOCKET=y
CONFIG_MQTT_TRANSPORT_WEBSOCKET_SECURE=y
# CONFIG_MQTT_MSG_ID_INCREMENTAL is not set
# CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED is not set
CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED=y
# CONFIG_MQTT_REPORT_DELETED_MESSAGES is not set
# CONFIG_MQTT_USE_CUSTOM_CONFIG is not set
# CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED is not set
CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED=y
# CONFIG_MQTT_USE_CORE_0 is not set
CONFIG_MQTT_USE_CORE_1=y
# CONFIG_MQTT_CUSTOM_OUTBOX is not set
# end of ESP-MQTT Configurations
# end of Component config
@@ -2849,12 +2877,12 @@ CONFIG_ESP32_APPTRACE_DEST_NONE=y
CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y
# CONFIG_BLUEDROID_ENABLED is not set
CONFIG_NIMBLE_ENABLED=y
CONFIG_NIMBLE_MEM_ALLOC_MODE_INTERNAL=y
# CONFIG_NIMBLE_MEM_ALLOC_MODE_EXTERNAL is not set
# CONFIG_NIMBLE_MEM_ALLOC_MODE_INTERNAL is not set
CONFIG_NIMBLE_MEM_ALLOC_MODE_EXTERNAL=y
# CONFIG_NIMBLE_MEM_ALLOC_MODE_DEFAULT is not set
CONFIG_NIMBLE_PINNED_TO_CORE=0
CONFIG_NIMBLE_PINNED_TO_CORE_0=y
# CONFIG_NIMBLE_PINNED_TO_CORE_1 is not set
CONFIG_NIMBLE_PINNED_TO_CORE=1
# CONFIG_NIMBLE_PINNED_TO_CORE_0 is not set
CONFIG_NIMBLE_PINNED_TO_CORE_1=y
CONFIG_NIMBLE_TASK_STACK_SIZE=4096
CONFIG_BT_NIMBLE_TASK_STACK_SIZE=4096
CONFIG_NIMBLE_ROLE_CENTRAL=y
@@ -2868,7 +2896,7 @@ CONFIG_BT_NIMBLE_SM_SC_LVL=0
# CONFIG_NIMBLE_NVS_PERSIST is not set
CONFIG_NIMBLE_MAX_BONDS=3
CONFIG_NIMBLE_RPA_TIMEOUT=900
CONFIG_NIMBLE_MAX_CONNECTIONS=3
CONFIG_NIMBLE_MAX_CONNECTIONS=4
CONFIG_NIMBLE_MAX_CCCDS=8
CONFIG_NIMBLE_CRYPTO_STACK_MBEDTLS=y
# CONFIG_NIMBLE_HS_FLOW_CTRL is not set
@@ -2891,7 +2919,7 @@ CONFIG_SW_COEXIST_ENABLE=y
CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=y
CONFIG_ESP_WIFI_SW_COEXIST_ENABLE=y
# CONFIG_CAM_CTLR_DVP_CAM_ISR_IRAM_SAFE is not set
# CONFIG_GPTIMER_ISR_IRAM_SAFE is not set
CONFIG_GPTIMER_ISR_IRAM_SAFE=y
# CONFIG_MCPWM_ISR_IRAM_SAFE is not set
# CONFIG_EVENT_LOOP_PROFILING is not set
CONFIG_POST_EVENTS_FROM_ISR=y
@@ -2938,12 +2966,12 @@ CONFIG_ESP32S3_SPIRAM_SUPPORT=y
CONFIG_DEFAULT_PSRAM_CLK_IO=30
CONFIG_DEFAULT_PSRAM_CS_IO=26
# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_80 is not set
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160=y
# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240 is not set
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=160
# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160 is not set
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240
CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32
CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304
CONFIG_MAIN_TASK_STACK_SIZE=3584
CONFIG_MAIN_TASK_STACK_SIZE=8192
# CONFIG_CONSOLE_UART_DEFAULT is not set
# CONFIG_CONSOLE_UART_CUSTOM is not set
# CONFIG_CONSOLE_UART_NONE is not set
@@ -2975,8 +3003,8 @@ CONFIG_ESP32_WIFI_TX_BA_WIN=6
CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y
CONFIG_ESP32_WIFI_RX_BA_WIN=6
CONFIG_ESP32_WIFI_NVS_ENABLED=y
CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0=y
# CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1 is not set
# CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0 is not set
CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1=y
CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752
CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32
CONFIG_ESP32_WIFI_IRAM_OPT=y
+2
View File
@@ -7,6 +7,8 @@ CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_OFFSET=0x8000
CONFIG_PARTITION_TABLE_MD5=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y
File diff suppressed because it is too large Load Diff
+105 -77
View File
@@ -596,38 +596,25 @@ CONFIG_PARTITION_TABLE_MD5=y
#
# Gateway App
#
CONFIG_GATEWAY_CHANNEL_COUNT=2
CONFIG_GATEWAY_CHANNEL_COUNT=1
#
# Gateway Channel 1
#
CONFIG_GATEWAY_CHANNEL1_GW_ID=3
# CONFIG_GATEWAY_CHANNEL1_PHY_DISABLED is not set
# CONFIG_GATEWAY_CHANNEL1_PHY_NATIVE is not set
CONFIG_GATEWAY_CHANNEL1_PHY_UART1=y
CONFIG_GATEWAY_CHANNEL1_PHY_NATIVE=y
# CONFIG_GATEWAY_CHANNEL1_PHY_UART1 is not set
# CONFIG_GATEWAY_CHANNEL1_PHY_UART2 is not set
CONFIG_GATEWAY_CHANNEL1_SERIAL_TX_PIN=1
CONFIG_GATEWAY_CHANNEL1_SERIAL_RX_PIN=2
CONFIG_GATEWAY_CHANNEL1_SERIAL_BAUDRATE=9600
CONFIG_GATEWAY_CHANNEL1_SERIAL_RX_BUFFER=512
CONFIG_GATEWAY_CHANNEL1_SERIAL_TX_BUFFER=512
CONFIG_GATEWAY_CHANNEL1_SERIAL_QUERY_TIMEOUT_MS=100
CONFIG_GATEWAY_CHANNEL1_NATIVE_BUS_ID=0
CONFIG_GATEWAY_CHANNEL1_NATIVE_TX_PIN=2
CONFIG_GATEWAY_CHANNEL1_NATIVE_RX_PIN=1
CONFIG_GATEWAY_CHANNEL1_NATIVE_BAUDRATE=1200
# end of Gateway Channel 1
#
# Gateway Channel 2
#
CONFIG_GATEWAY_CHANNEL2_GW_ID=4
# CONFIG_GATEWAY_CHANNEL2_PHY_DISABLED is not set
# CONFIG_GATEWAY_CHANNEL2_PHY_NATIVE is not set
# CONFIG_GATEWAY_CHANNEL2_PHY_UART1 is not set
CONFIG_GATEWAY_CHANNEL2_PHY_UART2=y
CONFIG_GATEWAY_CHANNEL2_SERIAL_TX_PIN=6
CONFIG_GATEWAY_CHANNEL2_SERIAL_RX_PIN=7
CONFIG_GATEWAY_CHANNEL2_SERIAL_BAUDRATE=9600
CONFIG_GATEWAY_CHANNEL2_SERIAL_RX_BUFFER=512
CONFIG_GATEWAY_CHANNEL2_SERIAL_TX_BUFFER=512
CONFIG_GATEWAY_CHANNEL2_SERIAL_QUERY_TIMEOUT_MS=100
# end of Gateway Channel 2
#
@@ -635,11 +622,9 @@ CONFIG_GATEWAY_CHANNEL2_SERIAL_QUERY_TIMEOUT_MS=100
#
CONFIG_GATEWAY_CACHE_SUPPORTED=y
CONFIG_GATEWAY_CACHE_START_ENABLED=y
CONFIG_GATEWAY_CACHE_RECONCILIATION_ENABLED=y
# CONFIG_GATEWAY_CACHE_FULL_STATE_MIRROR is not set
CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS=5000
CONFIG_GATEWAY_CACHE_OUTSIDE_BUS_FIRST=y
# CONFIG_GATEWAY_CACHE_LOCAL_GATEWAY_FIRST is not set
# CONFIG_GATEWAY_CACHE_RECONCILIATION_ENABLED is not set
CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS=10000
CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS=120000
# end of Gateway Cache
# CONFIG_GATEWAY_ENABLE_DALI_BUS is not set
@@ -663,17 +648,17 @@ CONFIG_GATEWAY_ETHERNET_IGNORE_INIT_FAILURE=y
#
# Gateway Wired Ethernet
#
CONFIG_GATEWAY_ETHERNET_W5500_SPI_HOST=1
CONFIG_GATEWAY_ETHERNET_W5500_SCLK_GPIO=14
CONFIG_GATEWAY_ETHERNET_W5500_MOSI_GPIO=13
CONFIG_GATEWAY_ETHERNET_W5500_MISO_GPIO=12
CONFIG_GATEWAY_ETHERNET_W5500_CS_GPIO=15
CONFIG_GATEWAY_ETHERNET_W5500_INT_GPIO=4
CONFIG_GATEWAY_ETHERNET_W5500_SPI_HOST=2
CONFIG_GATEWAY_ETHERNET_W5500_SCLK_GPIO=48
CONFIG_GATEWAY_ETHERNET_W5500_MOSI_GPIO=47
CONFIG_GATEWAY_ETHERNET_W5500_MISO_GPIO=33
CONFIG_GATEWAY_ETHERNET_W5500_CS_GPIO=34
CONFIG_GATEWAY_ETHERNET_W5500_INT_GPIO=36
CONFIG_GATEWAY_ETHERNET_W5500_POLL_PERIOD_MS=0
CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=36
CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO=5
CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=40
CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO=-1
CONFIG_GATEWAY_ETHERNET_PHY_ADDR=1
CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE=3072
CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE=4096
# end of Gateway Wired Ethernet
CONFIG_GATEWAY_BRIDGE_SUPPORTED=y
@@ -690,19 +675,33 @@ CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED=y
CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED=y
CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED=y
# CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED is not set
# CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS is not set
CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS=y
CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS=y
CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID=0x01e5
CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID=0xa401
CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER=0x0001
CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION=0x08
CONFIG_GATEWAY_KNX_MAIN_GROUP=0
CONFIG_GATEWAY_KNX_DALI_BUS_ID=0
# CONFIG_GATEWAY_KNX_DEBUG_DUMP_MEMORY is not set
CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y
CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y
CONFIG_GATEWAY_KNX_UDP_PORT=3671
CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS="224.0.23.12"
CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=4353
CONFIG_GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS=65281
CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=65534
CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO=0
CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW=y
CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO=10
# CONFIG_GATEWAY_KNX_PROGRAMMING_LED_ACTIVE_HIGH is not set
CONFIG_GATEWAY_KNX_TP_UART_PORT=0
CONFIG_GATEWAY_KNX_TP_TX_PIN=-1
CONFIG_GATEWAY_KNX_TP_RX_PIN=-1
CONFIG_GATEWAY_KNX_TP_BAUDRATE=19200
CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE=8192
CONFIG_GATEWAY_KNX_TP_STARTUP_TIMEOUT_MS=2000
CONFIG_GATEWAY_KNX_TP_UART_9BIT_MODE=y
CONFIG_GATEWAY_KNX_TP_FULL_IP_FORWARD=y
CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE=12288
CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY=5
CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set
@@ -726,6 +725,8 @@ CONFIG_GATEWAY_STATUS_LED_GPIO=-1
CONFIG_GATEWAY_BOOT_BUTTON_GPIO=0
CONFIG_GATEWAY_BOOT_BUTTON_ACTIVE_LOW=y
CONFIG_GATEWAY_BOOT_BUTTON_LONG_PRESS_MS=3000
CONFIG_GATEWAY_SETUP_AP_BUTTON_GPIO=-1
CONFIG_GATEWAY_BUTTON_TASK_STACK_SIZE=8192
# end of Gateway Network Services
# end of Gateway App
@@ -799,12 +800,12 @@ CONFIG_BT_CONTROLLER_ENABLED=y
#
# General
#
CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_INTERNAL=y
# CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL is not set
# CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_INTERNAL is not set
CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL=y
# CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_DEFAULT is not set
CONFIG_BT_NIMBLE_PINNED_TO_CORE=0
CONFIG_BT_NIMBLE_PINNED_TO_CORE_0=y
# CONFIG_BT_NIMBLE_PINNED_TO_CORE_1 is not set
CONFIG_BT_NIMBLE_PINNED_TO_CORE=1
# CONFIG_BT_NIMBLE_PINNED_TO_CORE_0 is not set
CONFIG_BT_NIMBLE_PINNED_TO_CORE_1=y
CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=4096
CONFIG_BT_NIMBLE_LEGACY_VHCI_ENABLE=y
# end of General
@@ -846,7 +847,7 @@ CONFIG_BT_NIMBLE_MAX_CONN_REATTEMPT=3
CONFIG_BT_NIMBLE_HS_PVCY=y
# CONFIG_BT_NIMBLE_HOST_ALLOW_CONNECT_WITH_SCAN is not set
# CONFIG_BT_NIMBLE_HOST_QUEUE_CONG_CHECK is not set
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=3
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=4
CONFIG_BT_NIMBLE_MAX_CCCDS=8
CONFIG_BT_NIMBLE_CRYPTO_STACK_MBEDTLS=y
CONFIG_BT_NIMBLE_HS_STOP_TIMEOUT_MS=2000
@@ -1009,10 +1010,10 @@ CONFIG_BT_NIMBLE_HCI_UART_CTS_PIN=23
# CONFIG_BT_NIMBLE_MEM_DEBUG is not set
# CONFIG_BT_NIMBLE_LOG_LEVEL_NONE is not set
# CONFIG_BT_NIMBLE_LOG_LEVEL_ERROR is not set
# CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING is not set
CONFIG_BT_NIMBLE_LOG_LEVEL_INFO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
# CONFIG_BT_NIMBLE_LOG_LEVEL_INFO is not set
# CONFIG_BT_NIMBLE_LOG_LEVEL_DEBUG is not set
CONFIG_BT_NIMBLE_LOG_LEVEL=1
CONFIG_BT_NIMBLE_LOG_LEVEL=2
CONFIG_BT_NIMBLE_PRINT_ERR_NAME=y
# CONFIG_BT_NIMBLE_DEBUG is not set
# CONFIG_BT_NIMBLE_TEST_THROUGHPUT_TEST is not set
@@ -1041,9 +1042,9 @@ CONFIG_BT_CTRL_MODE_EFF=1
CONFIG_BT_CTRL_BLE_MAX_ACT=6
CONFIG_BT_CTRL_BLE_MAX_ACT_EFF=6
CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB=0
CONFIG_BT_CTRL_PINNED_TO_CORE_0=y
# CONFIG_BT_CTRL_PINNED_TO_CORE_1 is not set
CONFIG_BT_CTRL_PINNED_TO_CORE=0
# CONFIG_BT_CTRL_PINNED_TO_CORE_0 is not set
CONFIG_BT_CTRL_PINNED_TO_CORE_1=y
CONFIG_BT_CTRL_PINNED_TO_CORE=1
CONFIG_BT_CTRL_HCI_MODE_VHCI=y
# CONFIG_BT_CTRL_HCI_MODE_UART_H4 is not set
CONFIG_BT_CTRL_HCI_TL=1
@@ -1075,12 +1076,12 @@ CONFIG_BT_CTRL_RX_ANTENNA_INDEX_EFF=0
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N0 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P3 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P6 is not set
CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P9=y
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P9 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P12 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P15 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P18 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P20 is not set
CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_EFF=11
CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P20=y
CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_EFF=15
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_SUPP=y
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=100
CONFIG_BT_CTRL_BLE_ADV_REPORT_DISCARD_THRSHOLD=20
@@ -1306,15 +1307,15 @@ CONFIG_ESP_ERR_TO_NAME_LOOKUP=y
#
# ESP-Driver:GPIO Configurations
#
# CONFIG_GPIO_CTRL_FUNC_IN_IRAM is not set
CONFIG_GPIO_CTRL_FUNC_IN_IRAM=y
# end of ESP-Driver:GPIO Configurations
#
# ESP-Driver:GPTimer Configurations
#
CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM=y
# CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM is not set
# CONFIG_GPTIMER_ISR_CACHE_SAFE is not set
CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM=y
CONFIG_GPTIMER_ISR_CACHE_SAFE=y
CONFIG_GPTIMER_OBJ_CACHE_SAFE=y
# CONFIG_GPTIMER_ENABLE_DEBUG_LOG is not set
# end of ESP-Driver:GPTimer Configurations
@@ -1750,9 +1751,9 @@ CONFIG_ESP_ROM_PRINT_IN_IRAM=y
# ESP System Settings
#
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80 is not set
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240 is not set
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160 is not set
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240
#
# Cache config
@@ -1798,7 +1799,7 @@ CONFIG_ESP_SYSTEM_IN_IRAM=y
CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT=y
# CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT is not set
# CONFIG_ESP_SYSTEM_PANIC_GDBSTUB is not set
CONFIG_ESP_SYSTEM_PANIC_REBOOT_DELAY_SECONDS=0
CONFIG_ESP_SYSTEM_PANIC_REBOOT_DELAY_SECONDS=2
CONFIG_ESP_SYSTEM_RTC_FAST_MEM_AS_HEAP_DEPCHECK=y
CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP=y
@@ -1811,7 +1812,7 @@ CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=y
CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304
CONFIG_ESP_MAIN_TASK_STACK_SIZE=3584
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y
# CONFIG_ESP_MAIN_TASK_AFFINITY_CPU1 is not set
# CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set
@@ -1878,9 +1879,9 @@ CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER=y
CONFIG_ESP_WIFI_TX_BUFFER_TYPE=1
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32
CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER=y
# CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUFFER is not set
CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=0
# CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER is not set
CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUFFER=y
CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=1
CONFIG_ESP_WIFI_RX_MGMT_BUF_NUM_DEF=5
# CONFIG_ESP_WIFI_CSI_ENABLED is not set
CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y
@@ -1888,8 +1889,8 @@ CONFIG_ESP_WIFI_TX_BA_WIN=6
CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y
CONFIG_ESP_WIFI_RX_BA_WIN=6
CONFIG_ESP_WIFI_NVS_ENABLED=y
CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_0=y
# CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1 is not set
# CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_0 is not set
CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1=y
CONFIG_ESP_WIFI_SOFTAP_BEACON_MAX_LEN=752
CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32
CONFIG_ESP_WIFI_IRAM_OPT=y
@@ -2006,12 +2007,12 @@ CONFIG_FATFS_DONT_TRUST_LAST_ALLOC=0
#
# CONFIG_FREERTOS_SMP is not set
# CONFIG_FREERTOS_UNICORE is not set
CONFIG_FREERTOS_HZ=100
CONFIG_FREERTOS_HZ=1000
# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE is not set
# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL is not set
CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y
CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=1
CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1536
CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=2048
# CONFIG_FREERTOS_USE_IDLE_HOOK is not set
# CONFIG_FREERTOS_USE_TICK_HOOK is not set
CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16
@@ -2184,8 +2185,7 @@ CONFIG_LWIP_GARP_TMR_INTERVAL=60
CONFIG_LWIP_ESP_MLDV6_REPORT=y
CONFIG_LWIP_MLDV6_TMR_INTERVAL=40
CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32
CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y
# CONFIG_LWIP_DHCP_DOES_ACD_CHECK is not set
CONFIG_LWIP_DHCP_DOES_ACD_CHECK=y
# CONFIG_LWIP_DHCP_DOES_NOT_CHECK_OFFERED_IP is not set
# CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set
CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y
@@ -2204,13 +2204,16 @@ CONFIG_LWIP_DHCPS_STATIC_ENTRIES=y
CONFIG_LWIP_DHCPS_ADD_DNS=y
# end of DHCP server
# CONFIG_LWIP_AUTOIP is not set
CONFIG_LWIP_AUTOIP=y
CONFIG_LWIP_AUTOIP_TRIES=2
CONFIG_LWIP_AUTOIP_MAX_CONFLICTS=9
CONFIG_LWIP_AUTOIP_RATE_LIMIT_INTERVAL=20
CONFIG_LWIP_IPV4=y
CONFIG_LWIP_IPV6=y
# CONFIG_LWIP_IPV6_AUTOCONFIG is not set
CONFIG_LWIP_IPV6_NUM_ADDRESSES=3
# CONFIG_LWIP_IPV6_FORWARD is not set
# CONFIG_LWIP_NETIF_STATUS_CALLBACK is not set
CONFIG_LWIP_NETIF_STATUS_CALLBACK=y
CONFIG_LWIP_NETIF_LOOPBACK=y
CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8
@@ -2774,13 +2777,36 @@ CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y
#
CONFIG_DALI_PHY_COUNT=16
CONFIG_DALI_DEFAULT_BAUDRATE=1200
CONFIG_DALI_API_QUEUE_LEN=10
CONFIG_DALI_TX_QUEUE_LEN=1
CONFIG_DALI_TIMER_RESOLUTION_HZ=3636363
CONFIG_DALI_CUSTOM_HALF_BIT_TIME_X100_US=0
CONFIG_DALI_TX_STOP_CONDITION_US=0
CONFIG_DALI_RX_STOP_CONDITION_US=0
CONFIG_DALI_QUERY_RESPONSE_TIMEOUT_MS=25
CONFIG_DALI_DOUBLE_SEND_DELAY_MS=15
CONFIG_DALI_FORWARD_ACTIVITY_WAIT_MS=15
CONFIG_DALI_FORWARD_AFTER_BACKWARD_WAIT_MS=5
CONFIG_DALI_FORWARD_MAX_WAIT_MS=50
CONFIG_DALI_BACKWARD_IDLE_TIMEOUT_MS=10
CONFIG_DALI_BUS_POWER_CHECK_INTERVAL_MS=500
CONFIG_DALI_BUS_ABNORMAL_REPORT_INTERVAL_MS=3000
# CONFIG_DALI_LOG_LEVEL_NONE is not set
# CONFIG_DALI_LOG_LEVEL_ERROR is not set
# CONFIG_DALI_LOG_LEVEL_WARN is not set
# CONFIG_DALI_LOG_LEVEL_INFO is not set
CONFIG_DALI_LOG_LEVEL_DEBUG=y
# CONFIG_DALI_LOG_LEVEL_VERBOSE is not set
CONFIG_DALI_LOG_LEVEL=4
CONFIG_DALI_TX_ACTIVE_LOW=y
# CONFIG_DALI_TX_ACTIVE_HIGH is not set
CONFIG_DALI_RX_ACTIVE_LOW=y
# CONFIG_DALI_RX_ACTIVE_HIGH is not set
CONFIG_DALI_API_QUEUE_LEN=64
CONFIG_DALI_TX_QUEUE_LEN=4
CONFIG_DALI_TX_REPLY_QUEUE_LEN=4
CONFIG_DALI_RX_QUEUE_LEN=50
CONFIG_DALI_DEBUG_QUEUE_LEN=100
# CONFIG_DALI_ENABLE_DEBUG_TASK is not set
CONFIG_DALI_DALI_TASK_STACK_SIZE=2048
CONFIG_DALI_ENABLE_DEBUG_TASK=y
CONFIG_DALI_DALI_TASK_STACK_SIZE=8192
CONFIG_DALI_DALI_TASK_PRIORITY=2
CONFIG_DALI_DEBUG_TASK_STACK_SIZE=2048
CONFIG_DALI_DEBUG_TASK_PRIORITY=1
@@ -2802,10 +2828,12 @@ CONFIG_MQTT_TRANSPORT_SSL=y
CONFIG_MQTT_TRANSPORT_WEBSOCKET=y
CONFIG_MQTT_TRANSPORT_WEBSOCKET_SECURE=y
# CONFIG_MQTT_MSG_ID_INCREMENTAL is not set
# CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED is not set
CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED=y
# CONFIG_MQTT_REPORT_DELETED_MESSAGES is not set
# CONFIG_MQTT_USE_CUSTOM_CONFIG is not set
# CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED is not set
CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED=y
# CONFIG_MQTT_USE_CORE_0 is not set
CONFIG_MQTT_USE_CORE_1=y
# CONFIG_MQTT_CUSTOM_OUTBOX is not set
# end of ESP-MQTT Configurations
# end of Component config
+169 -1
View File
@@ -14,6 +14,174 @@ config DALI_DEFAULT_BAUDRATE
help
Default baudrate used during initialization.
config DALI_TIMER_RESOLUTION_HZ
int "Native DALI timer resolution Hz"
range 1000000 8000000
default 3636363
help
GPTimer resolution for native DALI Manchester timing. The default
3.636363 MHz timing matches the gateway's tuned 1200 bps behavior for
older control gear that needs more recovery margin between commands.
config DALI_CUSTOM_HALF_BIT_TIME_X100_US
int "Custom native DALI half-bit time x100 us"
range 0 500000
default 0
help
Development override for the native DALI half-bit period, expressed in
1/100 us. Set to 0 to derive the value from the configured baudrate.
Standard 1200 bps DALI is 41667, meaning 416.67 us.
config DALI_TX_STOP_CONDITION_US
int "Custom TX stop condition us"
range 0 10000
default 0
help
Development override for the native TX stop-condition wait. Set to 0 to
use the scaled standard timing.
config DALI_RX_STOP_CONDITION_US
int "Custom RX stop condition us"
range 0 10000
default 0
help
Development override for the native RX stop-condition wait. Set to 0 to
use the scaled standard timing.
config DALI_QUERY_RESPONSE_TIMEOUT_MS
int "DALI query response timeout ms"
range 10 100
default 30
help
Time to wait for a complete backward frame after a forward query has
finished transmitting. DALI backward frames start 5.5-10.5 ms after the
forward frame and last about 9.95 ms, so 25 ms leaves margin without the
legacy 50 ms no-response delay.
config DALI_DOUBLE_SEND_DELAY_MS
int "Double-send delay ms"
range 0 100
default 20
help
Delay between the two frames sent by dali_send_double(), measured after
the first frame has completed. Exposed for development tuning.
config DALI_FORWARD_ACTIVITY_WAIT_MS
int "Forward-frame wait after bus activity ms"
range 0 1000
default 20
help
Minimum delay before sending a 2- to 4-byte forward frame after normal
bus activity. This is the native queue anti-collision wait.
config DALI_FORWARD_AFTER_BACKWARD_WAIT_MS
int "Forward-frame wait after backward frame ms"
range 0 1000
default 10
help
Minimum delay before sending a 2- to 4-byte forward frame after the last
valid 1-byte backward frame received by the native bus.
config DALI_FORWARD_MAX_WAIT_MS
int "Forward-frame maximum queue wait ms"
range 0 5000
default 50
help
Maximum time the native queue waits for a forward-frame send window. If
no valid send window is reached before this timeout, the frame is
dropped with an error status.
config DALI_BACKWARD_IDLE_TIMEOUT_MS
int "Backward-frame idle wait timeout ms"
range 0 1000
default 10
help
Time a 1-byte backward frame waits for an idle bus before being sent
anyway. Backward frame sends are not echo-verified because collisions
during addressing responses are valid DALI behavior.
config DALI_BUS_POWER_CHECK_INTERVAL_MS
int "Bus power-down check interval ms"
range 10 5000
default 500
help
Interval used to resample the RX pin while the native DALI bus is marked
power-down. This lets the HAL recover when the bus was already powered
before gateway startup and no RX edge is generated.
config DALI_BUS_ABNORMAL_REPORT_INTERVAL_MS
int "Legacy bus abnormal report interval ms"
range 0 60000
default 3000
help
Interval for publishing the legacy two-byte FF FD bus-abnormal raw frame
while the native DALI bus is power-down. Set to 0 to disable the report.
choice DALI_LOG_LEVEL_CHOICE
prompt "DALI log level"
default DALI_LOG_LEVEL_WARN
help
Runtime log level applied to the native DALI HAL ESP-IDF log tag.
config DALI_LOG_LEVEL_NONE
bool "No output"
config DALI_LOG_LEVEL_ERROR
bool "Error"
config DALI_LOG_LEVEL_WARN
bool "Warning"
config DALI_LOG_LEVEL_INFO
bool "Info"
config DALI_LOG_LEVEL_DEBUG
bool "Debug"
config DALI_LOG_LEVEL_VERBOSE
bool "Verbose"
endchoice
config DALI_LOG_LEVEL
int
default 0 if DALI_LOG_LEVEL_NONE
default 1 if DALI_LOG_LEVEL_ERROR
default 2 if DALI_LOG_LEVEL_WARN
default 3 if DALI_LOG_LEVEL_INFO
default 4 if DALI_LOG_LEVEL_DEBUG
default 5 if DALI_LOG_LEVEL_VERBOSE
choice DALI_TX_ACTIVE_LEVEL
prompt "DALI TX pin active level"
default DALI_TX_ACTIVE_LOW
help
Select the physical GPIO level that drives the DALI bus active. The
native gateway default is TX active low.
config DALI_TX_ACTIVE_LOW
bool "Active low"
config DALI_TX_ACTIVE_HIGH
bool "Active high"
endchoice
choice DALI_RX_ACTIVE_LEVEL
prompt "DALI RX pin active level"
default DALI_RX_ACTIVE_HIGH
help
Select the physical GPIO level read when the DALI bus is active. The
native gateway default is RX active high.
config DALI_RX_ACTIVE_LOW
bool "Active low"
config DALI_RX_ACTIVE_HIGH
bool "Active high"
endchoice
config DALI_API_QUEUE_LEN
int "Global API queue length"
range 1 64
@@ -48,7 +216,7 @@ config DALI_ENABLE_DEBUG_TASK
config DALI_DALI_TASK_STACK_SIZE
int "DALI task stack size"
range 1024 8192
default 2048
default 4096
config DALI_DALI_TASK_PRIORITY
int "DALI task priority"
+24
View File
@@ -41,10 +41,34 @@ void app_main(void) {
Use `menuconfig` under `DALI Component` to configure:
- Bus count and default baudrate.
- Native timing values for development, including timer resolution, half-bit period,
TX/RX stop conditions, query response timeout, double-send delay, and TX queue
arbitration waits.
- TX/RX active polarity. The native gateway default is TX active low and RX active high.
- Native bus power-down polling and legacy `FF FD` bus-abnormal raw-frame reporting intervals.
- Native DALI HAL log level for the `dali_hal` ESP-IDF log tag.
- Queue sizes.
- Task stack sizes and priorities.
- Optional debug task.
The native bus monitor uses `CONFIG_DALI_BUS_POWER_CHECK_INTERVAL_MS` to resample RX while
power-down and `CONFIG_DALI_BUS_ABNORMAL_REPORT_INTERVAL_MS` to publish legacy `FF FD`
raw frames while down. The report interval defaults to 3000 ms; set it to 0 to disable the
compatibility report.
Native timing defaults target standard 1200 bps DALI: a 416.67 us half-bit period is
generated by the default 3.636363 MHz timer. `CONFIG_DALI_CUSTOM_HALF_BIT_TIME_X100_US`
can override the half-bit period for development; keep it at 0 for baudrate-derived timing.
Query no-response timeout defaults to 30 ms, which covers the 5.5-10.5 ms backward-frame
start window plus the approximately 9.95 ms backward frame duration.
Native TX queue arbitration uses frame length as the frame type signal. Two- to four-byte
forward frames wait up to `CONFIG_DALI_FORWARD_MAX_WAIT_MS` for a valid send window,
using `CONFIG_DALI_FORWARD_ACTIVITY_WAIT_MS` after normal bus activity and
`CONFIG_DALI_FORWARD_AFTER_BACKWARD_WAIT_MS` after a valid backward frame. One-byte
backward frames wait up to `CONFIG_DALI_BACKWARD_IDLE_TIMEOUT_MS` for idle, then are
sent without echo verification because addressing-phase collisions are valid.
## API Note
The global TX response queue symbol was renamed:
+28 -7
View File
@@ -6,6 +6,14 @@
#include <memory.h> // for memset
#include "freertos/semphr.h"
#ifndef CONFIG_DALI_QUERY_RESPONSE_TIMEOUT_MS
#define CONFIG_DALI_QUERY_RESPONSE_TIMEOUT_MS 30
#endif
#ifndef CONFIG_DALI_DOUBLE_SEND_DELAY_MS
#define CONFIG_DALI_DOUBLE_SEND_DELAY_MS 20
#endif
static SemaphoreHandle_t s_dali_core_lock;
static SemaphoreHandle_t dali_core_mutex(void)
@@ -31,6 +39,20 @@ static inline void dali_core_unlock(void)
}
}
static UBaseType_t drain_rx_queue(QueueHandle_t rx_q)
{
if (rx_q == NULL) {
return 0;
}
Dali_msg_t stale = {0};
UBaseType_t drained = 0;
while (xQueueReceive(rx_q, &stale, 0) == pdTRUE) {
drained++;
}
return drained;
}
Dali_msg_t dali_msg_new_generic(uint8_t bit_length, uint8_t address, uint8_t cmd1, uint8_t cmd2, uint8_t cmd3) {
Dali_msg_t dali_msg;
dali_msg.id = 0;
@@ -90,7 +112,7 @@ void dali_send_double(Dali_msg_t *dali_msg) {
dali_send_locked(dali_msg);
// TODO check status
dali_msg->id++; // increment message ID
dali_delay_ms(10); // delay 13ms 101.8.1.2: 13.5 - 75ms
dali_delay_ms(CONFIG_DALI_DOUBLE_SEND_DELAY_MS);
dali_send_locked(dali_msg);
// TODO check status
dali_core_unlock();
@@ -110,17 +132,16 @@ int dali_query(Dali_msg_t *tx_msg, Dali_msg_t *rx_msg) {
return -1;
}
// TODO check empty queue
if(xQueueReceive(rx_q, rx_msg, 0) == pdTRUE) {
printf("Queue not empty\n");
return -1;
dali_core_lock();
UBaseType_t drained = drain_rx_queue(rx_q);
if (drained > 0) {
printf("dali_query: dropped %u stale RX frame(s)\n", (unsigned)drained);
}
// printf("check A tx=%d tm=%d[ms] st=%d len=%d d0=0x%X\n", ret, tx_msg->type, tx_msg->status, tx_msg->length, tx_msg->data[0]);
dali_core_lock();
dali_send_locked(tx_msg);
// receive message from DALI task
ret = xQueueReceive(rx_q, rx_msg, pdMS_TO_TICKS(50));
ret = xQueueReceive(rx_q, rx_msg, pdMS_TO_TICKS(CONFIG_DALI_QUERY_RESPONSE_TIMEOUT_MS));
dali_core_unlock();
// printf("B rx=%d tm=%d[ms] st=%d len=%d d0=0x%X\n", ret, rx_msg->type, rx_msg->status, rx_msg->length, rx_msg->data[0]);
return ret;
+621 -52
View File
@@ -4,6 +4,15 @@
//ESP-IDF HAL
#ifdef CONFIG_IDF_TARGET
#include "sdkconfig.h"
#ifndef CONFIG_DALI_LOG_LEVEL
#define CONFIG_DALI_LOG_LEVEL 2
#endif
#ifndef LOG_LOCAL_LEVEL
#define LOG_LOCAL_LEVEL CONFIG_DALI_LOG_LEVEL
#endif
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
@@ -39,7 +48,7 @@
#endif
#ifndef CONFIG_DALI_DALI_TASK_STACK_SIZE
#define CONFIG_DALI_DALI_TASK_STACK_SIZE 2048
#define CONFIG_DALI_DALI_TASK_STACK_SIZE 4096
#endif
#ifndef CONFIG_DALI_DALI_TASK_PRIORITY
@@ -54,14 +63,57 @@
#define CONFIG_DALI_DEBUG_TASK_PRIORITY 1
#endif
#ifndef CONFIG_DALI_TIMER_RESOLUTION_HZ
#define CONFIG_DALI_TIMER_RESOLUTION_HZ 3636363
#endif
#ifndef CONFIG_DALI_CUSTOM_HALF_BIT_TIME_X100_US
#define CONFIG_DALI_CUSTOM_HALF_BIT_TIME_X100_US 0
#endif
#ifndef CONFIG_DALI_TX_STOP_CONDITION_US
#define CONFIG_DALI_TX_STOP_CONDITION_US 0
#endif
#ifndef CONFIG_DALI_RX_STOP_CONDITION_US
#define CONFIG_DALI_RX_STOP_CONDITION_US 0
#endif
#ifndef CONFIG_DALI_BUS_POWER_CHECK_INTERVAL_MS
#define CONFIG_DALI_BUS_POWER_CHECK_INTERVAL_MS 500
#endif
#ifndef CONFIG_DALI_BUS_ABNORMAL_REPORT_INTERVAL_MS
#define CONFIG_DALI_BUS_ABNORMAL_REPORT_INTERVAL_MS 3000
#endif
#ifndef CONFIG_DALI_FORWARD_ACTIVITY_WAIT_MS
#define CONFIG_DALI_FORWARD_ACTIVITY_WAIT_MS 20
#endif
#ifndef CONFIG_DALI_FORWARD_AFTER_BACKWARD_WAIT_MS
#define CONFIG_DALI_FORWARD_AFTER_BACKWARD_WAIT_MS 10
#endif
#ifndef CONFIG_DALI_FORWARD_MAX_WAIT_MS
#define CONFIG_DALI_FORWARD_MAX_WAIT_MS 50
#endif
#ifndef CONFIG_DALI_BACKWARD_IDLE_TIMEOUT_MS
#define CONFIG_DALI_BACKWARD_IDLE_TIMEOUT_MS 10
#endif
#define WITHIN_RANGE(x, min, max) ((x) > (min) && (x) < (max))
#define MAX_DELTA_RELOAD_TIME 600000000 // 600s - max u32: 4,294,967,295~4,294s
#define DALI_BAUDRATE_MIN 400U
#define DALI_BAUDRATE_MAX 2400U
#define DALI_BUS_POWER_CHECK_INTERVAL_US ((uint64_t)CONFIG_DALI_BUS_POWER_CHECK_INTERVAL_MS * 1000ULL)
#define DALI_BUS_ABNORMAL_REPORT_INTERVAL_US ((uint64_t)CONFIG_DALI_BUS_ABNORMAL_REPORT_INTERVAL_MS * 1000ULL)
typedef struct {
uint32_t hb;
uint32_t timer_alarm_ticks;
uint32_t rx_hb_min;
uint32_t rx_hb_max;
uint32_t rx_2hb_min;
@@ -76,6 +128,11 @@ typedef struct {
static const char *TAG = "dali_hal";
static inline void apply_dali_log_level(void)
{
esp_log_level_set(TAG, (esp_log_level_t)CONFIG_DALI_LOG_LEVEL);
}
typedef struct {
uint8_t bus_id;
uint8_t tx_pin;
@@ -91,8 +148,17 @@ typedef struct {
uint32_t rx_tx_delta;
uint32_t rx_pulse_width;
uint8_t rx_level;
uint8_t tx_level;
uint64_t bus_level_check_time;
uint64_t bus_abnormal_report_time;
Dali_msg_t tx_data;
bool bus_activity_seen;
uint64_t rx_last_frame_time;
uint8_t rx_last_frame_length;
uint8_t rx_last_frame_status;
volatile bool force_backward_tx;
Dali_msg_t rx_data;
uint8_t tx_half_bit_counter;
@@ -137,17 +203,31 @@ static inline void dali_hal_unlock(void)
}
}
static uint32_t dali_half_bit_from_baud(uint32_t baudrate)
static uint32_t dali_half_bit_x100_from_baud(uint32_t baudrate)
{
if (baudrate < DALI_BAUDRATE_MIN || baudrate > DALI_BAUDRATE_MAX) {
return 0;
}
uint64_t hb = 500000ULL + (baudrate / 2U); // round to nearest
hb /= baudrate;
if (hb == 0 || hb > 2000000ULL) { // should never happen with checked bounds
uint64_t hb_x100 = 50000000ULL + (baudrate / 2U); // round to nearest 0.01 us
hb_x100 /= baudrate;
if (hb_x100 == 0 || hb_x100 > 200000000ULL) { // should never happen with checked bounds
return 0;
}
return (uint32_t)hb;
return (uint32_t)hb_x100;
}
static uint32_t half_bit_us_from_x100(uint32_t hb_x100)
{
return (hb_x100 + 50U) / 100U;
}
static uint32_t timer_alarm_ticks_from_x100(uint32_t hb_x100)
{
uint64_t ticks = ((uint64_t)CONFIG_DALI_TIMER_RESOLUTION_HZ * hb_x100 + 50000000ULL) / 100000000ULL;
if (ticks == 0 || ticks > UINT32_MAX) {
return 0;
}
return (uint32_t)ticks;
}
static inline uint32_t scale_time_by_hb(uint32_t base_us, uint32_t hb_us)
@@ -155,25 +235,40 @@ static inline uint32_t scale_time_by_hb(uint32_t base_us, uint32_t hb_us)
return (uint32_t)(((uint64_t)base_us * hb_us + (DALI_TIME_HB / 2U)) / DALI_TIME_HB);
}
static inline uint32_t configured_or_scaled_time(uint32_t configured_us, uint32_t base_us,
uint32_t hb_us)
{
return configured_us > 0 ? configured_us : scale_time_by_hb(base_us, hb_us);
}
static esp_err_t update_timing_locked(uint32_t baudrate)
{
uint32_t hb = dali_half_bit_from_baud(baudrate);
if (hb == 0) {
ESP_LOGE(TAG, "invalid baudrate: %u", baudrate);
uint32_t hb_x100 = CONFIG_DALI_CUSTOM_HALF_BIT_TIME_X100_US;
if (hb_x100 == 0) {
hb_x100 = dali_half_bit_x100_from_baud(baudrate);
}
uint32_t hb = half_bit_us_from_x100(hb_x100);
uint32_t timer_alarm_ticks = timer_alarm_ticks_from_x100(hb_x100);
if (hb == 0 || timer_alarm_ticks == 0) {
ESP_LOGE(TAG, "invalid native timing: baudrate=%u hbX100Us=%u timerHz=%u",
baudrate, hb_x100, CONFIG_DALI_TIMER_RESOLUTION_HZ);
return ESP_ERR_INVALID_ARG;
}
dali_timing_t new_timing = {
.hb = hb,
.timer_alarm_ticks = timer_alarm_ticks,
.rx_hb_min = scale_time_by_hb(DALI_RX_HB_MIN, hb),
.rx_hb_max = scale_time_by_hb(DALI_RX_HB_MAX, hb),
.rx_2hb_min = scale_time_by_hb(DALI_RX_2HB_MIN, hb),
.rx_2hb_max = scale_time_by_hb(DALI_RX_2HB_MAX, hb),
.rx_stop_cond = scale_time_by_hb(DALI_RX_STOP_COND, hb),
.rx_stop_cond = configured_or_scaled_time(CONFIG_DALI_RX_STOP_CONDITION_US,
DALI_RX_STOP_COND, hb),
.time_bus_down = scale_time_by_hb(DALI_TIME_BUS_DOWN, hb),
.time_break_min = scale_time_by_hb(DALI_TIME_BREAK_MIN, hb),
.time_recovery_min = scale_time_by_hb(DALI_TIME_RECOVERY_MIN, hb),
.tx_stop_cond = scale_time_by_hb(DALI_TX_STOP_COND, hb),
.tx_stop_cond = configured_or_scaled_time(CONFIG_DALI_TX_STOP_CONDITION_US,
DALI_TX_STOP_COND, hb),
.collision_txrx_delta = scale_time_by_hb(DALI_COLLISION_TXRX_DELTA, hb),
};
@@ -199,7 +294,7 @@ static esp_err_t apply_timer_alarm_locked(void)
return ESP_OK;
}
gptimer_alarm_config_t timer_alarm_config = {
.alarm_count = s_timing.hb,
.alarm_count = s_timing.timer_alarm_ticks,
.reload_count = 0,
.flags = {
.auto_reload_on_alarm = true,
@@ -235,22 +330,330 @@ static bool s_debug_task_created = false;
#endif
static bool s_dali_task_created = false;
#define DALI_SET_BUS_HIGH(bus) gpio_set_level((bus)->tx_pin, DALI_TX_HIGH) // set bus level
#define DALI_SET_BUS_LOW(bus) gpio_set_level((bus)->tx_pin, DALI_TX_LOW) // set bus level
#define DALI_SET_BUS_LEVEL(bus, x) gpio_set_level((bus)->tx_pin, ((x)==DALI_TX_HIGH)) // set bus level
#define DALI_SET_BUS_HIGH(bus) do { gpio_set_level((bus)->tx_pin, DALI_TX_HIGH); (bus)->tx_level = 1; } while (0)
#define DALI_SET_BUS_LOW(bus) do { gpio_set_level((bus)->tx_pin, DALI_TX_LOW); (bus)->tx_level = 0; } while (0)
#define DALI_SET_BUS_LEVEL(bus, x) do { \
uint8_t level = (x) ? 1 : 0; \
gpio_set_level((bus)->tx_pin, level ? DALI_TX_HIGH : DALI_TX_LOW); \
(bus)->tx_level = level; \
} while (0)
// !!! read from RX pin, we need real bus level, not logic level of TX pin
// return: 0 - bus level low, active state
// 1 - bus level high, idle state
#define DALI_GET_BUS_LEVEL(bus) (gpio_get_level((bus)->rx_pin) == (DALI_RX_HIGH)) // get bus level
// return: 0 - tx pin drive bus low, active state
// 1 - tx pin drive bus high, idle state
#define DALI_GET_TX_LEVEL(bus) (gpio_get_level((bus)->tx_pin) == (DALI_TX_HIGH)) // get TX pin level
// return: 0 - TX drives bus low, active state
// 1 - TX releases/drives bus high, idle state
#define DALI_GET_TX_LEVEL(bus) ((bus)->tx_level)
static inline bool bus_valid(uint8_t bus_id) {
return bus_id < DALI_PHY_COUNT && s_bus[bus_id].inited;
}
static const char *bus_state_name(dali_bus_state_t state)
{
switch (state) {
case DALI_BUS_UNKNOWN: return "unknown";
case DALI_BUS_POWER_DOWN: return "power_down";
case DALI_BUS_ERROR: return "error";
case DALI_BUS_READY: return "ready";
case DALI_BUS_TRANSMITTING: return "transmitting";
case DALI_BUS_RECEIVING: return "receiving";
case DALI_BUS_TIME_BREAK: return "time_break";
case DALI_BUS_RECOVERY: return "recovery";
default: return "invalid";
}
}
static const char *tx_state_name(dali_tx_state_t state)
{
switch (state) {
case TX_STATE_ERROR: return "error";
case TX_STATE_COLLISION: return "collision";
case TX_STATE_IDLE: return "idle";
case TX_STATE_START: return "start";
case TX_STATE_DATA: return "data";
case TX_STATE_STOP: return "stop";
default: return "invalid";
}
}
static const char *rx_state_name(dali_rx_state_t state)
{
switch (state) {
case RX_STATE_ERROR: return "error";
case RX_STATE_IDLE: return "idle";
case RX_STATE_START: return "start";
case RX_STATE_DATA: return "data";
case RX_STATE_STOP: return "stop";
case RX_STATE_END: return "end";
default: return "invalid";
}
}
static uint32_t tx_completion_timeout_ms(const Dali_msg_t *msg)
{
uint32_t hb = s_timing_ready ? s_timing.hb : DALI_TIME_HB;
uint32_t stop_us = s_timing_ready ? s_timing.tx_stop_cond : DALI_TX_STOP_COND;
uint32_t bits = msg ? msg->length : DALI_MAX_BITS;
if (bits == 0 || bits > DALI_MAX_BITS) {
bits = DALI_MAX_BITS;
}
uint64_t frame_us = ((uint64_t)(2U + bits * 2U) * hb) + stop_us;
uint32_t frame_ms = (uint32_t)((frame_us + 999U) / 1000U);
uint32_t timeout_ms = frame_ms + 20U;
if (timeout_ms < 20U) {
timeout_ms = 20U;
}
if (timeout_ms > 500U) {
timeout_ms = 500U;
}
return timeout_ms;
}
static inline bool IRAM_ATTR is_backward_tx_msg(const Dali_msg_t *msg)
{
return msg != NULL && msg->length == 8;
}
static bool is_forward_tx_msg(const Dali_msg_t *msg)
{
return msg != NULL && msg->length >= 16 && msg->length <= 32 && (msg->length % 8) == 0;
}
static UBaseType_t queue_waiting(QueueHandle_t queue);
static uint64_t bus_last_activity_time_us(const dali_bus_ctx_t *bus)
{
if (bus == NULL || !bus->bus_activity_seen) {
return 0;
}
return bus->rx_last_edge_time > bus->tx_last_edge_time ? bus->rx_last_edge_time
: bus->tx_last_edge_time;
}
static bool bus_idle_for_tx(const dali_bus_ctx_t *bus)
{
return bus != NULL && bus->bus_state == DALI_BUS_READY && bus->tx_state == TX_STATE_IDLE &&
bus->rx_state == RX_STATE_IDLE && bus->rx_level == 1 && queue_waiting(bus->tx_queue) == 0;
}
static bool last_activity_was_backward_frame(const dali_bus_ctx_t *bus, uint64_t last_activity)
{
return bus != NULL && bus->rx_last_frame_status == DALI_FRAME_OK &&
bus->rx_last_frame_length == 8 && bus->rx_last_frame_time != 0 &&
bus->rx_last_frame_time >= last_activity;
}
static TickType_t tx_wait_poll_ticks(void)
{
TickType_t ticks = pdMS_TO_TICKS(1);
return ticks == 0 ? 1 : ticks;
}
static bool wait_for_forward_tx_window(dali_bus_ctx_t *bus, const Dali_msg_t *msg)
{
if (bus == NULL || !is_forward_tx_msg(msg)) {
return true;
}
const uint64_t started = esp_timer_get_time();
const uint64_t max_wait_us = (uint64_t)CONFIG_DALI_FORWARD_MAX_WAIT_MS * 1000ULL;
const TickType_t poll_ticks = tx_wait_poll_ticks();
while (true) {
if (bus->bus_state == DALI_BUS_POWER_DOWN) {
return false;
}
const uint64_t now = esp_timer_get_time();
if (bus_idle_for_tx(bus)) {
const uint64_t last_activity = bus_last_activity_time_us(bus);
if (last_activity == 0) {
return true;
}
const uint32_t wait_ms = last_activity_was_backward_frame(bus, last_activity)
? CONFIG_DALI_FORWARD_AFTER_BACKWARD_WAIT_MS
: CONFIG_DALI_FORWARD_ACTIVITY_WAIT_MS;
if (wait_ms == 0 || (now - last_activity) >= ((uint64_t)wait_ms * 1000ULL)) {
return true;
}
}
if (max_wait_us == 0 || (now - started) >= max_wait_us) {
return false;
}
vTaskDelay(poll_ticks);
}
}
static bool wait_for_backward_tx_window(dali_bus_ctx_t *bus, const Dali_msg_t *msg,
bool *force_start)
{
if (force_start != NULL) {
*force_start = false;
}
if (bus == NULL || !is_backward_tx_msg(msg)) {
return true;
}
const uint64_t started = esp_timer_get_time();
const uint64_t timeout_us = (uint64_t)CONFIG_DALI_BACKWARD_IDLE_TIMEOUT_MS * 1000ULL;
const TickType_t poll_ticks = tx_wait_poll_ticks();
while (true) {
if (bus->bus_state == DALI_BUS_POWER_DOWN) {
return false;
}
if (bus_idle_for_tx(bus)) {
return true;
}
const uint64_t now = esp_timer_get_time();
if (timeout_us == 0 || (now - started) >= timeout_us) {
if (force_start != NULL) {
*force_start = true;
}
return true;
}
vTaskDelay(poll_ticks);
}
}
static UBaseType_t queue_waiting(QueueHandle_t queue)
{
return queue ? uxQueueMessagesWaiting(queue) : 0;
}
static void log_tx_message(const char *prefix, const dali_bus_ctx_t *bus,
const Dali_msg_t *msg, uint32_t timeout_ms)
{
if (bus == NULL || msg == NULL) {
return;
}
ESP_LOGW(TAG,
"%s bus=%u timeoutMs=%lu busState=%s txState=%s rxState=%s "
"txQ=%u replyQ=%u status=%u len=%u data=%02x %02x %02x %02x",
prefix, bus->bus_id, (unsigned long)timeout_ms,
bus_state_name(bus->bus_state), tx_state_name(bus->tx_state),
rx_state_name(bus->rx_state), (unsigned)queue_waiting(bus->tx_queue),
(unsigned)queue_waiting(bus->tx_reply_queue), msg->status, msg->length,
msg->data[0], msg->data[1], msg->data[2], msg->data[3]);
}
static void drain_tx_reply_queue(dali_bus_ctx_t *bus)
{
if (bus == NULL || bus->tx_reply_queue == NULL) {
return;
}
Dali_msg_t stale = {0};
UBaseType_t drained = 0;
while (xQueueReceive(bus->tx_reply_queue, &stale, 0) == pdTRUE) {
drained++;
}
if (drained > 0) {
ESP_LOGW(TAG,
"bus=%u dropped %u stale TX completion(s) status=%u len=%u data=%02x %02x %02x %02x",
bus->bus_id, (unsigned)drained, stale.status, stale.length, stale.data[0],
stale.data[1], stale.data[2], stale.data[3]);
}
}
static void recover_tx_timeout(dali_bus_ctx_t *bus)
{
if (bus == NULL) {
return;
}
if (bus->tx_queue) {
xQueueReset(bus->tx_queue);
}
if (bus->tx_reply_queue) {
xQueueReset(bus->tx_reply_queue);
}
bus->tx_data.status = DALI_FRAME_ERROR;
bus->tx_state = TX_STATE_IDLE;
bus->rx_state = RX_STATE_IDLE;
bus->tx_half_bit_counter = 0;
bus->tx_data_bit_counter = 0;
bus->rx_half_bit_counter = 0;
bus->rx_data_bit_counter = 0;
if (bus->bus_state == DALI_BUS_TRANSMITTING || bus->bus_state == DALI_BUS_TIME_BREAK ||
bus->bus_state == DALI_BUS_RECOVERY) {
bus->tx_last_edge_time = esp_timer_get_time();
DALI_SET_BUS_HIGH(bus);
bus->bus_state = DALI_BUS_READY;
}
ESP_LOGW(TAG, "bus=%u native TX queue recovered busState=%s txState=%s rxState=%s",
bus->bus_id, bus_state_name(bus->bus_state), tx_state_name(bus->tx_state),
rx_state_name(bus->rx_state));
}
static inline bool IRAM_ATTR interval_elapsed_us(uint64_t time_now, uint64_t last_time,
uint64_t interval_us)
{
return interval_us > 0 && (time_now - last_time) >= interval_us;
}
static void IRAM_ATTR publish_bus_abnormal_from_isr(dali_bus_ctx_t *bus, BaseType_t *yield)
{
if (bus == NULL || dali_raw_receive_queue == NULL || DALI_BUS_ABNORMAL_REPORT_INTERVAL_US == 0) {
return;
}
Dali_msg_t abnormal = {0};
abnormal.id = bus->bus_id;
abnormal.type = DALI_MSG_FORWARD;
abnormal.status = DALI_FRAME_OK;
abnormal.length = 16;
abnormal.data[0] = 0xFF;
abnormal.data[1] = 0xFD;
if (xQueueSendToBackFromISR(dali_raw_receive_queue, &abnormal, yield) != pdTRUE) {
Dali_msg_t dropped = {0};
xQueueReceiveFromISR(dali_raw_receive_queue, &dropped, yield);
xQueueSendToBackFromISR(dali_raw_receive_queue, &abnormal, yield);
}
}
static void IRAM_ATTR maybe_report_bus_abnormal_from_isr(dali_bus_ctx_t *bus, uint64_t time_now,
BaseType_t *yield)
{
if (bus == NULL || DALI_BUS_ABNORMAL_REPORT_INTERVAL_US == 0) {
return;
}
if (!interval_elapsed_us(time_now, bus->bus_abnormal_report_time,
DALI_BUS_ABNORMAL_REPORT_INTERVAL_US)) {
return;
}
publish_bus_abnormal_from_isr(bus, yield);
bus->bus_abnormal_report_time = time_now;
}
static void IRAM_ATTR poll_power_down_bus_from_isr(dali_bus_ctx_t *bus, uint64_t time_now,
BaseType_t *yield)
{
if (bus == NULL || bus->bus_state != DALI_BUS_POWER_DOWN) {
return;
}
if (interval_elapsed_us(time_now, bus->bus_level_check_time,
DALI_BUS_POWER_CHECK_INTERVAL_US)) {
const uint8_t current_level = DALI_GET_BUS_LEVEL(bus);
bus->bus_level_check_time = time_now;
if (current_level != bus->rx_level) {
bus->rx_level = current_level;
bus->rx_last_edge_time = time_now;
}
}
maybe_report_bus_abnormal_from_isr(bus, time_now, yield);
}
static inline void publish_rx_frame_from_isr(Dali_msg_t *msg, QueueHandle_t queue, BaseType_t *yield)
{
if (msg == NULL) {
@@ -264,6 +667,48 @@ static inline void publish_rx_frame_from_isr(Dali_msg_t *msg, QueueHandle_t queu
}
}
static inline void IRAM_ATTR note_rx_frame_from_isr(dali_bus_ctx_t *bus)
{
if (bus == NULL) {
return;
}
bus->rx_last_frame_time = bus->rx_last_edge_time;
bus->rx_last_frame_length = bus->rx_data.length;
bus->rx_last_frame_status = bus->rx_data.status;
}
static void IRAM_ATTR complete_tx_from_isr(dali_bus_ctx_t *bus, BaseType_t *yield)
{
if (bus == NULL) {
return;
}
bus->tx_data.status = DALI_FRAME_OK;
if (!is_backward_tx_msg(&bus->tx_data) && bus->tx_reply_queue) {
if (xQueueSendToBackFromISR(bus->tx_reply_queue, &bus->tx_data, yield) != pdTRUE) {
Dali_msg_t dropped = {0};
xQueueReceiveFromISR(bus->tx_reply_queue, &dropped, yield);
xQueueSendToBackFromISR(bus->tx_reply_queue, &bus->tx_data, yield);
}
}
bus->tx_state = TX_STATE_IDLE;
}
static void IRAM_ATTR start_tx_collision_recovery_from_isr(dali_bus_ctx_t *bus,
uint64_t time_now)
{
if (bus == NULL) {
return;
}
bus->rx_last_edge_time = time_now;
bus->tx_last_edge_time = time_now;
DALI_SET_BUS_LOW(bus);
bus->bus_state = DALI_BUS_TIME_BREAK;
bus->tx_data.status = DALI_FRAME_COLLISION;
}
// GPIO ISR handler
// define rx_gpio_isr_handler on any edge
static void IRAM_ATTR rx_gpio_isr_handler(void* arg)
@@ -273,6 +718,7 @@ static void IRAM_ATTR rx_gpio_isr_handler(void* arg)
return;
}
BaseType_t yield = false;
uint64_t rx_current_edge_time = esp_timer_get_time(); // get time in us
uint8_t rx_previous_level = bus->rx_level;
@@ -280,6 +726,7 @@ static void IRAM_ATTR rx_gpio_isr_handler(void* arg)
// rx_level = 1 if and only if DALI bus is really high, idle
bus->rx_level = DALI_GET_BUS_LEVEL(bus); // get level of RX pin - not depend on hw: 0 - low, 1 - high
bus->bus_activity_seen = true;
bus->rx_pulse_width = rx_current_edge_time - bus->rx_last_edge_time; // time from last edge
bus->rx_tx_delta = rx_current_edge_time - bus->tx_last_edge_time; // time from last edge
@@ -287,8 +734,16 @@ static void IRAM_ATTR rx_gpio_isr_handler(void* arg)
// always save time of last edge
bus->rx_last_edge_time = rx_current_edge_time; // get time in us
if(bus->bus_state == DALI_BUS_READY && bus->rx_level == 0) // found start bit
bool tx_stop_released = bus->bus_state == DALI_BUS_TRANSMITTING &&
bus->tx_state == TX_STATE_STOP &&
DALI_GET_TX_LEVEL(bus) == 1;
if((bus->bus_state == DALI_BUS_READY || tx_stop_released) && bus->rx_level == 0) // found start bit
{
if (tx_stop_released) {
complete_tx_from_isr(bus, &yield);
}
// within range for backward frame
uint32_t time_ms = bus->rx_pulse_width / 1000; // 1ms = 1000us
if(time_ms>255) bus->rx_data.type = 255;
@@ -378,12 +833,14 @@ static void IRAM_ATTR rx_gpio_isr_handler(void* arg)
// if collision detected: we are too late after bit was transmitted
else if (bus->bus_state == DALI_BUS_TRANSMITTING && bus->rx_tx_delta > s_timing.collision_txrx_delta)
else if (bus->bus_state == DALI_BUS_TRANSMITTING && bus->tx_state != TX_STATE_STOP &&
!is_backward_tx_msg(&bus->tx_data) &&
bus->rx_tx_delta > s_timing.collision_txrx_delta)
{
// we need now to start collision recovery with time break: 101.9.2.4
bus->tx_last_edge_time = esp_timer_get_time(); // get time in us
DALI_SET_BUS_LOW(bus); // force TX low - active state, inform about collision, this also generate new GPIO ISR
bus->tx_last_edge_time = esp_timer_get_time(); // get time in us
bus->bus_state = DALI_BUS_TIME_BREAK; // we are in time break state
bus->tx_data.status = DALI_FRAME_COLLISION; // collision detected
}
@@ -399,7 +856,11 @@ static void IRAM_ATTR rx_gpio_isr_handler(void* arg)
dbg.rx_pulse_width = bus->rx_pulse_width;
dbg.rx_tx_delta = bus->rx_tx_delta;
dbg.bus_id = bus->bus_id;
xQueueSendToBackFromISR(rx_dbg_queue, &dbg, NULL); // send data to queue
xQueueSendToBackFromISR(rx_dbg_queue, &dbg, &yield); // send data to queue
}
if (yield) {
portYIELD_FROM_ISR();
}
}
@@ -423,6 +884,18 @@ static bool IRAM_ATTR handle_bus_timer(dali_bus_ctx_t *bus, uint64_t time_now)
bus->tx_last_edge_time = time_now - MAX_DELTA_RELOAD_TIME/2; // half of max time
}
poll_power_down_bus_from_isr(bus, time_now, &yield);
rx_delta = time_now - bus->rx_last_edge_time;
tx_delta = time_now - bus->tx_last_edge_time;
if (bus->bus_state == DALI_BUS_TRANSMITTING && bus->tx_state == TX_STATE_STOP &&
DALI_GET_TX_LEVEL(bus) == 1 &&
(bus->rx_state == RX_STATE_START || bus->rx_state == RX_STATE_DATA ||
bus->rx_state == RX_STATE_STOP)) {
complete_tx_from_isr(bus, &yield);
bus->bus_state = DALI_BUS_RECEIVING;
}
// recovery from different error states: UNKNOWN, ERROR, POWER_DOWN
if(bus->bus_state <= DALI_BUS_ERROR)
{ // 101.8.2.4 - startup BUS after 2.4ms
@@ -434,8 +907,13 @@ static bool IRAM_ATTR handle_bus_timer(dali_bus_ctx_t *bus, uint64_t time_now)
// if bus power down - if bus is low for more then 45ms
if(bus->rx_level==0 && rx_delta > s_timing.time_bus_down)
{ // power lost
bool entered_power_down = bus->bus_state != DALI_BUS_POWER_DOWN;
bus->bus_state = DALI_BUS_POWER_DOWN; // bus is power down - recovery see previous if
DALI_SET_BUS_HIGH(bus); // make sure TX is high
if (entered_power_down) {
bus->bus_level_check_time = time_now;
bus->bus_abnormal_report_time = time_now;
}
}
// recovery from collision detection
@@ -444,9 +922,9 @@ static bool IRAM_ATTR handle_bus_timer(dali_bus_ctx_t *bus, uint64_t time_now)
// BUS: ACTIVE, LOW - BUS is busy, let caller to restart transmission
if(bus->bus_state == DALI_BUS_TIME_BREAK && rx_delta > s_timing.time_break_min)
{
bus->tx_last_edge_time = esp_timer_get_time(); // get time in us
DALI_SET_BUS_HIGH(bus); // TX high - idle state - generate ISR on RX pin
bus->tx_last_edge_time = esp_timer_get_time(); // get time in us
// read bus state
if(DALI_GET_BUS_LEVEL(bus) == 0) // other device is keeping bus low
{
@@ -467,33 +945,58 @@ static bool IRAM_ATTR handle_bus_timer(dali_bus_ctx_t *bus, uint64_t time_now)
}
// start transmitting
else if(bus->bus_state == DALI_BUS_READY && bus->tx_state == TX_STATE_IDLE && bus->tx_queue && xQueueReceiveFromISR(bus->tx_queue, &bus->tx_data, NULL) == pdTRUE)
else if(bus->tx_state == TX_STATE_IDLE && bus->tx_queue)
{
bus->tx_data.status = DALI_FRAME_ERROR; // error status - will be set ok on success
bus->bus_state = DALI_BUS_TRANSMITTING; // bus is transmitting
bus->tx_state = TX_STATE_START; // start transmitting
bus->tx_half_bit_counter = 0;
bus->tx_data_bit_counter = 0; // actually sent bits count
DALI_SET_BUS_LOW(bus); // start bit first half
bus->tx_last_edge_time = esp_timer_get_time(); // get time in us
Dali_msg_t pending = {0};
bool can_start = bus->bus_state == DALI_BUS_READY;
bool force_backward = false;
if (!can_start && bus->force_backward_tx &&
xQueuePeekFromISR(bus->tx_queue, &pending) == pdTRUE) {
if (is_backward_tx_msg(&pending) && bus->bus_state != DALI_BUS_POWER_DOWN) {
can_start = true;
force_backward = true;
} else {
bus->force_backward_tx = false;
}
}
if (can_start && xQueueReceiveFromISR(bus->tx_queue, &bus->tx_data, &yield) == pdTRUE) {
bus->force_backward_tx = false;
if (force_backward) {
bus->rx_state = RX_STATE_IDLE;
}
bus->tx_data.status = DALI_FRAME_ERROR; // error status - will be set ok on success
bus->bus_activity_seen = true;
bus->bus_state = DALI_BUS_TRANSMITTING; // bus is transmitting
bus->tx_state = TX_STATE_START; // start transmitting
bus->tx_half_bit_counter = 0;
bus->tx_data_bit_counter = 0; // actually sent bits count
bus->tx_last_edge_time = esp_timer_get_time(); // get time in us
DALI_SET_BUS_LOW(bus); // start bit first half
}
}
else if(bus->bus_state == DALI_BUS_TRANSMITTING)
{
uint8_t bus_level = DALI_GET_BUS_LEVEL(bus);
if(bus->tx_state != TX_STATE_STOP && !is_backward_tx_msg(&bus->tx_data) &&
tx_delta > s_timing.collision_txrx_delta &&
bus_level != DALI_GET_TX_LEVEL(bus)) {
bus->rx_level = bus_level;
bus->rx_tx_delta = tx_delta;
start_tx_collision_recovery_from_isr(bus, time_now);
}
// transmit data
if(bus->tx_state == TX_STATE_START) {
else if(bus->tx_state == TX_STATE_START) {
bus->tx_state = TX_STATE_DATA; // start transmitting data
bus->tx_half_bit_counter++;
DALI_SET_BUS_HIGH(bus); // start bit second half
bus->tx_last_edge_time = esp_timer_get_time(); // get time in us
DALI_SET_BUS_HIGH(bus); // start bit second half
}
else if(bus->tx_state == TX_STATE_DATA) {
bool value = (bus->tx_data.data[bus->tx_data_bit_counter/8] >> ( 7 - (bus->tx_data_bit_counter % 8) )) & 0x01;
value ^= bus->tx_half_bit_counter & 0x01; // xor=invert value for odd half bit 1:0->1 and 0:1->0
bus->tx_last_edge_time = esp_timer_get_time(); // get time in us
DALI_SET_BUS_LEVEL(bus, value);
bus->tx_last_edge_time = esp_timer_get_time(); // get time in us
bus->tx_half_bit_counter++; // increment half bit counter before next test
if(bus->tx_half_bit_counter & 0x01) { // next bit
bus->tx_data_bit_counter++;
@@ -506,15 +1009,11 @@ static bool IRAM_ATTR handle_bus_timer(dali_bus_ctx_t *bus, uint64_t time_now)
// here we check TX (NOT RX) bit state
if(DALI_GET_TX_LEVEL(bus) == 0) // really ok - otherwise we will keep bus low forever
{
DALI_SET_BUS_HIGH(bus);
bus->tx_last_edge_time = esp_timer_get_time(); // get time in us
DALI_SET_BUS_HIGH(bus);
}
else if(tx_delta > s_timing.tx_stop_cond) {
bus->tx_data.status = DALI_FRAME_OK; // frame is OK
if (bus->tx_reply_queue) {
xQueueSendToBackFromISR(bus->tx_reply_queue, &bus->tx_data, &yield); // send data to queue
}
bus->tx_state = TX_STATE_IDLE; // final state with transmitted data
complete_tx_from_isr(bus, &yield);
bus->bus_state = DALI_BUS_READY; // bus is ready
}
}
@@ -522,7 +1021,7 @@ static bool IRAM_ATTR handle_bus_timer(dali_bus_ctx_t *bus, uint64_t time_now)
else if(bus->bus_state == DALI_BUS_READY && bus->tx_state > TX_STATE_IDLE)
{
// we are not transmitting but we have data - reply to queue and let error state in tx_data.status
if (bus->tx_reply_queue) {
if (bus->tx_reply_queue && !is_backward_tx_msg(&bus->tx_data)) {
xQueueSendToBackFromISR(bus->tx_reply_queue, &bus->tx_data, &yield); // send data to queue
}
bus->tx_state = TX_STATE_IDLE; // clear state
@@ -538,6 +1037,7 @@ static bool IRAM_ATTR handle_bus_timer(dali_bus_ctx_t *bus, uint64_t time_now)
// rx_data.status = DALI_FRAME_ERROR; // should be set inside ISR
bus->rx_data.length = bus->rx_data_bit_counter; // set length of data
// rx_data.data[0] = 0xAA; // debug
note_rx_frame_from_isr(bus);
publish_rx_frame_from_isr(&bus->rx_data, bus->rx_queue, &yield); // send data to queue
}
}
@@ -549,9 +1049,14 @@ static bool IRAM_ATTR handle_bus_timer(dali_bus_ctx_t *bus, uint64_t time_now)
bus->rx_data.status = DALI_FRAME_OK; // frame is OK
bus->rx_data.length = bus->rx_data_bit_counter; // set length of data
// rx_data.data[0] = 0xBB; // debug
note_rx_frame_from_isr(bus);
publish_rx_frame_from_isr(&bus->rx_data, bus->rx_queue, &yield); // send data to queue
}
}
else if(bus->rx_state == RX_STATE_START && rx_delta > s_timing.rx_hb_max) {
bus->rx_state = RX_STATE_ERROR;
bus->rx_data.status = DALI_FRAME_TIME_VIOLATION;
}
}
return yield;
}
@@ -588,7 +1093,7 @@ static void debug_task(void *pvParameters)
else v = '0' + dbg.level;
}
else v=' ';
printf("bus[%u] rx: [%2d] pw=%lu v=%u rtd=%lu [%c]",
ESP_LOGD(TAG, "bus[%u] rx: [%2d] pw=%lu v=%u rtd=%lu [%c]",
dbg.bus_id, i++, dbg.rx_pulse_width, dbg.level, dbg.rx_tx_delta, v);
if(dbg.rx_pulse_width < 1000) {
if(dbg.rx_pulse_width > 550) HB+=2;
@@ -659,7 +1164,7 @@ static esp_err_t ensure_timer_started(void)
.flags = {
.intr_shared = true,
},
.resolution_hz = 1 * 1000 * 1000, // 1MHz, 1 tick = 1us
.resolution_hz = CONFIG_DALI_TIMER_RESOLUTION_HZ,
};
err = gptimer_new_timer(&timer_config, &gptimer);
if (err != ESP_OK) {
@@ -726,6 +1231,9 @@ static esp_err_t init_bus(uint8_t bus_id, uint8_t tx_pin, uint8_t rx_pin)
bus->bus_id = bus_id;
bus->tx_pin = tx_pin;
bus->rx_pin = rx_pin;
bus->tx_level = 1;
gpio_set_level(bus->tx_pin, DALI_TX_HIGH);
gpio_config_t io_conf;
@@ -771,6 +1279,13 @@ static esp_err_t init_bus(uint8_t bus_id, uint8_t tx_pin, uint8_t rx_pin)
bus->rx_last_edge_time = esp_timer_get_time(); // get time in us - startup time
bus->rx_level = DALI_GET_BUS_LEVEL(bus); // get level of RX pin
bus->tx_last_edge_time = bus->rx_last_edge_time;
bus->bus_level_check_time = bus->rx_last_edge_time;
bus->bus_abnormal_report_time = 0;
bus->bus_activity_seen = false;
bus->rx_last_frame_time = 0;
bus->rx_last_frame_length = 0;
bus->rx_last_frame_status = DALI_FRAME_UNKNOWN;
bus->force_backward_tx = false;
err = ensure_isr_service();
if (err != ESP_OK) {
@@ -805,17 +1320,69 @@ static int dali_tx_bus(dali_bus_ctx_t *bus, Dali_msg_t *dali_msg)
if (bus == NULL || !bus->inited) {
return ESP_FAIL;
}
if(xQueueSendToBack(bus->tx_queue, dali_msg, pdMS_TO_TICKS(50)) == pdFALSE) {
xQueueReset(bus->tx_queue); // clear queue
printf("dali_tx: Queue full\n");
if (bus->tx_queue == NULL || bus->tx_reply_queue == NULL || dali_msg == NULL) {
return ESP_FAIL;
}
if(xQueueReceive(bus->tx_reply_queue, dali_msg, pdMS_TO_TICKS(50)) == pdFALSE) {
xQueueReset(bus->tx_reply_queue); // clear queue
printf("dali_tx: No reply\n");
const uint32_t timeout_ms = tx_completion_timeout_ms(dali_msg);
const TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms);
drain_tx_reply_queue(bus);
if (bus->bus_state == DALI_BUS_POWER_DOWN) {
dali_msg->status = DALI_FRAME_ERROR;
log_tx_message("native TX skipped, bus power down", bus, dali_msg, timeout_ms);
return ESP_FAIL;
}
return ESP_OK;
ESP_LOGD(TAG, "bus=%u queue TX len=%u data=%02x %02x %02x %02x timeoutMs=%lu",
bus->bus_id, dali_msg->length, dali_msg->data[0], dali_msg->data[1],
dali_msg->data[2], dali_msg->data[3], (unsigned long)timeout_ms);
if (is_forward_tx_msg(dali_msg) && !wait_for_forward_tx_window(bus, dali_msg)) {
dali_msg->status = DALI_FRAME_ERROR;
log_tx_message("native TX wait timeout", bus, dali_msg,
CONFIG_DALI_FORWARD_MAX_WAIT_MS);
return ESP_FAIL;
}
bool force_backward = false;
if (is_backward_tx_msg(dali_msg) &&
!wait_for_backward_tx_window(bus, dali_msg, &force_backward)) {
dali_msg->status = DALI_FRAME_ERROR;
log_tx_message("native backward TX wait timeout", bus, dali_msg,
CONFIG_DALI_BACKWARD_IDLE_TIMEOUT_MS);
return ESP_FAIL;
}
bus->force_backward_tx = force_backward;
if(xQueueSendToBack(bus->tx_queue, dali_msg, 0) == pdFALSE) {
bus->force_backward_tx = false;
log_tx_message("native TX queue full", bus, dali_msg, timeout_ms);
if (bus->tx_state == TX_STATE_IDLE) {
xQueueReset(bus->tx_queue);
}
return ESP_FAIL;
}
if (is_backward_tx_msg(dali_msg)) {
dali_msg->status = DALI_FRAME_OK;
return ESP_OK;
}
if(xQueueReceive(bus->tx_reply_queue, dali_msg, timeout_ticks) == pdFALSE) {
if (bus->tx_state == TX_STATE_IDLE && queue_waiting(bus->tx_queue) == 0) {
*dali_msg = bus->tx_data;
log_tx_message("native TX completion queue missed", bus, dali_msg, timeout_ms);
return dali_msg->status == DALI_FRAME_OK ? ESP_OK : ESP_FAIL;
}
log_tx_message("native TX completion timeout", bus, dali_msg, timeout_ms);
recover_tx_timeout(bus);
return ESP_FAIL;
}
ESP_LOGD(TAG, "bus=%u TX complete status=%u len=%u data=%02x %02x %02x %02x",
bus->bus_id, dali_msg->status, dali_msg->length, dali_msg->data[0],
dali_msg->data[1], dali_msg->data[2], dali_msg->data[3]);
return dali_msg->status == DALI_FRAME_OK ? ESP_OK : ESP_FAIL;
}
// dali_task - should run at highest priority
@@ -843,6 +1410,7 @@ void dali_task(void *pvParameters)
esp_err_t dali_hal_init(uint8_t dali_id, uint8_t tx_pin, uint8_t rx_pin)
{
dali_hal_lock();
apply_dali_log_level();
esp_err_t err = ensure_timing_ready_locked();
if (err == ESP_OK) {
@@ -873,6 +1441,7 @@ esp_err_t dali_hal_init(uint8_t dali_id, uint8_t tx_pin, uint8_t rx_pin)
esp_err_t dali_hal_set_baudrate(uint32_t baudrate)
{
dali_hal_lock();
apply_dali_log_level();
bool resume_timer = s_timer_started && gptimer;
if (resume_timer) {
+8
View File
@@ -3,6 +3,10 @@
#include <stdint.h>
#include "dali_hal.h"
#ifdef __cplusplus
extern "C" {
#endif
/*
Addressing: 102.7.2.1
0AAA AAAx - short address AAAAAA 0-63
@@ -77,5 +81,9 @@ void dali_set_search_addr24(uint32_t addr24);
uint32_t dali_binary_search();
void dali_change_short_address(int addr1, int addr2);
#ifdef __cplusplus
}
#endif
+24 -7
View File
@@ -8,6 +8,10 @@
#include "freertos/task.h"
#include "freertos/queue.h"
#ifdef __cplusplus
extern "C" {
#endif
#ifdef CONFIG_DALI_PHY_COUNT
#define DALI_PHY_COUNT CONFIG_DALI_PHY_COUNT
#else
@@ -182,13 +186,22 @@ typedef struct Dali_msg Dali_msg_t;
// define HW DALI for gpio functions
#ifndef DALI_HW_PINS
// define HW DALI for gpio functions
// - what we should write to pin to get HIGH/LOW
#define DALI_TX_HIGH 1 // idle state
#define DALI_TX_LOW 0 // active state
// - what we should read from pin to get HIGH/LOW
#define DALI_RX_HIGH 1 // idle state
#define DALI_RX_LOW 0 // active state
// Physical GPIO levels for logical DALI idle/high and active/low bus states.
#ifdef CONFIG_DALI_TX_ACTIVE_HIGH
#define DALI_TX_HIGH 0 // idle state
#define DALI_TX_LOW 1 // active state
#else
#define DALI_TX_HIGH 1 // idle state
#define DALI_TX_LOW 0 // active state
#endif
#ifdef CONFIG_DALI_RX_ACTIVE_LOW
#define DALI_RX_HIGH 1 // idle state
#define DALI_RX_LOW 0 // active state
#else
#define DALI_RX_HIGH 0 // idle state
#define DALI_RX_LOW 1 // active state
#endif
#endif
// LED onboard - debug
@@ -228,3 +241,7 @@ esp_err_t dali_hal_get_bus_info(uint8_t bus_id, dali_hal_bus_info_t *info);
QueueHandle_t dali_hal_raw_receive_queue(void);
void dali_task(void *pvParameters);
#ifdef __cplusplus
}
#endif
@@ -68,6 +68,7 @@ struct DaliChannelInfo {
uint8_t gateway_id{0};
DaliPhyKind phy_kind{DaliPhyKind::kCustom};
std::string name;
std::optional<uint8_t> native_bus_id;
};
struct DaliRawFrame {
@@ -125,6 +126,7 @@ class DaliDomainService {
void addRawFrameSink(std::function<void(const DaliRawFrame& frame)> sink);
bool resetBus(uint8_t gateway_id) const;
bool isBusIdle(uint8_t gateway_id, uint32_t quiet_ms) const;
bool writeBridgeFrame(uint8_t gateway_id, const uint8_t* data, size_t len) const;
std::vector<uint8_t> transactBridgeFrame(uint8_t gateway_id, const uint8_t* data,
size_t len) const;
@@ -162,6 +164,7 @@ class DaliDomainService {
bool on(uint8_t gateway_id, int short_address) const;
bool off(uint8_t gateway_id, int short_address) const;
bool off(int short_address) const;
std::optional<uint8_t> queryActualLevel(uint8_t gateway_id, int short_address) const;
std::optional<uint16_t> queryGroupMask(uint8_t gateway_id, int short_address) const;
std::optional<uint8_t> querySceneLevel(uint8_t gateway_id, int short_address, int scene) const;
std::optional<DaliAddressSettingsSnapshot> queryAddressSettings(uint8_t gateway_id,
@@ -194,10 +197,13 @@ class DaliDomainService {
static void RawFrameTaskEntry(void* arg);
void rawFrameTaskLoop();
void notifyRawFrameSinks(const DaliRawFrame& frame);
void markBusActivity(uint8_t gateway_id) const;
std::vector<std::unique_ptr<DaliChannel>> channels_;
std::vector<std::function<void(const DaliRawFrame& frame)>> raw_frame_sinks_;
SemaphoreHandle_t raw_frame_sink_lock_{nullptr};
mutable SemaphoreHandle_t bus_activity_lock_{nullptr};
mutable std::map<uint8_t, TickType_t> last_bus_activity_ticks_;
TaskHandle_t raw_frame_task_handle_{nullptr};
};
+310 -52
View File
@@ -1,22 +1,82 @@
#include "dali_domain.hpp"
#define LOG_LOCAL_LEVEL CONFIG_DALI_LOG_LEVEL
#include "esp_log.h"
#include "dali.h"
#include "dali_hal.h"
#include "dali.hpp"
#include "driver/uart.h"
#include "esp_log.h"
#include "freertos/queue.h"
#include <algorithm>
#include <utility>
#ifndef CONFIG_DALI_QUERY_RESPONSE_TIMEOUT_MS
#define CONFIG_DALI_QUERY_RESPONSE_TIMEOUT_MS 25
#endif
static const char *TAG = "dali_domain";
namespace gateway {
namespace {
constexpr const char* kTag = "dali_domain";
constexpr size_t kSerialRxPacketMaxBytes = 8;
constexpr UBaseType_t kSerialRxQueueDepth = 8;
constexpr uint32_t kHardwareQueryRawPostSuppressMs = 10;
portMUX_TYPE s_query_raw_suppress_lock = portMUX_INITIALIZER_UNLOCKED;
uint8_t s_query_raw_suppress_inflight[DALI_PHY_COUNT] = {};
TickType_t s_query_raw_suppress_post_until[DALI_PHY_COUNT] = {};
void BeginHardwareQueryRawSuppress(uint8_t bus_id) {
if (bus_id >= DALI_PHY_COUNT) {
return;
}
portENTER_CRITICAL(&s_query_raw_suppress_lock);
if (s_query_raw_suppress_inflight[bus_id] < UINT8_MAX) {
++s_query_raw_suppress_inflight[bus_id];
}
s_query_raw_suppress_post_until[bus_id] = 0;
portEXIT_CRITICAL(&s_query_raw_suppress_lock);
}
bool TakeHardwareQueryRawSuppress(uint8_t bus_id) {
if (bus_id >= DALI_PHY_COUNT) {
return false;
}
bool suppress = false;
const TickType_t now = xTaskGetTickCount();
portENTER_CRITICAL(&s_query_raw_suppress_lock);
const TickType_t post_until = s_query_raw_suppress_post_until[bus_id];
if (s_query_raw_suppress_inflight[bus_id] > 0) {
suppress = true;
} else if (post_until != 0 && now <= post_until) {
suppress = true;
} else if (post_until != 0) {
s_query_raw_suppress_post_until[bus_id] = 0;
}
portEXIT_CRITICAL(&s_query_raw_suppress_lock);
return suppress;
}
void ClearHardwareQueryRawSuppress(uint8_t bus_id) {
if (bus_id >= DALI_PHY_COUNT) {
return;
}
const TickType_t post_until = xTaskGetTickCount() +
pdMS_TO_TICKS(kHardwareQueryRawPostSuppressMs);
portENTER_CRITICAL(&s_query_raw_suppress_lock);
if (s_query_raw_suppress_inflight[bus_id] > 0) {
--s_query_raw_suppress_inflight[bus_id];
}
if (s_query_raw_suppress_inflight[bus_id] == 0) {
s_query_raw_suppress_post_until[bus_id] = post_until;
}
portEXIT_CRITICAL(&s_query_raw_suppress_lock);
}
DaliDomainSnapshot MakeSnapshot(uint8_t gateway_id, int address, const char* kind) {
DaliDomainSnapshot snapshot;
@@ -64,6 +124,20 @@ struct SerialRxPacket {
uint8_t data[kSerialRxPacketMaxBytes]{};
};
std::vector<uint8_t> LegacyQueryResponse(uint8_t status, uint8_t value = 0x00) {
return {status, value};
}
void LogQueryRxPacket(const char* transport, int id, const std::vector<uint8_t>& packet,
const char* note = nullptr) {
const unsigned first = packet.size() > 0 ? packet[0] : 0;
const unsigned second = packet.size() > 1 ? packet[1] : 0;
ESP_LOGD(TAG, "%s query rx packet id=%d len=%u data=%02x %02x%s%s",
transport == nullptr ? "dali" : transport, id,
static_cast<unsigned>(packet.size()), first, second,
note == nullptr ? "" : " note=", note == nullptr ? "" : note);
}
bool SendHardwareFrame(uint8_t bus_id, const uint8_t* data, size_t len) {
if (data == nullptr || len != 3) {
return false;
@@ -78,9 +152,11 @@ bool SendHardwareFrame(uint8_t bus_id, const uint8_t* data, size_t len) {
switch (data[0]) {
case 0x10:
ESP_LOGD(TAG, "sending hardware frame for bus=%u data=%02x %02x", bus_id, data[1], data[2]);
dali_send(&tx);
return true;
case 0x11:
ESP_LOGD(TAG, "sending extended frame for bus=%u data=%02x %02x", bus_id, data[1], data[2]);
dali_send_double(&tx);
return true;
default:
@@ -89,13 +165,21 @@ bool SendHardwareFrame(uint8_t bus_id, const uint8_t* data, size_t len) {
}
std::vector<uint8_t> TransactHardwareFrame(uint8_t bus_id, const uint8_t* data, size_t len) {
if (data == nullptr || len != 3) {
if (data == nullptr) {
return {};
}
if (len != 3) {
if (len > 0 && data[0] == 0x12) {
const auto packet = LegacyQueryResponse(0xFD);
LogQueryRxPacket("hardware", bus_id, packet, "invalid-query-len");
return packet;
}
return std::vector<uint8_t>{};
}
switch (data[0]) {
case 0x00:
return {0xFF};
ESP_LOGD(TAG, "received reset for bus=%u", bus_id);
case 0x01:
return {1};
case 0x10:
@@ -103,13 +187,31 @@ std::vector<uint8_t> TransactHardwareFrame(uint8_t bus_id, const uint8_t* data,
return SendHardwareFrame(bus_id, data, len) ? std::vector<uint8_t>{0xFF}
: std::vector<uint8_t>{0xFD};
case 0x12: {
ESP_LOGD(TAG, "received hardware query frame for bus=%u data=%02x %02x", bus_id, data[1],
data[2]);
Dali_msg_t tx = dali_msg_new(data[1], data[2]);
tx.id = bus_id;
Dali_msg_t rx = {};
BeginHardwareQueryRawSuppress(bus_id);
if (dali_query(&tx, &rx) == pdTRUE) {
return {0xFF, rx.data[0]};
ClearHardwareQueryRawSuppress(bus_id);
if (rx.status != DALI_FRAME_OK || rx.length != 8) {
ESP_LOGW(TAG, "hardware query response for bus=%u has invalid status or length", bus_id);
const auto packet = LegacyQueryResponse(0xFD);
LogQueryRxPacket("hardware", bus_id, packet, "invalid-status-or-length");
return packet;
}
ESP_LOGD(TAG, "got hardware query response for bus=%u status=%u len=%u data=%02x %02x "
"%02x %02x",
bus_id, rx.status, rx.length, rx.data[0], rx.data[1], rx.data[2], rx.data[3]);
const std::vector<uint8_t> packet{0xFF, rx.data[0]};
LogQueryRxPacket("hardware", bus_id, packet, "ok");
return packet;
}
return {0xFE};
ClearHardwareQueryRawSuppress(bus_id);
const auto packet = LegacyQueryResponse(0xFE);
LogQueryRxPacket("hardware", bus_id, packet, "no-response");
return packet;
}
default:
return {};
@@ -151,13 +253,25 @@ std::vector<uint8_t> ReadSerialFrame(QueueHandle_t queue, size_t len, uint32_t t
std::vector<uint8_t> TransactSerialFrame(int uart_port, QueueHandle_t queue,
uint32_t query_timeout_ms, const uint8_t* data,
size_t len) {
if (data == nullptr || len == 0) {
const auto packet = LegacyQueryResponse(0xFD);
LogQueryRxPacket("serial", uart_port, packet, "empty-query");
return packet;
}
if (data[0] == 0x12 && len != 3) {
const auto packet = LegacyQueryResponse(0xFD);
LogQueryRxPacket("serial", uart_port, packet, "invalid-query-len");
return packet;
}
if (data != nullptr && len > 0 && data[0] == 0x12) {
DrainSerialQueue(queue);
}
if (!WriteSerialFrame(uart_port, data, len)) {
return {0xFD};
const auto packet = LegacyQueryResponse(0xFD);
LogQueryRxPacket("serial", uart_port, packet, "write-failed");
return packet;
}
if (data == nullptr || len == 0 || data[0] != 0x12) {
if (data[0] != 0x12) {
return {0xFF};
}
@@ -171,12 +285,17 @@ std::vector<uint8_t> TransactSerialFrame(int uart_port, QueueHandle_t queue,
break;
}
auto response = PacketToVector(packet, 2);
LogQueryRxPacket("serial", uart_port, response, "raw");
if (!response.empty() &&
(response[0] == 0xFF || response[0] == 0xFE || response[0] == 0xFD)) {
return response;
const auto packet = LegacyQueryResponse(response[0], response.size() > 1 ? response[1] : 0x00);
LogQueryRxPacket("serial", uart_port, packet, "accepted");
return packet;
}
}
return {0xFE};
const auto packet = LegacyQueryResponse(0xFE);
LogQueryRxPacket("serial", uart_port, packet, "timeout");
return packet;
}
} // namespace
@@ -195,9 +314,21 @@ struct DaliDomainService::DaliChannel {
};
DaliDomainService::DaliDomainService()
: raw_frame_sink_lock_(xSemaphoreCreateMutex()) {}
: raw_frame_sink_lock_(xSemaphoreCreateMutex()),
bus_activity_lock_(xSemaphoreCreateMutex()) {
esp_log_level_set(TAG, (esp_log_level_t)CONFIG_DALI_LOG_LEVEL);
}
DaliDomainService::~DaliDomainService() = default;
DaliDomainService::~DaliDomainService() {
if (raw_frame_sink_lock_ != nullptr) {
vSemaphoreDelete(raw_frame_sink_lock_);
raw_frame_sink_lock_ = nullptr;
}
if (bus_activity_lock_ != nullptr) {
vSemaphoreDelete(bus_activity_lock_);
bus_activity_lock_ = nullptr;
}
}
bool DaliDomainService::bindTransport(const DaliChannelConfig& config, DaliTransportHooks hooks) {
if (!hooks.send) {
@@ -225,14 +356,14 @@ bool DaliDomainService::bindTransport(const DaliChannelConfig& config, DaliTrans
esp_err_t DaliDomainService::bindHardwareBus(const DaliHardwareBusConfig& config) {
esp_err_t err = dali_hal_set_baudrate(config.baudrate);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to set baudrate=%lu: %s", static_cast<unsigned long>(config.baudrate),
ESP_LOGE(TAG, "failed to set baudrate=%lu: %s", static_cast<unsigned long>(config.baudrate),
esp_err_to_name(err));
return err;
}
err = dali_hal_init(config.bus_id, config.tx_pin, config.rx_pin);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to init bus=%u tx=%u rx=%u: %s", config.bus_id, config.tx_pin,
ESP_LOGE(TAG, "failed to init bus=%u tx=%u rx=%u: %s", config.bus_id, config.tx_pin,
config.rx_pin, esp_err_to_name(err));
return err;
}
@@ -258,9 +389,9 @@ esp_err_t DaliDomainService::bindHardwareBus(const DaliHardwareBusConfig& config
}
err = startRawFrameTask();
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to start raw frame task: %s", esp_err_to_name(err));
ESP_LOGW(TAG, "failed to start raw frame task: %s", esp_err_to_name(err));
}
ESP_LOGI(kTag, "bound channel=%u gateway=%u hardware bus=%u tx=%u rx=%u baudrate=%lu",
ESP_LOGI(TAG, "bound channel=%u gateway=%u hardware bus=%u tx=%u rx=%u baudrate=%lu",
config.channel_index, config.gateway_id, config.bus_id, config.tx_pin, config.rx_pin,
static_cast<unsigned long>(config.baudrate));
return ESP_OK;
@@ -271,7 +402,7 @@ esp_err_t DaliDomainService::bindSerialBus(const DaliSerialBusConfig& config) {
return ESP_ERR_INVALID_ARG;
}
if (hasSerialPort(config.uart_port)) {
ESP_LOGE(kTag, "uart%d is already assigned to another DALI channel", config.uart_port);
ESP_LOGE(TAG, "uart%d is already assigned to another DALI channel", config.uart_port);
return ESP_ERR_INVALID_STATE;
}
@@ -287,26 +418,27 @@ esp_err_t DaliDomainService::bindSerialBus(const DaliSerialBusConfig& config) {
auto uart = static_cast<uart_port_t>(config.uart_port);
esp_err_t err = uart_param_config(uart, &uart_config);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to configure uart%d: %s", config.uart_port, esp_err_to_name(err));
ESP_LOGE(TAG, "failed to configure uart%d: %s", config.uart_port, esp_err_to_name(err));
return err;
}
err = uart_set_pin(uart, config.tx_pin, config.rx_pin, UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE);
err = uart_set_pin(uart, config.tx_pin < 0 ? UART_PIN_NO_CHANGE : config.tx_pin,
config.rx_pin < 0 ? UART_PIN_NO_CHANGE : config.rx_pin,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to set uart%d pins tx=%d rx=%d: %s", config.uart_port,
ESP_LOGE(TAG, "failed to set uart%d pins tx=%d rx=%d: %s", config.uart_port,
config.tx_pin, config.rx_pin, esp_err_to_name(err));
return err;
}
err = uart_driver_install(uart, config.rx_buffer_size, config.tx_buffer_size, 0, nullptr, 0);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(kTag, "failed to install uart%d driver: %s", config.uart_port, esp_err_to_name(err));
ESP_LOGE(TAG, "failed to install uart%d driver: %s", config.uart_port, esp_err_to_name(err));
return err;
}
uart_flush_input(uart);
QueueHandle_t serial_rx_queue = xQueueCreate(kSerialRxQueueDepth, sizeof(SerialRxPacket));
if (serial_rx_queue == nullptr) {
ESP_LOGE(kTag, "failed to create uart%d RX queue", config.uart_port);
ESP_LOGE(TAG, "failed to create uart%d RX queue", config.uart_port);
return ESP_ERR_NO_MEM;
}
@@ -336,12 +468,12 @@ esp_err_t DaliDomainService::bindSerialBus(const DaliSerialBusConfig& config) {
channel->serial_rx_queue = serial_rx_queue;
err = startSerialRxTask(*channel);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to start uart%d RX task: %s", config.uart_port,
ESP_LOGE(TAG, "failed to start uart%d RX task: %s", config.uart_port,
esp_err_to_name(err));
return err;
}
}
ESP_LOGI(kTag, "bound channel=%u gateway=%u serial uart%d tx=%d rx=%d baudrate=%lu",
ESP_LOGI(TAG, "bound channel=%u gateway=%u serial uart%d tx=%d rx=%d baudrate=%lu",
config.channel_index, config.gateway_id, config.uart_port, config.tx_pin, config.rx_pin,
static_cast<unsigned long>(config.baudrate));
return ESP_OK;
@@ -386,8 +518,12 @@ std::vector<DaliChannelInfo> DaliDomainService::channelInfo() const {
std::vector<DaliChannelInfo> info;
info.reserve(channels_.size());
for (const auto& channel : channels_) {
info.push_back(DaliChannelInfo{channel->config.channel_index, channel->config.gateway_id,
channel->phy_kind, channel->config.name});
DaliChannelInfo item{channel->config.channel_index, channel->config.gateway_id,
channel->phy_kind, channel->config.name};
if (channel->hardware_bus.has_value()) {
item.native_bus_id = channel->hardware_bus->bus_id;
}
info.push_back(std::move(item));
}
return info;
}
@@ -407,12 +543,38 @@ void DaliDomainService::addRawFrameSink(std::function<void(const DaliRawFrame& f
bool DaliDomainService::resetBus(uint8_t gateway_id) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->comm != nullptr && channel->comm->resetBus();
if (channel == nullptr || channel->comm == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->comm->resetBus();
}
bool DaliDomainService::isBusIdle(uint8_t gateway_id, uint32_t quiet_ms) const {
TickType_t last_activity = 0;
if (bus_activity_lock_ != nullptr) {
xSemaphoreTake(bus_activity_lock_, portMAX_DELAY);
}
if (const auto it = last_bus_activity_ticks_.find(gateway_id);
it != last_bus_activity_ticks_.end()) {
last_activity = it->second;
}
if (bus_activity_lock_ != nullptr) {
xSemaphoreGive(bus_activity_lock_);
}
if (last_activity == 0) {
return true;
}
return (xTaskGetTickCount() - last_activity) >= pdMS_TO_TICKS(quiet_ms);
}
bool DaliDomainService::writeBridgeFrame(uint8_t gateway_id, const uint8_t* data, size_t len) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->hooks.send && channel->hooks.send(data, len);
if (channel == nullptr || !channel->hooks.send) {
return false;
}
markBusActivity(gateway_id);
return channel->hooks.send(data, len);
}
std::vector<uint8_t> DaliDomainService::transactBridgeFrame(uint8_t gateway_id,
@@ -422,17 +584,26 @@ std::vector<uint8_t> DaliDomainService::transactBridgeFrame(uint8_t gateway_id,
if (channel == nullptr || !channel->hooks.transact) {
return {};
}
markBusActivity(gateway_id);
return channel->hooks.transact(data, len);
}
bool DaliDomainService::sendRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->comm != nullptr && channel->comm->sendRawNew(raw_addr, command);
if (channel == nullptr || channel->comm == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->comm->sendRawNew(raw_addr, command);
}
bool DaliDomainService::sendExtRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->comm != nullptr && channel->comm->sendExtRawNew(raw_addr, command);
if (channel == nullptr || channel->comm == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->comm->sendExtRawNew(raw_addr, command);
}
std::optional<uint8_t> DaliDomainService::queryRaw(uint8_t gateway_id, uint8_t raw_addr,
@@ -441,6 +612,7 @@ std::optional<uint8_t> DaliDomainService::queryRaw(uint8_t gateway_id, uint8_t r
if (channel == nullptr || channel->comm == nullptr) {
return std::nullopt;
}
markBusActivity(gateway_id);
return channel->comm->queryRawNew(raw_addr, command);
}
@@ -451,6 +623,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::discoverDeviceTypes(
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
markBusActivity(gateway_id);
const std::vector<int> fallback = fallback_types.empty() ? std::vector<int>{1, 4, 5, 6, 8}
: fallback_types;
auto discovery = channel->dali->base.discoverDeviceTypes(short_address, fallback,
@@ -472,6 +645,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::baseStatusSnapshot(
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
markBusActivity(gateway_id);
const auto raw_status = channel->dali->base.getStatus(short_address);
if (!raw_status.has_value()) {
return std::nullopt;
@@ -497,6 +671,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt1Snapshot(uint8_t gateway
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
markBusActivity(gateway_id);
const auto detailed = channel->dali->dt1.getDT1TestStatusDetailed(short_address);
if (!detailed.has_value()) {
return std::nullopt;
@@ -579,6 +754,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt4Snapshot(uint8_t gateway
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
markBusActivity(gateway_id);
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt4");
auto& dt4 = channel->dali->dt4;
PutOptionalInt(snapshot, "extendedVersion", dt4.getExtendedVersion(short_address));
@@ -661,6 +837,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt5Snapshot(uint8_t gateway
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
markBusActivity(gateway_id);
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt5");
auto& dt5 = channel->dali->dt5;
PutOptionalInt(snapshot, "extendedVersion", dt5.getExtendedVersion(short_address));
@@ -702,6 +879,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt6Snapshot(uint8_t gateway
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
markBusActivity(gateway_id);
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt6");
auto& dt6 = channel->dali->dt6;
PutOptionalInt(snapshot, "extendedVersion", dt6.getExtendedVersion(short_address));
@@ -765,6 +943,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt8StatusSnapshot(
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
markBusActivity(gateway_id);
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt8_status");
bool has_data = false;
@@ -799,6 +978,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt8SceneColorReport(
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
markBusActivity(gateway_id);
const auto report = channel->dali->dt8.getSceneColorReport(short_address, scene);
if (!report.has_value()) {
return std::nullopt;
@@ -822,6 +1002,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt8PowerOnLevelColorReport(
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
markBusActivity(gateway_id);
const auto report = channel->dali->dt8.getPowerOnLevelColorReport(short_address);
if (!report.has_value()) {
return std::nullopt;
@@ -844,6 +1025,7 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt8SystemFailureLevelColorR
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
markBusActivity(gateway_id);
const auto report = channel->dali->dt8.getSystemFailureLevelColorReport(short_address);
if (!report.has_value()) {
return std::nullopt;
@@ -866,8 +1048,11 @@ bool DaliDomainService::storeDt8SceneSnapshot(uint8_t gateway_id, int short_addr
int color_temperature, int red, int green,
int blue) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->dt8.storeSceneSnapshot(short_address, scene, brightness,
if (channel == nullptr || channel->dali == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->dali->dt8.storeSceneSnapshot(short_address, scene, brightness,
ToDaliCppColorMode(color_mode), color_temperature,
red, green, blue);
}
@@ -875,56 +1060,85 @@ bool DaliDomainService::storeDt8SceneSnapshot(uint8_t gateway_id, int short_addr
bool DaliDomainService::storeDt8PowerOnLevelSnapshot(uint8_t gateway_id, int short_address,
int level) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->dt8.storePowerOnLevelSnapshot(short_address, level);
if (channel == nullptr || channel->dali == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->dali->dt8.storePowerOnLevelSnapshot(short_address, level);
}
bool DaliDomainService::storeDt8SystemFailureLevelSnapshot(uint8_t gateway_id,
int short_address, int level) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->dt8.storeSystemFailureLevelSnapshot(short_address, level);
if (channel == nullptr || channel->dali == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->dali->dt8.storeSystemFailureLevelSnapshot(short_address, level);
}
bool DaliDomainService::setBright(uint8_t gateway_id, int short_address, int brightness) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->base.setBright(short_address, brightness);
if (channel == nullptr || channel->dali == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->dali->base.setBright(short_address, brightness);
}
bool DaliDomainService::setColTempRaw(uint8_t gateway_id, int short_address, int mirek) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->dt8.setColTempRaw(short_address, mirek);
if (channel == nullptr || channel->dali == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->dali->dt8.setColTempRaw(short_address, mirek);
}
bool DaliDomainService::setColTemp(uint8_t gateway_id, int short_address, int kelvin) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->dt8.setColorTemperature(short_address, kelvin);
if (channel == nullptr || channel->dali == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->dali->dt8.setColorTemperature(short_address, kelvin);
}
bool DaliDomainService::setColourRaw(uint8_t gateway_id, int raw_addr, int x, int y) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->dt8.setColourRaw(raw_addr, x, y);
if (channel == nullptr || channel->dali == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->dali->dt8.setColourRaw(raw_addr, x, y);
}
bool DaliDomainService::setColourRGB(uint8_t gateway_id, int short_address, int r, int g,
int b) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->dt8.setColourRGB(short_address, r, g, b);
if (channel == nullptr || channel->dali == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->dali->dt8.setColourRGB(short_address, r, g, b);
}
bool DaliDomainService::on(uint8_t gateway_id, int short_address) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr && channel->dali->base.on(short_address);
if (channel == nullptr || channel->dali == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->dali->base.on(short_address);
}
bool DaliDomainService::off(uint8_t gateway_id, int short_address) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr && channel->dali->base.off(short_address);
if (channel == nullptr || channel->dali == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->dali->base.off(short_address);
}
bool DaliDomainService::off(int short_address) const {
@@ -935,6 +1149,20 @@ bool DaliDomainService::off(int short_address) const {
return off(channels_.front()->config.gateway_id, short_address);
}
std::optional<uint8_t> DaliDomainService::queryActualLevel(uint8_t gateway_id,
int short_address) const {
const auto* channel = findChannelByGateway(gateway_id);
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
markBusActivity(gateway_id);
const auto level = channel->dali->base.getBright(short_address);
if (!level.has_value()) {
return std::nullopt;
}
return static_cast<uint8_t>(std::clamp(*level, 0, 254));
}
std::optional<uint16_t> DaliDomainService::queryGroupMask(uint8_t gateway_id,
int short_address) const {
const auto* channel = findChannelByGateway(gateway_id);
@@ -942,6 +1170,8 @@ std::optional<uint16_t> DaliDomainService::queryGroupMask(uint8_t gateway_id,
return std::nullopt;
}
markBusActivity(gateway_id);
const auto group_mask = channel->dali->base.getGroup(short_address);
if (!group_mask.has_value()) {
return std::nullopt;
@@ -957,6 +1187,8 @@ std::optional<uint8_t> DaliDomainService::querySceneLevel(uint8_t gateway_id, in
return std::nullopt;
}
markBusActivity(gateway_id);
const auto level = channel->dali->base.getScene(short_address, scene);
if (!level.has_value()) {
return std::nullopt;
@@ -972,6 +1204,8 @@ std::optional<DaliAddressSettingsSnapshot> DaliDomainService::queryAddressSettin
return std::nullopt;
}
markBusActivity(gateway_id);
DaliAddressSettingsSnapshot settings{};
if (const auto value = channel->dali->base.getPowerOnLevel(short_address); value.has_value()) {
@@ -1004,8 +1238,11 @@ std::optional<DaliAddressSettingsSnapshot> DaliDomainService::queryAddressSettin
bool DaliDomainService::applyGroupMask(uint8_t gateway_id, int short_address,
uint16_t group_mask) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->base.setGroup(short_address, group_mask);
if (channel == nullptr || channel->dali == nullptr) {
return false;
}
markBusActivity(gateway_id);
return channel->dali->base.setGroup(short_address, group_mask);
}
bool DaliDomainService::applySceneLevel(uint8_t gateway_id, int short_address, int scene,
@@ -1015,6 +1252,8 @@ bool DaliDomainService::applySceneLevel(uint8_t gateway_id, int short_address, i
return false;
}
markBusActivity(gateway_id);
if (*level == 255U) {
return channel->dali->base.removeScene(short_address, scene);
}
@@ -1030,6 +1269,8 @@ bool DaliDomainService::applyAddressSettings(uint8_t gateway_id, int short_addre
return false;
}
markBusActivity(gateway_id);
bool ok = true;
if (settings.power_on_level.has_value()) {
ok = ok && channel->dali->base.setPowerOnLevel(short_address, *settings.power_on_level);
@@ -1215,7 +1456,7 @@ void DaliDomainService::rawFrameTaskLoop() {
Dali_msg_t message = {};
while (true) {
if (queue == nullptr) {
vTaskDelay(pdMS_TO_TICKS(100));
vTaskDelay(pdMS_TO_TICKS(10));
queue = dali_hal_raw_receive_queue();
continue;
}
@@ -1233,6 +1474,12 @@ void DaliDomainService::rawFrameTaskLoop() {
if (byte_count > DALI_MAX_BYTES) {
byte_count = DALI_MAX_BYTES;
}
if (byte_count == 1 && TakeHardwareQueryRawSuppress(message.id)) {
continue;
}
if (byte_count != 1 && byte_count != 2 && byte_count != 3) {
continue;
}
DaliRawFrame frame;
frame.channel_index = channel->config.channel_index;
frame.gateway_id = channel->config.gateway_id;
@@ -1243,6 +1490,7 @@ void DaliDomainService::rawFrameTaskLoop() {
}
void DaliDomainService::notifyRawFrameSinks(const DaliRawFrame& frame) {
markBusActivity(frame.gateway_id);
if (raw_frame_sink_lock_ != nullptr) {
xSemaphoreTake(raw_frame_sink_lock_, portMAX_DELAY);
}
@@ -1255,6 +1503,16 @@ void DaliDomainService::notifyRawFrameSinks(const DaliRawFrame& frame) {
}
}
void DaliDomainService::markBusActivity(uint8_t gateway_id) const {
if (bus_activity_lock_ != nullptr) {
xSemaphoreTake(bus_activity_lock_, portMAX_DELAY);
}
last_bus_activity_ticks_[gateway_id] = xTaskGetTickCount();
if (bus_activity_lock_ != nullptr) {
xSemaphoreGive(bus_activity_lock_);
}
}
bool DaliDomainService::hasSerialPort(int uart_port) const {
return std::any_of(channels_.begin(), channels_.end(), [uart_port](const auto& channel) {
return channel->serial_bus.has_value() && channel->serial_bus->uart_port == uart_port;
+20 -1
View File
@@ -141,6 +141,13 @@ void RegisterGatt(struct ble_gatt_register_ctxt* ctxt, void* arg) {
}
}
std::vector<uint8_t> LegacyRawPayload(const std::vector<uint8_t>& data) {
if (data.size() == 1) {
return {0xBE, data[0]};
}
return data;
}
const struct ble_gatt_svc_def kGattServices[] = {
{
.type = BLE_GATT_SVC_TYPE_PRIMARY,
@@ -377,7 +384,7 @@ void GatewayBleBridge::handleDaliRawFrame(const DaliRawFrame& frame) {
if (!enabled_ || conn_handle_ == kInvalidConnectionHandle || frame.data.empty()) {
return;
}
notifyCharacteristic(frame.channel_index, frame.data);
notifyCharacteristic(frame.channel_index, LegacyRawPayload(frame.data));
}
void GatewayBleBridge::handleRawWrite(size_t channel_index, const std::vector<uint8_t>& payload) {
@@ -448,6 +455,18 @@ int GatewayBleBridge::handleGapEvent(struct ble_gap_event* event) {
notify_enabled_.fill(false);
last_notify_payload_.clear();
last_notify_at_us_ = 0;
struct ble_gap_upd_params params = {
.itvl_min = 15,
.itvl_max = 15,
.latency = 3,
.supervision_timeout = 1000,
.min_ce_len = 0,
.max_ce_len = 0,
};
int rc = ble_gap_update_params(event->connect.conn_handle, &params);
if (rc != 0) {
ESP_LOGW(kTag, "ble_gap_update_params rc=%d", rc);
}
ESP_LOGI(kTag, "BLE client connected handle=%u", conn_handle_);
} else if (enabled_) {
startAdvertising();
+4 -2
View File
@@ -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
@@ -1,5 +1,6 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
@@ -37,7 +38,7 @@ struct GatewayBridgeServiceConfig {
std::vector<int> reserved_uart_ports;
uint32_t bacnet_task_stack_size{8192};
UBaseType_t bacnet_task_priority{5};
uint32_t knx_task_stack_size{8192};
uint32_t knx_task_stack_size{12288};
UBaseType_t knx_task_priority{5};
std::optional<GatewayKnxConfig> default_knx_config;
};
@@ -60,12 +61,22 @@ class GatewayBridgeService {
const std::string& query = {});
GatewayBridgeHttpResponse handlePost(const std::string& action, int gateway_id,
const std::string& body);
std::string handleTransportRequest(uint8_t gateway_id, std::string_view request);
private:
struct ChannelRuntime;
ChannelRuntime* findRuntime(uint8_t gateway_id);
const ChannelRuntime* findRuntime(uint8_t gateway_id) const;
ChannelRuntime* selectKnxEndpointRuntime();
bool isKnxEndpointRuntime(const ChannelRuntime* runtime) const;
esp_err_t startKnxEndpoint(ChannelRuntime* requested_runtime,
std::set<int>* used_uarts = nullptr);
esp_err_t stopKnxEndpoint(ChannelRuntime* requested_runtime);
DaliBridgeResult routeKnxGroupWrite(uint16_t group_address, const uint8_t* data,
size_t len);
DaliBridgeResult routeKnxGroupObjectWrite(uint16_t group_object_number,
const uint8_t* data, size_t len);
void handleDaliRawFrame(const DaliRawFrame& frame);
void collectUsedRuntimeResources(uint8_t except_gateway_id,
std::set<uint16_t>* modbus_tcp_ports,
@@ -76,6 +87,7 @@ class GatewayBridgeService {
GatewayCache& cache_;
GatewayBridgeServiceConfig config_;
std::vector<std::unique_ptr<ChannelRuntime>> runtimes_;
ChannelRuntime* knx_endpoint_runtime_{nullptr};
};
} // namespace gateway
@@ -0,0 +1,37 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
namespace gateway::openknx {
struct FactoryFdskInfo {
bool available{false};
std::string serialNumber;
std::string label;
std::string qrCode;
};
struct FactoryCertificatePayload {
bool available{false};
std::string productIdentity;
std::string manufacturerId;
std::string applicationNumber;
std::string applicationVersion;
std::string serialNumber;
std::string fdskLabel;
std::string fdskQrCode;
std::string storage;
std::string createdAt;
std::string checksum;
};
bool LoadFactoryFdsk(uint8_t* data, size_t len);
FactoryFdskInfo LoadFactoryFdskInfo();
bool GenerateFactoryFdsk(FactoryFdskInfo* info = nullptr);
bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info = nullptr);
bool ResetFactoryFdskCache(FactoryFdskInfo* info = nullptr);
FactoryCertificatePayload BuildFactoryCertificatePayload();
} // namespace gateway::openknx
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,384 @@
#include "security_storage.h"
#include "gateway_knx_internal.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_timer.h"
#include "mbedtls/sha256.h"
#include "nvs.h"
#include "nvs_flash.h"
#include <algorithm>
#include <array>
#include <cinttypes>
#include <cstddef>
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <string>
namespace {
constexpr const char* kTag = "openknx_sec";
constexpr const char* kNamespace = "knx_sec";
constexpr const char* kFactoryFdskKey = "factory_fdsk";
constexpr size_t kFdskSize = 16;
constexpr size_t kSerialSize = 6;
constexpr size_t kFdskQrSize = 36;
constexpr const char* kProductIdentity = "REG1-Dali";
constexpr const char* kDevelopmentStorage = "base_mac_derived_plain_nvs_development";
constexpr char kFdskDerivationLabel[] = "DaliMaster REG1-Dali deterministic FDSK v1";
constexpr uint8_t kCrc4Tab[16] = {
0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9,
0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2,
};
constexpr char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
constexpr char kHexAlphabet[] = "0123456789ABCDEF";
extern "C" void knx_platform_clear_cached_fdsk() __attribute__((weak));
std::string hexValue(uint32_t value, int width) {
std::array<char, 9> buffer{};
std::snprintf(buffer.data(), buffer.size(), "%0*" PRIX32, width, value);
return buffer.data();
}
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;
}
bool plausibleKey(const uint8_t* data) {
const bool all_zero = std::all_of(data, data + kFdskSize, [](uint8_t value) {
return value == 0x00;
});
const bool all_ff = std::all_of(data, data + kFdskSize, [](uint8_t value) {
return value == 0xff;
});
return !all_zero && !all_ff;
}
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;
}
void clearOpenKnxFdskCache() {
if (knx_platform_clear_cached_fdsk != nullptr) {
knx_platform_clear_cached_fdsk();
}
}
int fromHexDigit(char value) {
if (value >= '0' && value <= '9') {
return value - '0';
}
if (value >= 'a' && value <= 'f') {
return value - 'a' + 10;
}
if (value >= 'A' && value <= 'F') {
return value - 'A' + 10;
}
return -1;
}
bool parseHexKey(const std::string& value, uint8_t* out) {
std::string digits;
digits.reserve(value.size());
for (char ch : value) {
if (ch == ':' || ch == '-' || ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') {
continue;
}
if (fromHexDigit(ch) < 0) {
return false;
}
digits.push_back(ch);
}
if (digits.size() != kFdskSize * 2U) {
return false;
}
for (size_t index = 0; index < kFdskSize; ++index) {
const int hi = fromHexDigit(digits[index * 2U]);
const int lo = fromHexDigit(digits[index * 2U + 1U]);
if (hi < 0 || lo < 0) {
return false;
}
out[index] = static_cast<uint8_t>((hi << 4) | lo);
}
return plausibleKey(out);
}
bool loadKnxSerialNumber(uint8_t* serial) {
if (serial == nullptr) {
return false;
}
std::array<uint8_t, kSerialSize> mac{};
if (!readBaseMac(mac.data())) {
return false;
}
serial[0] = static_cast<uint8_t>(
(gateway::knx_internal::kReg1DaliManufacturerId >> 8) & 0xff);
serial[1] = static_cast<uint8_t>(
gateway::knx_internal::kReg1DaliManufacturerId & 0xff);
std::copy(mac.begin() + 2, mac.end(), serial + 2);
return true;
}
bool deriveFactoryFdskFromSerial(const uint8_t* serial, uint8_t* key) {
if (serial == nullptr || key == nullptr) {
return false;
}
std::array<uint8_t, sizeof(kFdskDerivationLabel) - 1 + kSerialSize> material{};
std::copy(kFdskDerivationLabel, kFdskDerivationLabel + sizeof(kFdskDerivationLabel) - 1,
material.begin());
std::copy(serial, serial + kSerialSize, material.begin() + sizeof(kFdskDerivationLabel) - 1);
std::array<uint8_t, 32> digest{};
if (mbedtls_sha256(material.data(), material.size(), digest.data(), 0) != 0) {
return false;
}
std::copy(digest.begin(), digest.begin() + kFdskSize, key);
if (!plausibleKey(key)) {
key[kFdskSize - 1] ^= 0xA5;
}
return plausibleKey(key);
}
void syncFactoryFdskToNvs(const uint8_t* data) {
if (data == nullptr || !plausibleKey(data) || !ensureNvsReady()) {
return;
}
std::array<uint8_t, kFdskSize> stored{};
size_t stored_size = stored.size();
nvs_handle_t handle = 0;
esp_err_t err = nvs_open(kNamespace, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err));
return;
}
err = nvs_get_blob(handle, kFactoryFdskKey, stored.data(), &stored_size);
if (err == ESP_OK && stored_size == stored.size() &&
std::equal(stored.begin(), stored.end(), data)) {
nvs_close(handle);
return;
}
err = nvs_set_blob(handle, kFactoryFdskKey, data, kFdskSize);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to mirror deterministic KNX factory FDSK: %s", esp_err_to_name(err));
return;
}
clearOpenKnxFdskCache();
}
uint8_t crc4Array(const uint8_t* data, size_t len) {
uint8_t crc = 0;
for (size_t i = 0; i < len; ++i) {
crc = kCrc4Tab[crc ^ (data[i] >> 4)];
crc = kCrc4Tab[crc ^ (data[i] & 0x0f)];
}
return crc;
}
std::string toBase32NoPadding(const uint8_t* data, size_t len) {
std::string result;
result.reserve(((len * 8) + 4) / 5);
uint32_t buffer = 0;
int bits_left = 0;
for (size_t i = 0; i < len; ++i) {
buffer = (buffer << 8) | data[i];
bits_left += 8;
while (bits_left >= 5) {
const uint8_t index = static_cast<uint8_t>((buffer >> (bits_left - 5)) & 0x1f);
result.push_back(kBase32Alphabet[index]);
bits_left -= 5;
}
}
if (bits_left > 0) {
const uint8_t index = static_cast<uint8_t>((buffer << (5 - bits_left)) & 0x1f);
result.push_back(kBase32Alphabet[index]);
}
return result;
}
std::string toHex(const uint8_t* data, size_t len) {
std::string result;
result.reserve(len * 2);
for (size_t i = 0; i < len; ++i) {
result.push_back(kHexAlphabet[(data[i] >> 4) & 0x0f]);
result.push_back(kHexAlphabet[data[i] & 0x0f]);
}
return result;
}
std::string generateFdskQrCode(const uint8_t* serial, const uint8_t* key) {
std::array<uint8_t, kSerialSize + kFdskSize + 1> buffer{};
std::copy(serial, serial + kSerialSize, buffer.begin());
std::copy(key, key + kFdskSize, buffer.begin() + kSerialSize);
buffer[kSerialSize + kFdskSize] = static_cast<uint8_t>((crc4Array(buffer.data(), buffer.size() - 1) << 4) & 0xff);
std::string encoded = toBase32NoPadding(buffer.data(), buffer.size());
if (encoded.size() > kFdskQrSize) {
encoded.resize(kFdskQrSize);
}
return encoded;
}
std::string formatFdskLabel(const std::string& qr_code) {
std::string label;
label.reserve(qr_code.size() + (qr_code.size() / 6));
for (size_t i = 0; i < qr_code.size(); ++i) {
if (i != 0 && (i % 6) == 0) {
label.push_back('-');
}
label.push_back(qr_code[i]);
}
return label;
}
std::string fnv1aHex(const std::string& value) {
uint32_t hash = 2166136261u;
for (unsigned char ch : value) {
hash ^= ch;
hash *= 16777619u;
}
std::array<uint8_t, 4> bytes{
static_cast<uint8_t>((hash >> 24) & 0xff),
static_cast<uint8_t>((hash >> 16) & 0xff),
static_cast<uint8_t>((hash >> 8) & 0xff),
static_cast<uint8_t>(hash & 0xff),
};
return toHex(bytes.data(), bytes.size());
}
} // namespace
namespace gateway::openknx {
bool LoadFactoryFdsk(uint8_t* data, size_t len) {
if (data == nullptr || len < kFdskSize) {
return false;
}
std::array<uint8_t, kSerialSize> serial{};
std::array<uint8_t, kFdskSize> key{};
if (!loadKnxSerialNumber(serial.data()) ||
!deriveFactoryFdskFromSerial(serial.data(), key.data())) {
return false;
}
std::memcpy(data, key.data(), kFdskSize);
syncFactoryFdskToNvs(key.data());
return true;
}
FactoryFdskInfo LoadFactoryFdskInfo() {
FactoryFdskInfo info;
std::array<uint8_t, kFdskSize> key{};
std::array<uint8_t, kSerialSize> serial{};
if (!loadKnxSerialNumber(serial.data()) || !LoadFactoryFdsk(key.data(), key.size())) {
return info;
}
info.available = true;
info.serialNumber = toHex(serial.data(), serial.size());
info.qrCode = generateFdskQrCode(serial.data(), key.data());
info.label = formatFdskLabel(info.qrCode);
return info;
}
bool GenerateFactoryFdsk(FactoryFdskInfo* info) {
std::array<uint8_t, kFdskSize> key{};
const bool stored = LoadFactoryFdsk(key.data(), key.size());
std::fill(key.begin(), key.end(), 0);
if (!stored) {
return false;
}
if (info != nullptr) {
*info = LoadFactoryFdskInfo();
}
return true;
}
bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) {
std::array<uint8_t, kFdskSize> key{};
if (!parseHexKey(hex_key, key.data())) {
return false;
}
std::array<uint8_t, kSerialSize> serial{};
std::array<uint8_t, kFdskSize> derived{};
const bool stored = loadKnxSerialNumber(serial.data()) &&
deriveFactoryFdskFromSerial(serial.data(), derived.data()) &&
std::equal(key.begin(), key.end(), derived.begin());
if (stored) {
syncFactoryFdskToNvs(derived.data());
}
std::fill(key.begin(), key.end(), 0);
std::fill(derived.begin(), derived.end(), 0);
if (!stored) {
return false;
}
if (info != nullptr) {
*info = LoadFactoryFdskInfo();
}
return true;
}
bool ResetFactoryFdskCache(FactoryFdskInfo* info) {
clearOpenKnxFdskCache();
const auto loaded = LoadFactoryFdskInfo();
if (info != nullptr) {
*info = loaded;
}
return loaded.available;
}
FactoryCertificatePayload BuildFactoryCertificatePayload() {
FactoryCertificatePayload payload;
const auto info = LoadFactoryFdskInfo();
if (!info.available) {
return payload;
}
payload.available = true;
payload.productIdentity = kProductIdentity;
payload.manufacturerId = hexValue(gateway::knx_internal::kReg1DaliManufacturerId, 4);
payload.applicationNumber = hexValue(
gateway::knx_internal::kReg1DaliApplicationNumber, 2);
payload.applicationVersion = hexValue(
gateway::knx_internal::kReg1DaliApplicationVersion, 2);
payload.serialNumber = info.serialNumber;
payload.fdskLabel = info.label;
payload.fdskQrCode = info.qrCode;
payload.storage = kDevelopmentStorage;
payload.createdAt = "uptime_us:" + std::to_string(esp_timer_get_time());
payload.checksum = fnv1aHex(payload.productIdentity + "|" + payload.manufacturerId + "|" +
payload.applicationNumber + "|" + payload.applicationVersion + "|" +
payload.serialNumber + "|" + payload.fdskLabel + "|" +
payload.fdskQrCode + "|" + payload.createdAt);
return payload;
}
} // namespace gateway::openknx
extern "C" bool knx_platform_get_fdsk(uint8_t* data, size_t len) {
return gateway::openknx::LoadFactoryFdsk(data, len);
}
@@ -24,7 +24,8 @@ struct GatewayCacheConfig {
bool cache_enabled{true};
bool reconciliation_enabled{true};
bool full_state_mirror_enabled{false};
uint32_t flush_interval_ms{5000};
uint32_t flush_interval_ms{10000};
uint32_t refresh_interval_ms{120000};
uint32_t task_stack_size{4096};
UBaseType_t task_priority{3};
GatewayCachePriorityMode default_priority_mode{GatewayCachePriorityMode::kOutsideBusFirst};
@@ -71,6 +72,7 @@ struct GatewayCacheDaliRuntimeStatus {
std::optional<uint8_t> actual_level;
std::optional<uint8_t> scene_id;
bool use_min_level{false};
bool stale{false};
uint32_t revision{0};
bool anyKnown() const {
@@ -144,6 +146,8 @@ class GatewayCache {
std::optional<uint8_t> level);
bool setDaliSettings(uint8_t gateway_id, uint8_t short_address,
std::optional<GatewayCacheDaliSettingsSnapshot> settings);
bool setDaliActualLevel(uint8_t gateway_id, uint8_t short_address,
std::optional<uint8_t> level);
bool clearChannelFlagsIfMatched(uint8_t gateway_id, const GatewayCacheChannelFlags& flags);
void markGroupUpdateNeeded(uint8_t gateway_id, bool needed = true);
void markSceneUpdateNeeded(uint8_t gateway_id, bool needed = true);
@@ -174,6 +178,8 @@ class GatewayCache {
void closeStorageLocked();
bool persistSceneLocked(uint8_t gateway_id, uint8_t scene_id, const SceneEntry& scene);
bool persistGroupLocked(uint8_t gateway_id, uint8_t group_id, const GroupEntry& group);
bool persistDaliAddressStateLocked(uint8_t gateway_id, uint8_t short_address,
const GatewayCacheDaliAddressState& state);
bool commitStorageLocked();
bool shouldTrackUpdateFlagsLocked() const;
uint32_t nextDaliRuntimeRevisionLocked();
@@ -205,6 +211,8 @@ class GatewayCache {
GroupStore& ensureGroupStoreLocked(uint8_t gateway_id);
void loadSceneStoreLocked(uint8_t gateway_id, SceneStore& scenes);
void loadGroupStoreLocked(uint8_t gateway_id, GroupStore& groups);
void loadDaliStateStoreLocked(uint8_t gateway_id,
std::array<GatewayCacheDaliAddressState, 64>& states);
std::string readStringLocked(std::string_view key);
bool writeStringLocked(std::string_view key, std::string_view value);
bool eraseKeyLocked(std::string_view key);
+254 -4
View File
@@ -43,6 +43,18 @@ constexpr uint8_t kDaliCmdDt8StoreDtrAsColorX = 0xE0;
constexpr uint8_t kDaliCmdDt8StoreDtrAsColorY = 0xE1;
constexpr uint8_t kDaliCmdDt8StorePrimaryMin = 0xF0;
constexpr uint8_t kDaliCmdDt8StartAutoCalibration = 0xF6;
constexpr int kDaliStatePayloadVersion = 1;
constexpr uint32_t kDaliStateGroupMaskKnown = 1U << 0;
constexpr uint32_t kDaliStateActualKnown = 1U << 1;
constexpr uint32_t kDaliStateSceneKnown = 1U << 2;
constexpr uint32_t kDaliStateUseMinLevel = 1U << 3;
constexpr uint32_t kDaliStateStatusStale = 1U << 4;
constexpr uint32_t kDaliStatePowerOnKnown = 1U << 5;
constexpr uint32_t kDaliStateSystemFailureKnown = 1U << 6;
constexpr uint32_t kDaliStateMinKnown = 1U << 7;
constexpr uint32_t kDaliStateMaxKnown = 1U << 8;
constexpr uint32_t kDaliStateFadeTimeKnown = 1U << 9;
constexpr uint32_t kDaliStateFadeRateKnown = 1U << 10;
class LockGuard {
public:
@@ -134,6 +146,18 @@ bool ShouldMirrorObservedMutation(GatewayCacheRawFrameOrigin origin,
priority_mode == GatewayCachePriorityMode::kOutsideBusFirst;
}
bool ShouldAlwaysMirrorObservedStatus(uint8_t raw_addr, uint8_t command) {
if (!DecodeDaliTarget(raw_addr).has_value()) {
return false;
}
if ((raw_addr & 0x01) == 0) {
return command <= 254;
}
return command == kDaliCmdOff || command == kDaliCmdRecallMax ||
command == kDaliCmdRecallMin ||
(command >= kDaliCmdGoToSceneMin && command <= kDaliCmdGoToSceneMax);
}
void ClearDaliState(GatewayCacheDaliAddressState& state) {
state.group_mask_known = false;
state.group_mask = 0;
@@ -215,6 +239,139 @@ std::string BuildGroupPayload(const GatewayCache::GroupEntry& group) {
return std::string(payload);
}
uint8_t ByteValue(int value) {
return static_cast<uint8_t>(std::clamp(value, 0, 255));
}
uint16_t WordValue(int value) {
return static_cast<uint16_t>(std::clamp(value, 0, 0xffff));
}
uint16_t SceneKnownMask(const GatewayCacheDaliAddressState& state) {
uint16_t mask = 0;
for (size_t index = 0; index < state.scene_levels.size(); ++index) {
if (state.scene_levels[index].has_value()) {
mask |= static_cast<uint16_t>(1U << index);
}
}
return mask;
}
bool IsDefaultDaliAddressState(const GatewayCacheDaliAddressState& state) {
return !state.group_mask_known && state.group_mask == 0 && SceneKnownMask(state) == 0 &&
!state.settings.anyKnown() && !state.status.anyKnown();
}
uint32_t DaliStateFlags(const GatewayCacheDaliAddressState& state) {
uint32_t flags = 0;
if (state.group_mask_known) {
flags |= kDaliStateGroupMaskKnown;
}
if (state.status.actual_level.has_value()) {
flags |= kDaliStateActualKnown;
}
if (state.status.scene_id.has_value()) {
flags |= kDaliStateSceneKnown;
}
if (state.status.use_min_level) {
flags |= kDaliStateUseMinLevel;
}
if (state.status.stale) {
flags |= kDaliStateStatusStale;
}
if (state.settings.power_on_level.has_value()) {
flags |= kDaliStatePowerOnKnown;
}
if (state.settings.system_failure_level.has_value()) {
flags |= kDaliStateSystemFailureKnown;
}
if (state.settings.min_level.has_value()) {
flags |= kDaliStateMinKnown;
}
if (state.settings.max_level.has_value()) {
flags |= kDaliStateMaxKnown;
}
if (state.settings.fade_time.has_value()) {
flags |= kDaliStateFadeTimeKnown;
}
if (state.settings.fade_rate.has_value()) {
flags |= kDaliStateFadeRateKnown;
}
return flags;
}
std::string BuildDaliStatePayload(const GatewayCacheDaliAddressState& state) {
const uint16_t scene_known_mask = SceneKnownMask(state);
std::string payload = std::to_string(kDaliStatePayloadVersion);
payload += "," + std::to_string(DaliStateFlags(state));
payload += "," + std::to_string(state.status.revision);
payload += "," + std::to_string(state.group_mask);
payload += "," + std::to_string(state.status.actual_level.value_or(0));
payload += "," + std::to_string(state.status.scene_id.value_or(0));
payload += "," + std::to_string(state.settings.power_on_level.value_or(0));
payload += "," + std::to_string(state.settings.system_failure_level.value_or(0));
payload += "," + std::to_string(state.settings.min_level.value_or(0));
payload += "," + std::to_string(state.settings.max_level.value_or(0));
payload += "," + std::to_string(state.settings.fade_time.value_or(0));
payload += "," + std::to_string(state.settings.fade_rate.value_or(0));
payload += "," + std::to_string(scene_known_mask);
for (const auto& scene_level : state.scene_levels) {
payload += "," + std::to_string(scene_level.value_or(255));
}
return payload;
}
void ApplyDaliStatePayload(std::string_view raw, GatewayCacheDaliAddressState& state) {
const auto values = ParseCsv(raw);
if (values.size() < 13 || values[0] != kDaliStatePayloadVersion) {
return;
}
const uint32_t flags = static_cast<uint32_t>(std::max(values[1], 0));
state.group_mask_known = (flags & kDaliStateGroupMaskKnown) != 0;
state.group_mask = state.group_mask_known ? WordValue(values[3]) : 0;
state.status = {};
state.status.revision = static_cast<uint32_t>(std::max(values[2], 0));
state.status.stale = (flags & kDaliStateStatusStale) != 0;
state.status.use_min_level = (flags & kDaliStateUseMinLevel) != 0;
if ((flags & kDaliStateActualKnown) != 0) {
state.status.actual_level = ByteValue(values[4]);
}
if ((flags & kDaliStateSceneKnown) != 0) {
state.status.scene_id = static_cast<uint8_t>(std::min<int>(ByteValue(values[5]), 15));
}
state.settings = {};
if ((flags & kDaliStatePowerOnKnown) != 0) {
state.settings.power_on_level = ByteValue(values[6]);
}
if ((flags & kDaliStateSystemFailureKnown) != 0) {
state.settings.system_failure_level = ByteValue(values[7]);
}
if ((flags & kDaliStateMinKnown) != 0) {
state.settings.min_level = ByteValue(values[8]);
}
if ((flags & kDaliStateMaxKnown) != 0) {
state.settings.max_level = ByteValue(values[9]);
}
if ((flags & kDaliStateFadeTimeKnown) != 0) {
state.settings.fade_time = ByteValue(values[10]);
}
if ((flags & kDaliStateFadeRateKnown) != 0) {
state.settings.fade_rate = ByteValue(values[11]);
}
state.scene_levels.fill(std::nullopt);
const uint16_t scene_known_mask = WordValue(values[12]);
for (uint8_t scene_id = 0; scene_id < state.scene_levels.size(); ++scene_id) {
const size_t value_index = 13 + scene_id;
if ((scene_known_mask & (1U << scene_id)) != 0 && value_index < values.size()) {
state.scene_levels[scene_id] = ByteValue(values[value_index]);
}
}
}
} // namespace
GatewayCache::GatewayCache(GatewayCacheConfig config)
@@ -269,8 +426,11 @@ esp_err_t GatewayCache::start() {
return ESP_ERR_NO_MEM;
}
ESP_LOGI(kTag, "cache started namespace=%s flush_interval_ms=%u reconciliation=%d full_mirror=%d",
ESP_LOGI(kTag,
"cache started namespace=%s flush_interval_ms=%u refresh_interval_ms=%u "
"reconciliation=%d full_mirror=%d",
config_.storage_namespace.c_str(), static_cast<unsigned>(config_.flush_interval_ms),
static_cast<unsigned>(config_.refresh_interval_ms),
config_.reconciliation_enabled, config_.full_state_mirror_enabled);
return ESP_OK;
}
@@ -282,6 +442,10 @@ void GatewayCache::preloadChannel(uint8_t gateway_id) {
}
ensureSceneStoreLocked(gateway_id);
ensureGroupStoreLocked(gateway_id);
auto [it, inserted] = dali_states_.try_emplace(gateway_id);
if (inserted) {
loadDaliStateStoreLocked(gateway_id, it->second);
}
}
GatewayCache::SceneStore GatewayCache::scenes(uint8_t gateway_id) {
@@ -620,6 +784,7 @@ bool GatewayCache::setDaliGroupMask(uint8_t gateway_id, uint8_t short_address,
state.group_mask_known = group_mask.has_value();
state.group_mask = group_mask.value_or(0);
refreshDaliAddressAggregateStatusLocked(gateway_id, state);
dirty_ = true;
return true;
}
@@ -632,6 +797,7 @@ bool GatewayCache::setDaliSceneLevel(uint8_t gateway_id, uint8_t short_address,
auto& state = ensureDaliAddressStateLocked(gateway_id, short_address);
state.scene_levels[scene_id] = level;
dirty_ = true;
return true;
}
@@ -644,6 +810,31 @@ bool GatewayCache::setDaliSettings(uint8_t gateway_id, uint8_t short_address,
auto& state = ensureDaliAddressStateLocked(gateway_id, short_address);
state.settings = settings.value_or(GatewayCacheDaliSettingsSnapshot{});
dirty_ = true;
return true;
}
bool GatewayCache::setDaliActualLevel(uint8_t gateway_id, uint8_t short_address,
std::optional<uint8_t> level) {
LockGuard guard(lock_);
if (short_address >= 64) {
return false;
}
GatewayCacheDaliRuntimeStatus status;
status.actual_level = level;
status.revision = nextDaliRuntimeRevisionLocked();
status.stale = false;
auto& state = ensureDaliAddressStateLocked(gateway_id, short_address);
state.status.scene_id.reset();
state.status.use_min_level = false;
applyDaliRuntimeStatusToAddressLocked(state, status);
if (!level.has_value()) {
state.status.actual_level.reset();
state.status.revision = status.revision;
state.status.stale = false;
}
dirty_ = true;
return true;
}
@@ -712,7 +903,8 @@ bool GatewayCache::observeDaliCommand(uint8_t gateway_id, uint8_t raw_addr, uint
return false;
}
if (ShouldMirrorObservedMutation(origin, priority_mode_)) {
if (ShouldAlwaysMirrorObservedStatus(raw_addr, command) ||
ShouldMirrorObservedMutation(origin, priority_mode_)) {
mirrorDaliCommandLocked(gateway_id, raw_addr, command);
}
@@ -791,12 +983,15 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
GatewayCacheDaliRuntimeStatus status;
status.actual_level = command;
status.revision = nextDaliRuntimeRevisionLocked();
status.stale = false;
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
dirty_ = true;
return true;
}
if (command == kDaliCmdReset) {
clearDaliTargetStateLocked(gateway_id, *target, nextDaliRuntimeRevisionLocked());
dirty_ = true;
return true;
}
@@ -804,7 +999,9 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
GatewayCacheDaliRuntimeStatus status;
status.actual_level = command == kDaliCmdOff ? 0 : 254;
status.revision = nextDaliRuntimeRevisionLocked();
status.stale = false;
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
dirty_ = true;
return true;
}
@@ -812,7 +1009,9 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
GatewayCacheDaliRuntimeStatus status;
status.use_min_level = true;
status.revision = nextDaliRuntimeRevisionLocked();
status.stale = false;
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
dirty_ = true;
return true;
}
@@ -820,7 +1019,9 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
GatewayCacheDaliRuntimeStatus status;
status.scene_id = static_cast<uint8_t>(command - kDaliCmdGoToSceneMin);
status.revision = nextDaliRuntimeRevisionLocked();
status.stale = false;
applyDaliTargetRuntimeStatusLocked(gateway_id, *target, status);
dirty_ = true;
return true;
}
@@ -828,6 +1029,7 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
applyDaliTargetGroupMutationLocked(gateway_id, *target,
static_cast<uint8_t>(command & 0x0F),
command < (kDaliCmdAddToGroupMin + 16));
dirty_ = true;
return true;
}
@@ -836,6 +1038,7 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
applyDaliTargetSceneLevelLocked(gateway_id, *target,
static_cast<uint8_t>(command - kDaliCmdSetSceneMin),
*dtr_state.dtr0);
dirty_ = true;
return true;
}
@@ -843,12 +1046,14 @@ bool GatewayCache::mirrorDaliCommandLocked(uint8_t gateway_id, uint8_t raw_addr,
applyDaliTargetSceneLevelLocked(
gateway_id, *target, static_cast<uint8_t>(command - (kDaliCmdSetSceneMin + 16)),
static_cast<uint8_t>(255U));
dirty_ = true;
return true;
}
if (command >= kDaliCmdStoreDtrAsMaxLevel && command <= kDaliCmdStoreDtrAsFadeRate &&
dtr_state.dtr0.has_value()) {
applyDaliTargetSettingsLocked(gateway_id, *target, command, *dtr_state.dtr0);
dirty_ = true;
return true;
}
@@ -977,6 +1182,7 @@ void GatewayCache::applyDaliRuntimeStatusToAddressLocked(
}
}
state.status.revision = status.revision;
state.status.stale = status.stale;
}
void GatewayCache::applyDaliTargetGroupMutationLocked(uint8_t gateway_id,
@@ -1127,7 +1333,9 @@ void GatewayCache::refreshDaliAddressAggregateStatusLocked(uint8_t gateway_id,
if (const auto broadcast = dali_broadcast_status_.find(gateway_id);
broadcast != dali_broadcast_status_.end()) {
applyDaliRuntimeStatusToAddressLocked(state, broadcast->second);
if (!broadcast->second.stale) {
applyDaliRuntimeStatusToAddressLocked(state, broadcast->second);
}
}
const auto groups = dali_group_status_.find(gateway_id);
@@ -1137,6 +1345,9 @@ void GatewayCache::refreshDaliAddressAggregateStatusLocked(uint8_t gateway_id,
for (uint8_t group_id = 0; group_id < groups->second.size(); ++group_id) {
const uint16_t bit = static_cast<uint16_t>(1U << group_id);
if ((state.group_mask & bit) != 0) {
if (groups->second[group_id].stale) {
continue;
}
applyDaliRuntimeStatusToAddressLocked(state, groups->second[group_id]);
}
}
@@ -1209,6 +1420,14 @@ bool GatewayCache::flushDirty() {
}
}
for (const auto& [gateway_id, states] : dali_states_) {
for (uint8_t short_address = 0; short_address < states.size(); ++short_address) {
if (!persistDaliAddressStateLocked(gateway_id, short_address, states[short_address])) {
return false;
}
}
}
const esp_err_t commit_err = nvs_commit(storage_);
if (commit_err != ESP_OK) {
ESP_LOGE(kTag, "cache commit failed: %s", esp_err_to_name(commit_err));
@@ -1288,6 +1507,18 @@ bool GatewayCache::persistGroupLocked(uint8_t gateway_id, uint8_t group_id,
return commitStorageLocked();
}
bool GatewayCache::persistDaliAddressStateLocked(
uint8_t gateway_id, uint8_t short_address, const GatewayCacheDaliAddressState& state) {
if (short_address >= 64) {
return false;
}
if (!IsDefaultDaliAddressState(state)) {
return writeStringLocked(ShortKey("ds", gateway_id, short_address),
BuildDaliStatePayload(state));
}
return eraseKeyLocked(ShortKey("ds", gateway_id, short_address));
}
bool GatewayCache::commitStorageLocked() {
if (storage_ == 0) {
return false;
@@ -1307,7 +1538,9 @@ bool GatewayCache::shouldTrackUpdateFlagsLocked() const {
GatewayCacheDaliAddressState& GatewayCache::ensureDaliAddressStateLocked(uint8_t gateway_id,
uint8_t short_address) {
auto [it, inserted] = dali_states_.try_emplace(gateway_id);
(void)inserted;
if (inserted) {
loadDaliStateStoreLocked(gateway_id, it->second);
}
return it->second[short_address];
}
@@ -1377,6 +1610,23 @@ void GatewayCache::loadGroupStoreLocked(uint8_t gateway_id, GroupStore& groups)
}
}
void GatewayCache::loadDaliStateStoreLocked(
uint8_t gateway_id, std::array<GatewayCacheDaliAddressState, 64>& states) {
for (uint8_t short_address = 0; short_address < states.size(); ++short_address) {
ClearDaliState(states[short_address]);
const auto raw = readStringLocked(ShortKey("ds", gateway_id, short_address));
if (!raw.empty()) {
ApplyDaliStatePayload(raw, states[short_address]);
if (states[short_address].status.anyKnown()) {
states[short_address].status.stale = true;
}
if (states[short_address].status.revision > dali_runtime_revision_) {
dali_runtime_revision_ = states[short_address].status.revision;
}
}
}
}
std::string GatewayCache::readStringLocked(std::string_view key) {
if (!openStorageLocked()) {
return {};
+1 -1
View File
@@ -1,7 +1,7 @@
idf_component_register(
SRCS "src/gateway_controller.cpp"
INCLUDE_DIRS "include"
REQUIRES dali_domain gateway_runtime gateway_cache freertos log
REQUIRES dali_domain gateway_runtime gateway_cache gateway_bridge freertos log
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -19,6 +19,7 @@ namespace gateway {
class DaliDomainService;
struct DaliRawFrame;
class GatewayBridgeService;
class GatewayRuntime;
struct GatewayControllerConfig {
@@ -32,6 +33,9 @@ struct GatewayControllerConfig {
bool ip_router_supported{true};
bool internal_scene_supported{true};
bool internal_group_supported{true};
bool cache_supported{true};
uint32_t cache_refresh_interval_ms{120000};
uint32_t cache_refresh_idle_ms{100};
};
struct GatewayChannelSnapshot {
@@ -76,6 +80,7 @@ class GatewayController {
void addBleStateSink(BleStateSink sink);
void addWifiStateSink(WifiStateSink sink);
void addGatewayNameSink(GatewayNameSink sink);
void setBridgeService(GatewayBridgeService* bridge_service);
bool setupMode() const;
bool wirelessSetupMode() const;
@@ -100,13 +105,27 @@ class GatewayController {
uint8_t scene_id{0};
};
struct BridgeTransportRequestState {
uint8_t version{0};
uint16_t payload_length{0};
uint8_t total_chunks{0};
std::map<uint8_t, std::vector<uint8_t>> chunks;
};
struct CacheRefreshJob {
TickType_t next_due_tick{0};
uint8_t short_address{0};
};
static void TaskEntry(void* arg);
void taskLoop();
void dispatchCommand(const std::vector<uint8_t>& command);
void scheduleReconciliation(uint8_t gateway_id);
bool hasPendingReconciliation() const;
bool cacheRefreshEnabled() const;
bool runMaintenanceStep();
bool runReconciliationStep(uint8_t gateway_id, ReconciliationJob& job);
bool runCacheRefreshStep();
void reconcileGroupStep(uint8_t gateway_id, uint8_t short_address);
void reconcileSceneStep(uint8_t gateway_id, uint8_t short_address, uint8_t scene_id);
void reconcileSettingsStep(uint8_t gateway_id, uint8_t short_address);
@@ -156,10 +175,15 @@ class GatewayController {
void handleAllocationCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
void handleInternalSceneCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
void handleInternalGroupCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
void handleGatewayCacheCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
void handleBridgeTransportCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
void publishBridgeTransportResponse(uint8_t gateway_id, uint8_t version, uint8_t sequence,
std::string_view response);
GatewayRuntime& runtime_;
DaliDomainService& dali_domain_;
GatewayCache& cache_;
GatewayBridgeService* bridge_service_{nullptr};
GatewayControllerConfig config_;
TaskHandle_t task_handle_{nullptr};
SemaphoreHandle_t maintenance_lock_{nullptr};
@@ -167,7 +191,9 @@ class GatewayController {
std::vector<BleStateSink> ble_state_sinks_;
std::vector<WifiStateSink> wifi_state_sinks_;
std::vector<GatewayNameSink> gateway_name_sinks_;
std::map<uint16_t, BridgeTransportRequestState> bridge_transport_requests_;
std::map<uint8_t, ReconciliationJob> reconciliation_jobs_;
std::map<uint8_t, CacheRefreshJob> cache_refresh_jobs_;
std::atomic<int> maintenance_activity_gateway_{-1};
bool setup_mode_{false};
bool wireless_setup_mode_{false};
@@ -3,6 +3,7 @@
#include "dali_domain.hpp"
#include "esp_log.h"
#include "esp_system.h"
#include "gateway_bridge.hpp"
#include "gateway_runtime.hpp"
#include <algorithm>
@@ -21,6 +22,38 @@ constexpr uint8_t kDaliSceneCount = 16;
constexpr uint8_t kDaliCmdOff = 0x00;
constexpr uint8_t kDaliCmdRecallMax = 0x05;
constexpr TickType_t kMaintenancePollTicks = pdMS_TO_TICKS(20);
constexpr TickType_t kIdleMaintenancePollTicks = pdMS_TO_TICKS(1000);
constexpr uint8_t kBridgeTransportRequestOpcode = 0xB0;
constexpr uint8_t kBridgeTransportResponseOpcode = 0xB1;
constexpr uint8_t kBridgeTransportVersion = 1;
constexpr size_t kBridgeTransportMaxChunkBytes = 120;
constexpr uint8_t kGatewayFeatureCache = 0x40;
constexpr uint8_t kGatewayCacheOpcode = 0x39;
constexpr uint8_t kGatewayCacheProtocolVersion = 1;
constexpr uint8_t kGatewayCacheOpSummary = 0x00;
constexpr uint8_t kGatewayCacheOpShortAddress = 0x01;
constexpr uint8_t kGatewayCacheOpGroup = 0x02;
constexpr uint8_t kGatewayCacheOpBroadcast = 0x03;
constexpr uint8_t kGatewayCacheStatusOk = 0x00;
constexpr uint8_t kGatewayCacheStatusDisabled = 0x01;
constexpr uint8_t kGatewayCacheStatusInvalidArgument = 0x02;
constexpr uint16_t kCacheFlagActualKnown = 1U << 0;
constexpr uint16_t kCacheFlagSceneKnown = 1U << 1;
constexpr uint16_t kCacheFlagUseMinLevel = 1U << 2;
constexpr uint16_t kCacheFlagStatusStale = 1U << 3;
constexpr uint16_t kCacheFlagGroupMaskKnown = 1U << 4;
constexpr uint16_t kCacheFlagPowerOnKnown = 1U << 5;
constexpr uint16_t kCacheFlagSystemFailureKnown = 1U << 6;
constexpr uint16_t kCacheFlagMinKnown = 1U << 7;
constexpr uint16_t kCacheFlagMaxKnown = 1U << 8;
constexpr uint16_t kCacheFlagFadeTimeKnown = 1U << 9;
constexpr uint16_t kCacheFlagFadeRateKnown = 1U << 10;
constexpr const char* kBridgeTransportInvalidFrameResponse =
"{\"statusCode\":400,\"error\":\"invalid bridge transport frame\","
"\"message\":\"invalid bridge transport frame\"}";
constexpr const char* kBridgeTransportUnavailableResponse =
"{\"statusCode\":500,\"error\":\"bridge service is not enabled\","
"\"message\":\"bridge service is not enabled\"}";
class LockGuard {
public:
@@ -73,6 +106,79 @@ void AppendStringBytes(std::vector<uint8_t>& out, std::string_view value) {
}
}
void AppendLe16(std::vector<uint8_t>& out, uint16_t value) {
out.push_back(static_cast<uint8_t>(value & 0xFF));
out.push_back(static_cast<uint8_t>((value >> 8) & 0xFF));
}
void AppendLe32(std::vector<uint8_t>& out, uint32_t value) {
out.push_back(static_cast<uint8_t>(value & 0xFF));
out.push_back(static_cast<uint8_t>((value >> 8) & 0xFF));
out.push_back(static_cast<uint8_t>((value >> 16) & 0xFF));
out.push_back(static_cast<uint8_t>((value >> 24) & 0xFF));
}
uint8_t CacheByte(std::optional<uint8_t> value) {
return value.value_or(0xFF);
}
uint16_t CacheRuntimeFlags(const GatewayCacheDaliRuntimeStatus& status) {
uint16_t flags = 0;
if (status.actual_level.has_value()) {
flags |= kCacheFlagActualKnown;
}
if (status.scene_id.has_value()) {
flags |= kCacheFlagSceneKnown;
}
if (status.use_min_level) {
flags |= kCacheFlagUseMinLevel;
}
if (status.stale) {
flags |= kCacheFlagStatusStale;
}
return flags;
}
uint16_t CacheAddressFlags(const GatewayCacheDaliAddressState& state) {
uint16_t flags = CacheRuntimeFlags(state.status);
if (state.group_mask_known) {
flags |= kCacheFlagGroupMaskKnown;
}
if (state.settings.power_on_level.has_value()) {
flags |= kCacheFlagPowerOnKnown;
}
if (state.settings.system_failure_level.has_value()) {
flags |= kCacheFlagSystemFailureKnown;
}
if (state.settings.min_level.has_value()) {
flags |= kCacheFlagMinKnown;
}
if (state.settings.max_level.has_value()) {
flags |= kCacheFlagMaxKnown;
}
if (state.settings.fade_time.has_value()) {
flags |= kCacheFlagFadeTimeKnown;
}
if (state.settings.fade_rate.has_value()) {
flags |= kCacheFlagFadeRateKnown;
}
return flags;
}
uint16_t CacheSceneKnownMask(const GatewayCacheDaliAddressState& state) {
uint16_t mask = 0;
for (size_t index = 0; index < state.scene_levels.size(); ++index) {
if (state.scene_levels[index].has_value()) {
mask |= static_cast<uint16_t>(1U << index);
}
}
return mask;
}
uint16_t BridgeTransportRequestKey(uint8_t gateway_id, uint8_t sequence) {
return static_cast<uint16_t>((static_cast<uint16_t>(gateway_id) << 8) | sequence);
}
void AppendPaddedName(std::vector<uint8_t>& out, std::string_view name) {
const auto normalized = NormalizeName(name);
out.push_back(static_cast<uint8_t>(normalized.size()));
@@ -185,6 +291,10 @@ void GatewayController::addGatewayNameSink(GatewayNameSink sink) {
}
}
void GatewayController::setBridgeService(GatewayBridgeService* bridge_service) {
bridge_service_ = bridge_service;
}
bool GatewayController::setupMode() const {
return setup_mode_;
}
@@ -256,7 +366,11 @@ void GatewayController::taskLoop() {
worked = runMaintenanceStep();
}
if (!worked) {
ulTaskNotifyTake(pdTRUE, hasPendingReconciliation() ? kMaintenancePollTicks : portMAX_DELAY);
const TickType_t wait_ticks = hasPendingReconciliation()
? kMaintenancePollTicks
: cacheRefreshEnabled() ? kIdleMaintenancePollTicks
: portMAX_DELAY;
ulTaskNotifyTake(pdTRUE, wait_ticks);
}
}
}
@@ -289,42 +403,50 @@ bool GatewayController::hasPendingReconciliation() const {
return !reconciliation_jobs_.empty();
}
bool GatewayController::cacheRefreshEnabled() const {
return config_.cache_supported && cache_.cacheEnabled() &&
config_.cache_refresh_interval_ms > 0;
}
bool GatewayController::runMaintenanceStep() {
if (!cache_.reconciliationEnabled()) {
return false;
}
uint8_t gateway_id = 0;
ReconciliationJob job;
{
LockGuard guard(maintenance_lock_);
if (reconciliation_jobs_.empty()) {
return false;
if (cache_.reconciliationEnabled()) {
bool has_job = false;
uint8_t gateway_id = 0;
ReconciliationJob job;
{
LockGuard guard(maintenance_lock_);
if (!reconciliation_jobs_.empty()) {
const auto it = reconciliation_jobs_.begin();
has_job = true;
gateway_id = it->first;
job = it->second;
}
}
const auto it = reconciliation_jobs_.begin();
gateway_id = it->first;
job = it->second;
}
if (runtime_.shouldYieldMaintenance(gateway_id)) {
return false;
}
if (has_job) {
if (runtime_.shouldYieldMaintenance(gateway_id)) {
return false;
}
const bool keep_job = runReconciliationStep(gateway_id, job);
const bool keep_job = runReconciliationStep(gateway_id, job);
{
LockGuard guard(maintenance_lock_);
auto it = reconciliation_jobs_.find(gateway_id);
if (it == reconciliation_jobs_.end()) {
{
LockGuard guard(maintenance_lock_);
auto it = reconciliation_jobs_.find(gateway_id);
if (it == reconciliation_jobs_.end()) {
return true;
}
if (keep_job) {
it->second = job;
} else {
reconciliation_jobs_.erase(it);
}
}
return true;
}
if (keep_job) {
it->second = job;
} else {
reconciliation_jobs_.erase(it);
}
}
return true;
return runCacheRefreshStep();
}
bool GatewayController::runReconciliationStep(uint8_t gateway_id, ReconciliationJob& job) {
@@ -399,6 +521,49 @@ bool GatewayController::runReconciliationStep(uint8_t gateway_id, Reconciliation
}
}
bool GatewayController::runCacheRefreshStep() {
if (!cacheRefreshEnabled()) {
return false;
}
const TickType_t interval_ticks =
std::max<TickType_t>(1, pdMS_TO_TICKS(config_.cache_refresh_interval_ms));
const auto channels = dali_domain_.channelInfo();
const TickType_t now = xTaskGetTickCount();
for (const auto& channel : channels) {
auto& job = cache_refresh_jobs_[channel.gateway_id];
if (job.next_due_tick == 0) {
job.next_due_tick = now + interval_ticks;
continue;
}
if (now < job.next_due_tick) {
continue;
}
if (runtime_.shouldYieldMaintenance(channel.gateway_id) ||
dali_domain_.isAllocAddr(channel.gateway_id) ||
!dali_domain_.isBusIdle(channel.gateway_id, config_.cache_refresh_idle_ms)) {
continue;
}
maintenance_activity_gateway_.store(channel.gateway_id);
const auto actual_level = dali_domain_.queryActualLevel(channel.gateway_id, job.short_address);
maintenance_activity_gateway_.store(-1);
cache_.setDaliActualLevel(channel.gateway_id, job.short_address, actual_level);
++job.short_address;
if (job.short_address >= kDaliShortAddressCount) {
job.short_address = 0;
job.next_due_tick = xTaskGetTickCount() + interval_ticks;
} else {
job.next_due_tick = xTaskGetTickCount();
}
return true;
}
return false;
}
void GatewayController::reconcileGroupStep(uint8_t gateway_id, uint8_t short_address) {
const auto policy = cache_.priorityMode();
const auto state = cache_.daliAddressState(gateway_id, short_address);
@@ -555,6 +720,9 @@ void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
if (config_.internal_group_supported) {
feature |= 0x20;
}
if (config_.cache_supported) {
feature |= kGatewayFeatureCache;
}
publishPayload(gateway_id, {0x03, gateway_id, feature});
break;
}
@@ -653,12 +821,18 @@ void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
}
}
break;
case kGatewayCacheOpcode:
handleGatewayCacheCommand(gateway_id, command);
break;
case 0xA0:
handleInternalSceneCommand(gateway_id, command);
break;
case 0xA2:
handleInternalGroupCommand(gateway_id, command);
break;
case kBridgeTransportRequestOpcode:
handleBridgeTransportCommand(gateway_id, command);
break;
default:
ESP_LOGW(kTag, "unhandled opcode=0x%02x gateway=%u", opcode, gateway_id);
break;
@@ -714,12 +888,120 @@ void GatewayController::publishPayload(uint8_t, const std::vector<uint8_t>& payl
publishFrame(GatewayRuntime::buildNotificationFrame(payload));
}
void GatewayController::publishBridgeTransportResponse(uint8_t gateway_id, uint8_t version,
uint8_t sequence,
std::string_view response) {
const size_t total_chunks =
std::max<size_t>(1, (response.size() + kBridgeTransportMaxChunkBytes - 1) /
kBridgeTransportMaxChunkBytes);
for (size_t index = 0; index < total_chunks; ++index) {
const size_t start = index * kBridgeTransportMaxChunkBytes;
const size_t chunk_length =
std::min(kBridgeTransportMaxChunkBytes, response.size() - start);
std::vector<uint8_t> payload{
kBridgeTransportResponseOpcode,
gateway_id,
version,
sequence,
static_cast<uint8_t>(total_chunks),
static_cast<uint8_t>(index),
static_cast<uint8_t>(response.size() & 0xFF),
static_cast<uint8_t>((response.size() >> 8) & 0xFF),
static_cast<uint8_t>(chunk_length & 0xFF),
static_cast<uint8_t>((chunk_length >> 8) & 0xFF),
};
payload.reserve(payload.size() + chunk_length);
for (size_t offset = 0; offset < chunk_length; ++offset) {
payload.push_back(static_cast<uint8_t>(response[start + offset]));
}
publishPayload(gateway_id, payload);
}
}
void GatewayController::publishFrame(const std::vector<uint8_t>& frame) {
for (const auto& sink : notification_sinks_) {
sink(frame);
}
}
void GatewayController::handleBridgeTransportCommand(uint8_t gateway_id,
const std::vector<uint8_t>& command) {
const uint8_t version = command.size() > 4 ? command[4] : kBridgeTransportVersion;
const uint8_t sequence = command.size() > 5 ? command[5] : 0;
const uint16_t request_key = BridgeTransportRequestKey(gateway_id, sequence);
if (command.size() < 11) {
bridge_transport_requests_.erase(request_key);
publishBridgeTransportResponse(gateway_id, version, sequence,
kBridgeTransportInvalidFrameResponse);
return;
}
const uint8_t total_chunks = command[6];
const uint8_t chunk_index = command[7];
const uint16_t payload_length =
static_cast<uint16_t>(command[8] | (static_cast<uint16_t>(command[9]) << 8));
if (version != kBridgeTransportVersion || total_chunks == 0 || chunk_index >= total_chunks) {
bridge_transport_requests_.erase(request_key);
publishBridgeTransportResponse(gateway_id, version, sequence,
kBridgeTransportInvalidFrameResponse);
return;
}
auto& state = bridge_transport_requests_[request_key];
if (chunk_index == 0 || state.version != version || state.payload_length != payload_length ||
state.total_chunks != total_chunks) {
state = BridgeTransportRequestState{};
state.version = version;
state.payload_length = payload_length;
state.total_chunks = total_chunks;
}
const size_t payload_start = 10;
const size_t payload_end = command.size() - 1;
if (payload_end < payload_start) {
bridge_transport_requests_.erase(request_key);
publishBridgeTransportResponse(gateway_id, version, sequence,
kBridgeTransportInvalidFrameResponse);
return;
}
if (state.chunks.find(chunk_index) == state.chunks.end()) {
state.chunks[chunk_index] =
std::vector<uint8_t>(command.begin() + payload_start, command.begin() + payload_end);
}
if (state.chunks.size() < total_chunks) {
return;
}
std::vector<uint8_t> request_bytes;
request_bytes.reserve(payload_length);
for (uint8_t index = 0; index < total_chunks; ++index) {
const auto it = state.chunks.find(index);
if (it == state.chunks.end()) {
bridge_transport_requests_.erase(request_key);
publishBridgeTransportResponse(gateway_id, version, sequence,
kBridgeTransportInvalidFrameResponse);
return;
}
request_bytes.insert(request_bytes.end(), it->second.begin(), it->second.end());
}
bridge_transport_requests_.erase(request_key);
if (request_bytes.size() != payload_length) {
publishBridgeTransportResponse(gateway_id, version, sequence,
kBridgeTransportInvalidFrameResponse);
return;
}
const std::string response =
bridge_service_ == nullptr
? std::string(kBridgeTransportUnavailableResponse)
: bridge_service_->handleTransportRequest(
gateway_id,
std::string_view(reinterpret_cast<const char*>(request_bytes.data()),
request_bytes.size()));
publishBridgeTransportResponse(gateway_id, version, sequence, response);
}
void GatewayController::handleDaliRawFrame(const DaliRawFrame& frame) {
if (frame.data.size() != 2 && frame.data.size() != 3) {
return;
@@ -1230,4 +1512,90 @@ void GatewayController::handleInternalGroupCommand(uint8_t gateway_id,
}
}
void GatewayController::handleGatewayCacheCommand(uint8_t gateway_id,
const std::vector<uint8_t>& command) {
const uint8_t op = command.size() > 4 ? command[4] : kGatewayCacheOpSummary;
const uint8_t arg = command.size() > 5 ? command[5] : 0;
const bool enabled = config_.cache_supported && cache_.cacheEnabled();
if (op == kGatewayCacheOpSummary) {
const uint8_t flags = static_cast<uint8_t>((config_.cache_supported ? 0x01 : 0x00) |
(cache_.cacheEnabled() ? 0x02 : 0x00) |
(cacheRefreshEnabled() ? 0x04 : 0x00));
std::vector<uint8_t> payload{kGatewayCacheOpcode,
gateway_id,
op,
kGatewayCacheStatusOk,
kGatewayCacheProtocolVersion,
flags};
AppendLe16(payload, static_cast<uint16_t>(std::min<uint32_t>(
config_.cache_refresh_interval_ms / 1000U, 0xffffU)));
publishPayload(gateway_id, payload);
return;
}
if (!enabled) {
publishPayload(gateway_id, {kGatewayCacheOpcode, gateway_id, op,
kGatewayCacheStatusDisabled, arg});
return;
}
if (op == kGatewayCacheOpShortAddress) {
if (arg >= kDaliShortAddressCount) {
publishPayload(gateway_id, {kGatewayCacheOpcode, gateway_id, op,
kGatewayCacheStatusInvalidArgument, arg});
return;
}
const auto state = cache_.daliAddressState(gateway_id, arg);
std::vector<uint8_t> payload{kGatewayCacheOpcode, gateway_id, op, kGatewayCacheStatusOk, arg};
AppendLe16(payload, CacheAddressFlags(state));
payload.push_back(CacheByte(state.status.actual_level));
payload.push_back(CacheByte(state.status.scene_id));
AppendLe16(payload, state.group_mask_known ? state.group_mask : 0);
payload.push_back(CacheByte(state.settings.power_on_level));
payload.push_back(CacheByte(state.settings.system_failure_level));
payload.push_back(CacheByte(state.settings.min_level));
payload.push_back(CacheByte(state.settings.max_level));
payload.push_back(CacheByte(state.settings.fade_time));
payload.push_back(CacheByte(state.settings.fade_rate));
AppendLe32(payload, state.status.revision);
AppendLe16(payload, CacheSceneKnownMask(state));
for (const auto& level : state.scene_levels) {
payload.push_back(CacheByte(level));
}
publishPayload(gateway_id, payload);
return;
}
if (op == kGatewayCacheOpGroup) {
if (arg >= kDaliSceneCount) {
publishPayload(gateway_id, {kGatewayCacheOpcode, gateway_id, op,
kGatewayCacheStatusInvalidArgument, arg});
return;
}
const auto status = cache_.daliGroupStatus(gateway_id, arg);
std::vector<uint8_t> payload{kGatewayCacheOpcode, gateway_id, op, kGatewayCacheStatusOk, arg};
AppendLe16(payload, CacheRuntimeFlags(status));
payload.push_back(CacheByte(status.actual_level));
payload.push_back(CacheByte(status.scene_id));
AppendLe32(payload, status.revision);
publishPayload(gateway_id, payload);
return;
}
if (op == kGatewayCacheOpBroadcast) {
const auto status = cache_.daliBroadcastStatus(gateway_id);
std::vector<uint8_t> payload{kGatewayCacheOpcode, gateway_id, op, kGatewayCacheStatusOk, 0};
AppendLe16(payload, CacheRuntimeFlags(status));
payload.push_back(CacheByte(status.actual_level));
payload.push_back(CacheByte(status.scene_id));
AppendLe32(payload, status.revision);
publishPayload(gateway_id, payload);
return;
}
publishPayload(gateway_id, {kGatewayCacheOpcode, gateway_id, op,
kGatewayCacheStatusInvalidArgument, arg});
}
} // namespace gateway
+10 -2
View File
@@ -1,7 +1,15 @@
idf_component_register(
SRCS "src/gateway_knx.cpp"
SRCS
"src/gateway_knx.cpp"
"src/gateway_knx_bridge.cpp"
"src/gateway_knx_router_lifecycle.cpp"
"src/gateway_knx_router_openknx.cpp"
"src/gateway_knx_router_packets.cpp"
"src/gateway_knx_router_services.cpp"
"src/ets_device_runtime.cpp"
"src/ets_memory_loader.cpp"
INCLUDE_DIRS "include"
REQUIRES dali_cpp esp_driver_uart 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)
@@ -0,0 +1,132 @@
#pragma once
#include "esp_idf_platform.h"
#include "ets_memory_loader.h"
#include "knx/bau07B0.h"
#include "knx/cemi_frame.h"
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
class GroupObject;
namespace gateway::openknx {
class TpuartUartInterface;
class EtsDeviceRuntime {
public:
using CemiFrameSender = std::function<void(const uint8_t* data, size_t len)>;
using GroupWriteHandler = std::function<void(uint16_t group_address, const uint8_t* data,
size_t len)>;
using GroupObjectWriteHandler = std::function<void(uint16_t group_object_number,
const uint8_t* data, size_t len)>;
using FunctionPropertyHandler = std::function<bool(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response)>;
using FunctionPropertyExtHandler = std::function<bool(uint16_t object_type,
uint8_t object_instance,
uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response)>;
EtsDeviceRuntime(std::string nvs_namespace,
uint16_t fallback_individual_address,
uint16_t tunnel_client_address = 0,
std::unique_ptr<TpuartUartInterface> tp_uart_interface = nullptr);
~EtsDeviceRuntime();
uint16_t individualAddress() const;
uint16_t tunnelClientAddress() const;
bool configured() const;
bool programmingMode() const;
void setProgrammingMode(bool enabled);
void toggleProgrammingMode();
EtsMemorySnapshot snapshot() const;
uint8_t paramByte(uint32_t addr) const;
bool paramBit(uint32_t addr, uint8_t shift) const;
// Accessors for OpenKNX integration (DIB construction, IP parameter object).
DeviceObject& deviceObject();
Platform& platform();
void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler,
FunctionPropertyHandler state_handler);
void setFunctionPropertyExtHandlers(FunctionPropertyExtHandler command_handler,
FunctionPropertyExtHandler state_handler);
void setGroupWriteHandler(GroupWriteHandler handler);
void setGroupObjectWriteHandler(GroupObjectWriteHandler handler);
void setBusFrameSender(CemiFrameSender sender);
void setNetworkInterface(esp_netif_t* netif);
bool hasTpUart() const;
bool enableTpUart(bool enabled = true);
bool tpUartOnline() const;
bool transmitTpFrame(const uint8_t* data, size_t len);
bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender);
bool handleBusFrame(const uint8_t* data, size_t len);
bool emitGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len,
CemiFrameSender sender);
void loop();
private:
static bool HandleOutboundCemiFrame(CemiFrame& frame, void* context);
static void EmitTunnelFrame(CemiFrame& frame, void* context);
static void HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data,
uint8_t data_length, void* context);
static void HandleGroupObjectWrite(GroupObject& ko);
static bool HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length);
static bool HandleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length);
static bool HandleFunctionPropertyExtCommand(uint16_t object_type,
uint8_t object_instance,
uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data,
uint8_t& result_length);
static bool HandleFunctionPropertyExtState(uint16_t object_type,
uint8_t object_instance,
uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data,
uint8_t& result_length);
static uint16_t DefaultTunnelClientAddress(uint16_t individual_address);
static bool DispatchFunctionProperty(FunctionPropertyHandler* handler, uint8_t object_index,
uint8_t property_id, uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length);
static bool DispatchFunctionPropertyExt(FunctionPropertyExtHandler* handler,
uint16_t object_type,
uint8_t object_instance,
uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data,
uint8_t& result_length);
void installGroupObjectCallbacks();
bool shouldConsumeTunnelFrame(CemiFrame& frame) const;
bool shouldConsumeBusFrame(CemiFrame& frame) const;
std::string nvs_namespace_;
std::unique_ptr<TpuartUartInterface> tp_uart_interface_;
EspIdfPlatform platform_;
Bau07B0 device_;
CemiFrameSender sender_;
CemiFrameSender bus_frame_sender_;
GroupWriteHandler group_write_handler_;
GroupObjectWriteHandler group_object_write_handler_;
FunctionPropertyHandler command_handler_;
FunctionPropertyHandler state_handler_;
FunctionPropertyExtHandler command_ext_handler_;
FunctionPropertyExtHandler state_ext_handler_;
bool suppress_group_object_write_callback_{false};
uint16_t group_object_callback_count_{0};
};
} // namespace gateway::openknx
+180 -27
View File
@@ -3,6 +3,8 @@
#include "bridge.hpp"
#include "model_value.hpp"
#include "knx/ip_parameter_object.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
@@ -10,12 +12,14 @@
#include "lwip/sockets.h"
#include <atomic>
#include <array>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>
@@ -23,11 +27,13 @@ namespace gateway {
namespace openknx {
class EtsDeviceRuntime;
class TpuartUartInterface;
}
constexpr uint16_t kGatewayKnxDefaultUdpPort = 3671;
constexpr const char* kGatewayKnxDefaultMulticastAddress = "224.0.23.12";
constexpr uint32_t kGatewayKnxDefaultTpBaudrate = 19200;
constexpr uint32_t kGatewayKnxDefaultTpStartupTimeoutMs = 2000;
struct GatewayKnxTpUartConfig {
int uart_port{1};
@@ -36,7 +42,9 @@ struct GatewayKnxTpUartConfig {
uint32_t baudrate{kGatewayKnxDefaultTpBaudrate};
size_t rx_buffer_size{1024};
size_t tx_buffer_size{1024};
uint32_t startup_timeout_ms{kGatewayKnxDefaultTpStartupTimeoutMs};
uint32_t read_timeout_ms{20};
bool nine_bit_mode{true};
};
enum class GatewayKnxMappingMode : uint8_t {
@@ -59,9 +67,15 @@ struct GatewayKnxConfig {
bool ets_database_enabled{true};
GatewayKnxMappingMode mapping_mode{GatewayKnxMappingMode::kFormula};
uint8_t main_group{0};
uint8_t dali_bus_id{0};
uint16_t udp_port{kGatewayKnxDefaultUdpPort};
std::string multicast_address{kGatewayKnxDefaultMulticastAddress};
uint16_t individual_address{0x1101};
uint16_t ip_interface_individual_address{0xff01};
uint16_t individual_address{0xfffe};
int programming_button_gpio{-1};
bool programming_button_active_low{true};
int programming_led_gpio{-1};
bool programming_led_active_high{true};
std::vector<GatewayKnxEtsAssociation> ets_associations;
GatewayKnxTpUartConfig tp_uart;
};
@@ -72,6 +86,8 @@ enum class GatewayKnxDaliDataType : uint8_t {
kBrightness = 2,
kColorTemperature = 3,
kRgb = 4,
kBrightnessRelative = 5,
kScene = 6,
};
enum class GatewayKnxDaliTargetKind : uint8_t {
@@ -109,8 +125,16 @@ struct GatewayKnxCommissioningBallast {
uint8_t short_address{0xff};
};
struct GatewayKnxReg1ScanOptions {
bool only_new{false};
bool randomize{false};
bool delete_all{false};
bool assign{false};
};
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value);
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config);
bool GatewayKnxConfigUsesTpUart(const GatewayKnxConfig& config);
const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode);
GatewayKnxMappingMode GatewayKnxMappingModeFromString(const std::string& value);
@@ -126,15 +150,19 @@ std::string GatewayKnxGroupAddressString(uint16_t group_address);
class GatewayKnxBridge {
public:
explicit GatewayKnxBridge(DaliBridgeEngine& engine);
~GatewayKnxBridge();
void setConfig(const GatewayKnxConfig& config);
void setRuntimeContext(const openknx::EtsDeviceRuntime* runtime);
const GatewayKnxConfig& config() const;
size_t etsBindingCount() const;
std::vector<GatewayKnxDaliBinding> describeDaliBindings() const;
DaliBridgeResult handleCemiFrame(const uint8_t* data, size_t len);
bool matchesGroupAddress(uint16_t group_address) const;
DaliBridgeResult handleGroupWrite(uint16_t group_address, const uint8_t* data,
size_t len);
DaliBridgeResult handleGroupObjectWrite(uint16_t group_object_number,
const uint8_t* data, size_t len);
bool handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
@@ -147,6 +175,8 @@ class GatewayKnxBridge {
GatewayKnxDaliDataType data_type,
GatewayKnxDaliTarget target,
const uint8_t* data, size_t len);
DaliBridgeResult executeReg1SceneWrite(uint16_t group_address, const uint8_t* data,
size_t len);
DaliBridgeResult executeEtsBindings(uint16_t group_address,
const std::vector<GatewayKnxDaliBinding>& bindings,
const uint8_t* data, size_t len);
@@ -174,10 +204,17 @@ class GatewayKnxBridge {
std::vector<uint8_t>* response);
bool handleReg1FoundEvgsState(const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
static void CommissioningScanTaskEntry(void* arg);
void runCommissioningScanTask();
DaliBridgeEngine& engine_;
GatewayKnxConfig config_;
const openknx::EtsDeviceRuntime* runtime_{nullptr};
std::map<uint16_t, std::vector<GatewayKnxDaliBinding>> ets_bindings_by_group_address_;
SemaphoreHandle_t commissioning_lock_{nullptr};
TaskHandle_t commissioning_scan_task_{nullptr};
std::atomic_bool commissioning_scan_cancel_requested_{false};
GatewayKnxReg1ScanOptions commissioning_scan_options_;
bool commissioning_scan_done_{true};
bool commissioning_assign_done_{true};
std::vector<GatewayKnxCommissioningBallast> commissioning_found_ballasts_;
@@ -185,14 +222,26 @@ class GatewayKnxBridge {
class GatewayKnxTpIpRouter {
public:
using CemiFrameHandler = std::function<DaliBridgeResult(const uint8_t* data, size_t len)>;
using GroupWriteHandler = std::function<DaliBridgeResult(uint16_t group_address,
const uint8_t* data,
size_t len)>;
using GroupObjectWriteHandler = std::function<DaliBridgeResult(uint16_t group_object_number,
const uint8_t* data,
size_t len)>;
GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler,
GatewayKnxTpIpRouter(GatewayKnxBridge& bridge,
std::string openknx_namespace = "openknx");
~GatewayKnxTpIpRouter();
void setConfig(const GatewayKnxConfig& config);
void setCommissioningOnly(bool enabled);
void setGroupWriteHandler(GroupWriteHandler handler);
void setGroupObjectWriteHandler(GroupObjectWriteHandler handler);
const GatewayKnxConfig& config() const;
bool tpUartOnline() const;
bool programmingMode();
esp_err_t setProgrammingMode(bool enabled);
esp_err_t toggleProgrammingMode();
esp_err_t start(uint32_t task_stack_size, UBaseType_t task_priority);
esp_err_t stop();
@@ -201,63 +250,167 @@ class GatewayKnxTpIpRouter {
bool publishDaliStatus(const GatewayKnxDaliTarget& target, uint8_t actual_level);
private:
bool handleFunctionPropertyExtCommand(uint16_t object_type,
uint8_t object_instance,
uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
bool handleFunctionPropertyExtState(uint16_t object_type,
uint8_t object_instance,
uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
static constexpr size_t kMaxTunnelClients = 16;
static constexpr size_t kMaxTcpClients = 4;
struct TcpClient {
int sock{-1};
::sockaddr_in remote{};
std::vector<uint8_t> rx_buffer;
TickType_t last_activity_tick{0};
};
struct TunnelClient {
bool connected{false};
uint8_t channel_id{0};
uint8_t connection_type{0};
uint8_t received_sequence{255};
uint8_t send_sequence{0};
uint8_t last_tunnel_confirmation_sequence{0};
uint16_t individual_address{0};
int tcp_sock{-1};
TickType_t last_activity_tick{0};
::sockaddr_in control_remote{};
::sockaddr_in data_remote{};
std::vector<uint8_t> last_received_cemi;
std::vector<uint8_t> last_tunnel_confirmation_packet;
};
static void TaskEntry(void* arg);
esp_err_t initializeRuntime();
void taskLoop();
void finishTask();
void closeSockets();
bool configureSocket();
void handleTcpAccept();
void handleTcpClient(TcpClient& client);
void closeTcpClient(TcpClient& client);
std::unique_ptr<openknx::TpuartUartInterface> createOpenKnxTpUartInterface();
bool configureTpUart();
bool initializeTpUart();
bool configureProgrammingGpio();
void refreshNetworkInterfaces(bool force_log = false);
void handleUdpDatagram(const uint8_t* data, size_t len, const ::sockaddr_in& remote);
void handleRoutingIndication(const uint8_t* body, size_t len);
void handleTunnellingRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote);
void handleConnectRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote);
void handleConnectionStateRequest(const uint8_t* body, size_t len,
void handleSearchRequest(uint16_t service, const uint8_t* packet_data, size_t len,
const ::sockaddr_in& remote);
void handleDescriptionRequest(const uint8_t* packet_data, size_t len,
const ::sockaddr_in& remote);
void handleRoutingIndication(const uint8_t* packet_data, size_t len);
void handleTunnellingRequest(const uint8_t* packet_data, size_t len, const ::sockaddr_in& remote);
void handleDeviceConfigurationRequest(const uint8_t* packet_data, size_t len,
const ::sockaddr_in& remote);
void handleConnectRequest(const uint8_t* packet_data, size_t len, const ::sockaddr_in& remote);
void handleConnectionStateRequest(const uint8_t* packet_data, size_t len,
const ::sockaddr_in& remote);
void handleDisconnectRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote);
void handleDisconnectRequest(const uint8_t* packet_data, size_t len, const ::sockaddr_in& remote);
void handleSecureService(uint16_t service, const uint8_t* body, size_t len,
const ::sockaddr_in& remote);
void sendTunnellingAck(uint8_t channel_id, uint8_t sequence, uint8_t status,
const ::sockaddr_in& remote);
void sendDeviceConfigurationAck(uint8_t channel_id, uint8_t sequence, uint8_t status,
const ::sockaddr_in& remote);
void sendConnectionHeaderAck(uint16_t service, uint8_t channel_id, uint8_t sequence,
uint8_t status, const ::sockaddr_in& remote);
void sendSecureSessionStatus(uint8_t status, const ::sockaddr_in& remote);
void sendTunnelIndication(const uint8_t* data, size_t len);
void sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data, size_t len);
bool sendCemiFrameToClient(TunnelClient& client, uint16_t service,
const uint8_t* data, size_t len);
void sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
const ::sockaddr_in& remote);
void sendDisconnectResponse(uint8_t channel_id, uint8_t status,
const ::sockaddr_in& remote);
void sendConnectResponse(uint8_t channel_id, uint8_t status,
const ::sockaddr_in& remote);
const ::sockaddr_in& remote, uint8_t connection_type,
uint16_t tunnel_address);
void sendRoutingIndication(const uint8_t* data, size_t len);
bool handleOpenKnxTunnelFrame(const uint8_t* data, size_t len);
bool sendPacket(const std::vector<uint8_t>& packet, const ::sockaddr_in& remote) const;
bool sendPacketToTunnelClient(const TunnelClient& client,
const std::vector<uint8_t>& packet) const;
bool currentTransportAllowsTcpHpai() const;
std::optional<std::array<uint8_t, 8>> localHpaiForRemote(const ::sockaddr_in& remote,
bool tcp = false) const;
// --- OpenKNX-backed DIB construction (uses KnxIpSearchResponse / KnxIpDescriptionResponse) ---
std::vector<uint8_t> buildOpenKnxSearchResponse(const ::sockaddr_in& remote) const;
std::vector<uint8_t> buildOpenKnxDescriptionResponse(const ::sockaddr_in& remote) const;
// --- Hand-rolled DIB builders (fallback when OpenKNX is unavailable) ---
std::vector<uint8_t> buildDeviceInfoDib(const ::sockaddr_in& remote) const;
std::vector<uint8_t> buildSupportedServiceDib() const;
std::vector<uint8_t> buildExtendedDeviceInfoDib() const;
std::vector<uint8_t> buildIpConfigDib(const ::sockaddr_in& remote, bool current) const;
std::vector<uint8_t> buildKnxAddressesDib() const;
std::vector<uint8_t> buildTunnelingInfoDib() const;
TunnelClient* findTunnelClient(uint8_t channel_id);
const TunnelClient* findTunnelClient(uint8_t channel_id) const;
TunnelClient* allocateTunnelClient(const ::sockaddr_in& control_remote,
const ::sockaddr_in& data_remote,
uint8_t connection_type);
void resetTunnelClient(TunnelClient& client);
uint8_t nextTunnelChannelId() const;
uint16_t effectiveTunnelAddressForSlot(size_t slot) const;
void pruneStaleTunnelClients();
bool handleOpenKnxTunnelFrame(const uint8_t* data, size_t len,
TunnelClient* response_client, uint16_t response_service,
const uint8_t* suppress_routing_echo = nullptr,
size_t suppress_routing_echo_len = 0);
bool handleOpenKnxBusFrame(const uint8_t* data, size_t len);
bool transmitOpenKnxTpFrame(const uint8_t* data, size_t len);
void selectOpenKnxNetworkInterface(const ::sockaddr_in& remote);
bool routeOpenKnxGroupWrite(const uint8_t* data, size_t len, const char* context);
bool emitOpenKnxGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len);
bool shouldRouteDaliApplicationFrames() const;
uint8_t advertisedMedium() const;
void syncOpenKnxConfigFromDevice();
uint16_t effectiveIndividualAddress() const;
uint16_t effectiveIpInterfaceIndividualAddress() const;
uint16_t effectiveKnxDeviceIndividualAddress() const;
uint16_t effectiveTunnelAddress() const;
void pollTpUart();
void handleTpUartControlByte(uint8_t byte);
void handleTpTelegram(const uint8_t* data, size_t len);
void forwardCemiToTp(const uint8_t* data, size_t len);
void pollProgrammingButton();
void updateProgrammingLed();
void setProgrammingLed(bool on);
GatewayKnxBridge& bridge_;
CemiFrameHandler handler_;
GroupWriteHandler group_write_handler_;
GroupObjectWriteHandler group_object_write_handler_;
std::string openknx_namespace_;
GatewayKnxConfig config_;
std::unique_ptr<openknx::EtsDeviceRuntime> ets_device_;
TaskHandle_t task_handle_{nullptr};
SemaphoreHandle_t openknx_lock_{nullptr};
SemaphoreHandle_t startup_semaphore_{nullptr};
esp_err_t startup_result_{ESP_OK};
std::atomic_bool stop_requested_{false};
std::atomic_bool started_{false};
int udp_sock_{-1};
int tcp_sock_{-1};
int active_tcp_sock_{-1};
int tp_uart_port_{-1};
uint8_t tunnel_channel_id_{1};
uint8_t expected_tunnel_sequence_{0};
uint8_t tunnel_send_sequence_{0};
bool tunnel_connected_{false};
::sockaddr_in tunnel_remote_{};
std::vector<uint8_t> tp_rx_frame_;
std::vector<uint8_t> tp_last_sent_telegram_;
TickType_t tp_uart_last_byte_tick_{0};
bool tp_uart_extended_frame_{false};
int tp_uart_tx_pin_{-1};
int tp_uart_rx_pin_{-1};
std::vector<uint32_t> multicast_joined_interfaces_;
TickType_t network_refresh_tick_{0};
std::array<TcpClient, kMaxTcpClients> tcp_clients_{};
std::array<TunnelClient, kMaxTunnelClients> tunnel_clients_{};
std::unique_ptr<IpParameterObject> knx_ip_parameters_;
uint8_t last_tunnel_channel_id_{0};
bool tp_uart_online_{false};
bool commissioning_only_{false};
std::atomic_bool openknx_configured_{false};
bool programming_button_last_pressed_{false};
bool programming_led_state_{false};
TickType_t programming_button_last_toggle_tick_{0};
std::string last_error_;
};
@@ -0,0 +1,102 @@
#pragma once
// Internal helpers and product identity shared by gateway_knx component sources.
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "sdkconfig.h"
#include "soc/uart_periph.h"
#include <cstdint>
#include <string>
namespace gateway {
namespace knx_internal {
constexpr const char* kTag = "gateway_knx";
#ifndef CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID
#define CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID 0x00A4
#endif
#ifndef CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER
#define CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER 0x0001
#endif
#ifndef CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION
#define CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION 0x08
#endif
#ifndef CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID
#define CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID 0xA401
#endif
inline constexpr uint16_t kReg1DaliManufacturerId =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID);
inline constexpr uint16_t kReg1DaliHardwareId =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID);
inline constexpr uint16_t kReg1DaliApplicationNumber =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER);
inline constexpr uint8_t kReg1DaliApplicationVersion =
static_cast<uint8_t>(CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION);
inline constexpr uint8_t kReg1DaliHardwareType[6] = {
0x00,
0x00,
static_cast<uint8_t>((kReg1DaliHardwareId >> 8) & 0xff),
static_cast<uint8_t>(kReg1DaliHardwareId & 0xff),
kReg1DaliApplicationVersion,
0x00};
inline constexpr uint8_t kReg1DaliOrderNumber[10] = {
'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0};
inline constexpr uint8_t kReg1DaliProgramVersion[5] = {
static_cast<uint8_t>((kReg1DaliManufacturerId >> 8) & 0xff),
static_cast<uint8_t>(kReg1DaliManufacturerId & 0xff),
static_cast<uint8_t>((kReg1DaliApplicationNumber >> 8) & 0xff),
static_cast<uint8_t>(kReg1DaliApplicationNumber & 0xff),
kReg1DaliApplicationVersion};
// 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
@@ -0,0 +1,617 @@
#include "ets_device_runtime.h"
#include "gateway_knx_internal.h"
#include "esp_log.h"
#include "knx/cemi_server.h"
#include "knx/group_object.h"
#include "knx/secure_application_layer.h"
#include "knx/property.h"
#include "tpuart_uart_interface.h"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <string>
#include <utility>
#include <vector>
namespace gateway::openknx {
namespace {
thread_local EtsDeviceRuntime* active_function_property_runtime = nullptr;
EtsDeviceRuntime* active_group_object_runtime = nullptr;
class ActiveFunctionPropertyRuntimeScope {
public:
explicit ActiveFunctionPropertyRuntimeScope(EtsDeviceRuntime* runtime)
: previous_(active_function_property_runtime) {
active_function_property_runtime = runtime;
}
~ActiveFunctionPropertyRuntimeScope() { active_function_property_runtime = previous_; }
private:
EtsDeviceRuntime* previous_;
};
constexpr uint16_t kInvalidIndividualAddress = 0xffff;
constexpr uint16_t kKnxUnconfiguredBroadcastAddress = 0xffff; // KNX broadcast IA for unconfigured devices
bool IsUsableIndividualAddress(uint16_t address) {
return address != 0 && address != kInvalidIndividualAddress;
}
std::string HexBytesString(const uint8_t* data, size_t length) {
if (data == nullptr || length == 0) {
return {};
}
std::string out;
out.reserve(length * 3);
char buffer[4] = {0};
for (size_t index = 0; index < length; ++index) {
std::snprintf(buffer, sizeof(buffer), "%02X", data[index]);
out += buffer;
if (index + 1 < length) {
out.push_back(' ');
}
}
return out;
}
std::string PrintableOrderNumber(const uint8_t* data, size_t length) {
if (data == nullptr || length == 0) {
return {};
}
std::string out;
out.reserve(length);
for (size_t index = 0; index < length; ++index) {
if (data[index] == 0) {
break;
}
out.push_back(static_cast<char>(data[index]));
}
return out;
}
void LogReg1DaliIdentity(const std::string& nvs_namespace, Bau07B0& device) {
uint8_t program_version[5] = {0};
if (auto* property = device.parameters().property(PID_PROG_VERSION); property != nullptr) {
property->read(program_version);
}
const std::string hardware_type =
HexBytesString(device.deviceObject().hardwareType(), LEN_HARDWARE_TYPE);
const std::string program_version_hex =
HexBytesString(program_version, sizeof(program_version));
const std::string order_number =
PrintableOrderNumber(device.deviceObject().orderNumber(),
sizeof(knx_internal::kReg1DaliOrderNumber));
ESP_LOGI("gateway_knx",
"OpenKNX identity namespace=%s manufacturer=0x%04x mask=0x%04x deviceVersion=0x%04x hardwareType=%s progVersion=%s order=%s",
nvs_namespace.c_str(), device.deviceObject().manufacturerId(),
device.deviceObject().maskVersion(), device.deviceObject().version(),
hardware_type.c_str(), program_version_hex.c_str(), order_number.c_str());
}
void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) {
device.deviceObject().manufacturerId(knx_internal::kReg1DaliManufacturerId);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
device.deviceObject().hardwareType(knx_internal::kReg1DaliHardwareType);
device.deviceObject().orderNumber(knx_internal::kReg1DaliOrderNumber);
device.parameters().property(PID_PROG_VERSION)->write(
knx_internal::kReg1DaliProgramVersion);
}
} // namespace
EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace,
uint16_t fallback_individual_address,
uint16_t tunnel_client_address,
std::unique_ptr<TpuartUartInterface> tp_uart_interface)
: nvs_namespace_(std::move(nvs_namespace)),
tp_uart_interface_(std::move(tp_uart_interface)),
platform_(tp_uart_interface_.get(), nvs_namespace_.c_str()),
device_(platform_) {
platform_.outboundCemiFrameCallback(&EtsDeviceRuntime::HandleOutboundCemiFrame, this);
ApplyReg1DaliIdentity(device_, platform_);
if (IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
}
ESP_LOGI("gateway_knx", "OpenKNX loading memory namespace=%s", nvs_namespace_.c_str());
device_.readMemory();
ApplyReg1DaliIdentity(device_, platform_);
installGroupObjectCallbacks();
if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) &&
IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
}
LogReg1DaliIdentity(nvs_namespace_, device_);
if (auto* server = device_.getCemiServer()) {
server->clientAddress(IsUsableIndividualAddress(tunnel_client_address)
? tunnel_client_address
: DefaultTunnelClientAddress(
device_.deviceObject().individualAddress()));
server->deviceAddressPropertiesTargetClient(false);
server->tunnelFrameCallback(&EtsDeviceRuntime::EmitTunnelFrame, this);
}
device_.functionPropertyCallback(&EtsDeviceRuntime::HandleFunctionPropertyCommand);
device_.functionPropertyStateCallback(&EtsDeviceRuntime::HandleFunctionPropertyState);
device_.functionPropertyExtCallback(&EtsDeviceRuntime::HandleFunctionPropertyExtCommand);
device_.functionPropertyExtStateCallback(&EtsDeviceRuntime::HandleFunctionPropertyExtState);
#ifdef USE_DATASECURE
device_.secureGroupWriteCallback(&EtsDeviceRuntime::HandleSecureGroupWrite, this);
#endif
}
EtsDeviceRuntime::~EtsDeviceRuntime() {
if (tp_uart_interface_ != nullptr) {
device_.enabled(false);
}
platform_.outboundCemiFrameCallback(nullptr, nullptr);
#ifdef USE_DATASECURE
device_.secureGroupWriteCallback(nullptr, nullptr);
#endif
#ifdef SMALL_GROUPOBJECT
if (active_group_object_runtime == this) {
GroupObject::classCallback(GroupObjectUpdatedHandler{});
}
#else
auto& table = device_.groupObjectTable();
for (uint16_t asap = 1; asap <= table.entryCount(); ++asap) {
table.get(asap).callback(GroupObjectUpdatedHandler{});
}
#endif
if (active_group_object_runtime == this) {
active_group_object_runtime = nullptr;
}
device_.functionPropertyCallback(nullptr);
device_.functionPropertyStateCallback(nullptr);
device_.functionPropertyExtCallback(nullptr);
device_.functionPropertyExtStateCallback(nullptr);
if (auto* server = device_.getCemiServer()) {
server->tunnelFrameCallback(nullptr, nullptr);
}
}
uint16_t EtsDeviceRuntime::individualAddress() const {
return const_cast<Bau07B0&>(device_).deviceObject().individualAddress();
}
uint16_t EtsDeviceRuntime::tunnelClientAddress() const {
if (auto* server = const_cast<Bau07B0&>(device_).getCemiServer()) {
return server->clientAddress();
}
return DefaultTunnelClientAddress(individualAddress());
}
bool EtsDeviceRuntime::configured() const { return const_cast<Bau07B0&>(device_).configured(); }
bool EtsDeviceRuntime::programmingMode() const {
return const_cast<Bau07B0&>(device_).deviceObject().progMode();
}
void EtsDeviceRuntime::setProgrammingMode(bool enabled) {
device_.deviceObject().progMode(enabled);
}
void EtsDeviceRuntime::toggleProgrammingMode() { setProgrammingMode(!programmingMode()); }
DeviceObject& EtsDeviceRuntime::deviceObject() { return device_.deviceObject(); }
Platform& EtsDeviceRuntime::platform() { return platform_; }
EtsMemorySnapshot EtsDeviceRuntime::snapshot() const {
EtsMemorySnapshot out;
auto& device = const_cast<Bau07B0&>(device_);
out.configured = device.configured();
out.individual_address = device.deviceObject().individualAddress();
device.forEachEtsAssociation(
[](uint16_t group_address, uint16_t group_object_number, void* context) {
auto* associations = static_cast<std::vector<EtsAssociation>*>(context);
if (associations != nullptr) {
associations->push_back(EtsAssociation{group_address, group_object_number});
}
},
&out.associations);
std::sort(out.associations.begin(), out.associations.end(),
[](const EtsAssociation& lhs, const EtsAssociation& rhs) {
if (lhs.group_address != rhs.group_address) {
return lhs.group_address < rhs.group_address;
}
return lhs.group_object_number < rhs.group_object_number;
});
out.associations.erase(
std::unique(out.associations.begin(), out.associations.end(),
[](const EtsAssociation& lhs, const EtsAssociation& rhs) {
return lhs.group_address == rhs.group_address &&
lhs.group_object_number == rhs.group_object_number;
}),
out.associations.end());
return out;
}
uint8_t EtsDeviceRuntime::paramByte(uint32_t addr) const {
auto& device = const_cast<Bau07B0&>(device_);
if (!device.configured()) {
return 0;
}
return device.parameters().getByte(addr);
}
bool EtsDeviceRuntime::paramBit(uint32_t addr, uint8_t shift) const {
if (shift > 7) {
return false;
}
return ((paramByte(addr) >> (7 - shift)) & 0x01U) != 0;
}
void EtsDeviceRuntime::setFunctionPropertyHandlers(FunctionPropertyHandler command_handler,
FunctionPropertyHandler state_handler) {
command_handler_ = std::move(command_handler);
state_handler_ = std::move(state_handler);
}
void EtsDeviceRuntime::setFunctionPropertyExtHandlers(FunctionPropertyExtHandler command_handler,
FunctionPropertyExtHandler state_handler) {
command_ext_handler_ = std::move(command_handler);
state_ext_handler_ = std::move(state_handler);
}
void EtsDeviceRuntime::setGroupWriteHandler(GroupWriteHandler handler) {
group_write_handler_ = std::move(handler);
}
void EtsDeviceRuntime::setGroupObjectWriteHandler(GroupObjectWriteHandler handler) {
group_object_write_handler_ = std::move(handler);
installGroupObjectCallbacks();
}
void EtsDeviceRuntime::setBusFrameSender(CemiFrameSender sender) {
bus_frame_sender_ = std::move(sender);
}
void EtsDeviceRuntime::setNetworkInterface(esp_netif_t* netif) {
platform_.networkInterface(netif);
}
bool EtsDeviceRuntime::hasTpUart() const { return tp_uart_interface_ != nullptr; }
bool EtsDeviceRuntime::enableTpUart(bool enabled) {
if (tp_uart_interface_ == nullptr) {
return false;
}
device_.enabled(enabled);
loop();
return !enabled || device_.enabled();
}
bool EtsDeviceRuntime::tpUartOnline() const {
return tp_uart_interface_ != nullptr && const_cast<Bau07B0&>(device_).enabled();
}
bool EtsDeviceRuntime::transmitTpFrame(const uint8_t* data, size_t len) {
auto* data_link_layer = device_.getDataLinkLayer();
if (tp_uart_interface_ == nullptr || data_link_layer == nullptr || data == nullptr || len < 2 ||
!data_link_layer->enabled()) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (!frame.valid()) {
return false;
}
return data_link_layer->transmitFrame(frame);
}
bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len,
CemiFrameSender sender) {
auto* server = device_.getCemiServer();
if (server == nullptr || data == nullptr || len < 2) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
const bool consumed = shouldConsumeTunnelFrame(frame);
if (!consumed) {
return false;
}
const bool suppress_group_object_route =
frame.messageCode() == L_data_req && frame.addressType() == GroupAddress &&
frame.apdu().type() == GroupValueWrite;
const bool previous_suppression = suppress_group_object_write_callback_;
if (suppress_group_object_route) {
suppress_group_object_write_callback_ = true;
}
sender_ = std::move(sender);
ActiveFunctionPropertyRuntimeScope callback_scope(this);
server->frameReceived(frame);
loop();
sender_ = nullptr;
suppress_group_object_write_callback_ = previous_suppression;
installGroupObjectCallbacks();
return consumed;
}
bool EtsDeviceRuntime::handleBusFrame(const uint8_t* data, size_t len) {
auto* data_link_layer = device_.getDataLinkLayer();
if (data_link_layer == nullptr || data == nullptr || len < 2) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
const bool consumed = shouldConsumeBusFrame(frame);
if (!consumed) {
return false;
}
ActiveFunctionPropertyRuntimeScope callback_scope(this);
data_link_layer->externalFrameReceived(frame);
loop();
installGroupObjectCallbacks();
return consumed;
}
bool EtsDeviceRuntime::emitGroupValue(uint16_t group_object_number, const uint8_t* data,
size_t len, CemiFrameSender sender) {
if (group_object_number == 0 || data == nullptr || !sender || !device_.configured()) {
return false;
}
auto& table = device_.groupObjectTable();
if (group_object_number > table.entryCount()) {
return false;
}
auto& group_object = table.get(group_object_number);
if (len != group_object.valueSize() || group_object.valueRef() == nullptr) {
return false;
}
if (group_object.sizeInTelegram() == 0) {
group_object.valueRef()[0] = data[0] & 0x01;
} else {
std::copy_n(data, len, group_object.valueRef());
}
const bool previous_suppression = suppress_group_object_write_callback_;
suppress_group_object_write_callback_ = true;
sender_ = std::move(sender);
group_object.objectWritten();
loop();
sender_ = nullptr;
suppress_group_object_write_callback_ = previous_suppression;
return true;
}
void EtsDeviceRuntime::loop() {
ActiveFunctionPropertyRuntimeScope callback_scope(this);
device_.loop();
}
bool EtsDeviceRuntime::HandleOutboundCemiFrame(CemiFrame& frame, void* context) {
auto* self = static_cast<EtsDeviceRuntime*>(context);
if (self == nullptr || !self->sender_) {
return false;
}
self->sender_(frame.data(), frame.dataLength());
return true;
}
void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) {
auto* self = static_cast<EtsDeviceRuntime*>(context);
if (self == nullptr) {
return;
}
if (self->sender_) {
self->sender_(frame.data(), frame.dataLength());
return;
}
if (self->bus_frame_sender_) {
self->bus_frame_sender_(frame.data(), frame.dataLength());
}
}
void EtsDeviceRuntime::HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data,
uint8_t data_length, void* context) {
auto* self = static_cast<EtsDeviceRuntime*>(context);
if (self == nullptr) {
return;
}
if (self->group_object_write_handler_) {
return;
}
if (self->group_write_handler_) {
self->group_write_handler_(group_address, data, data_length);
}
}
void EtsDeviceRuntime::HandleGroupObjectWrite(GroupObject& ko) {
auto* self = active_group_object_runtime;
if (self == nullptr || self->suppress_group_object_write_callback_ ||
!self->group_object_write_handler_) {
return;
}
const size_t value_size = ko.valueSize();
const uint8_t* value = ko.valueRef();
if (value == nullptr || value_size == 0) {
ESP_LOGW("gateway_knx", "OpenKNX group-object callback ignored namespace=%s ko=%u len=%u",
self->nvs_namespace_.c_str(), static_cast<unsigned>(ko.asap()),
static_cast<unsigned>(value_size));
return;
}
const std::string value_hex = HexBytesString(value, value_size);
ESP_LOGI("gateway_knx", "OpenKNX group-object callback namespace=%s ko=%u len=%u value=%s",
self->nvs_namespace_.c_str(), static_cast<unsigned>(ko.asap()),
static_cast<unsigned>(value_size), value_hex.c_str());
self->group_object_write_handler_(ko.asap(), value, value_size);
}
bool EtsDeviceRuntime::HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data,
uint8_t& result_length) {
if (active_function_property_runtime == nullptr) {
return false;
}
return DispatchFunctionProperty(&active_function_property_runtime->command_handler_, object_index,
property_id, length, data, result_data, result_length);
}
bool EtsDeviceRuntime::HandleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data,
uint8_t& result_length) {
if (active_function_property_runtime == nullptr) {
return false;
}
return DispatchFunctionProperty(&active_function_property_runtime->state_handler_, object_index,
property_id, length, data, result_data, result_length);
}
bool EtsDeviceRuntime::HandleFunctionPropertyExtCommand(uint16_t object_type,
uint8_t object_instance,
uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data,
uint8_t& result_length) {
if (active_function_property_runtime == nullptr) {
return false;
}
return DispatchFunctionPropertyExt(&active_function_property_runtime->command_ext_handler_,
object_type, object_instance, property_id, length, data,
result_data, result_length);
}
bool EtsDeviceRuntime::HandleFunctionPropertyExtState(uint16_t object_type,
uint8_t object_instance,
uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data,
uint8_t& result_length) {
if (active_function_property_runtime == nullptr) {
return false;
}
return DispatchFunctionPropertyExt(&active_function_property_runtime->state_ext_handler_,
object_type, object_instance, property_id, length, data,
result_data, result_length);
}
bool EtsDeviceRuntime::DispatchFunctionProperty(FunctionPropertyHandler* handler,
uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length) {
if (handler == nullptr || !*handler || result_data == nullptr) {
return false;
}
std::vector<uint8_t> response;
if (!(*handler)(object_index, property_id, data, length, &response)) {
return false;
}
result_length = static_cast<uint8_t>(std::min<size_t>(response.size(), result_length));
if (result_length > 0) {
std::copy_n(response.begin(), result_length, result_data);
}
return true;
}
bool EtsDeviceRuntime::DispatchFunctionPropertyExt(FunctionPropertyExtHandler* handler,
uint16_t object_type,
uint8_t object_instance,
uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data,
uint8_t& result_length) {
if (handler == nullptr || !*handler || result_data == nullptr) {
return false;
}
std::vector<uint8_t> response;
if (!(*handler)(object_type, object_instance, property_id, data, length, &response)) {
return false;
}
result_length = static_cast<uint8_t>(std::min<size_t>(response.size(), result_length));
if (result_length > 0) {
std::copy_n(response.begin(), result_length, result_data);
}
return true;
}
void EtsDeviceRuntime::installGroupObjectCallbacks() {
active_group_object_runtime = this;
auto& table = device_.groupObjectTable();
const uint16_t count = table.entryCount();
#ifdef SMALL_GROUPOBJECT
GroupObject::classCallback(&EtsDeviceRuntime::HandleGroupObjectWrite);
#else
for (uint16_t asap = 1; asap <= count; ++asap) {
table.get(asap).callback(&EtsDeviceRuntime::HandleGroupObjectWrite);
}
#endif
if (count != group_object_callback_count_) {
ESP_LOGI("gateway_knx", "OpenKNX group-object callbacks namespace=%s count=%u",
nvs_namespace_.c_str(), static_cast<unsigned>(count));
group_object_callback_count_ = count;
}
}
uint16_t EtsDeviceRuntime::DefaultTunnelClientAddress(uint16_t individual_address) {
if (!IsUsableIndividualAddress(individual_address)) {
return 0x1101;
}
const uint16_t line_base = individual_address & 0xff00;
uint16_t device = static_cast<uint16_t>((individual_address & 0x00ff) + 1);
if (device == 0 || device > 0xff) {
device = 1;
}
return static_cast<uint16_t>(line_base | device);
}
bool EtsDeviceRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const {
switch (frame.messageCode()) {
case M_PropRead_req:
case M_PropWrite_req:
case M_Reset_req:
case M_FuncPropCommand_req:
case M_FuncPropStateRead_req:
return true;
case L_data_req: {
// In commissioning / programming mode ETS may address the device via its
// individual address, the cEMI-client tunnel address (device+1), or the
// unconfigured broadcast address 0xFFFF. Consume only those; let all
// other individual-addressed frames (bus-scan, DeviceDescriptorRead, …)
// pass through to the physical TP-UART so real KNX devices on the bus
// can reply.
const uint16_t dest = frame.destinationAddress();
const uint16_t own_address = individualAddress();
const uint16_t client_address = tunnelClientAddress();
const bool commissioning = !const_cast<Bau07B0&>(device_).configured() || programmingMode();
// KNX Data Secure setup uses tool-access sync frames on group-address 0.
// Consume those locally so the secure layer can emit its response back on
// the active tunnel instead of forwarding the request to TP-UART only.
if (frame.addressType() == GroupAddress && dest == 0x0000 &&
frame.apdu().type() == SecureService) {
return true;
}
if (frame.addressType() == IndividualAddress) {
if (dest == own_address || dest == client_address ||
(commissioning && dest == kKnxUnconfiguredBroadcastAddress)) {
return true;
}
}
return false;
}
default:
return false;
}
}
bool EtsDeviceRuntime::shouldConsumeBusFrame(CemiFrame& frame) const {
#ifdef USE_DATASECURE
return frame.messageCode() == L_data_ind && frame.addressType() == GroupAddress &&
frame.apdu().type() == SecureService;
#else
return false;
#endif
}
} // namespace gateway::openknx
@@ -1,6 +1,7 @@
#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 "gateway_knx_internal.h"
#include "knx/bau07B0.h"
#include "knx/property.h"
@@ -28,18 +29,13 @@ bool IsErasedMemory(const uint8_t* data, size_t size) {
return std::all_of(data, data + size, [](uint8_t value) { return value == 0xff; });
}
constexpr uint16_t kReg1DaliManufacturerId = 0x00a4;
constexpr uint8_t kReg1DaliApplicationNumber = 0x01;
constexpr uint8_t kReg1DaliApplicationVersion = 0x05;
constexpr uint8_t kReg1DaliOrderNumber[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0};
void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) {
device.deviceObject().manufacturerId(kReg1DaliManufacturerId);
device.deviceObject().manufacturerId(knx_internal::kReg1DaliManufacturerId);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
device.deviceObject().orderNumber(kReg1DaliOrderNumber);
const uint8_t program_version[5] = {0x00, 0xa4, 0x00, kReg1DaliApplicationNumber,
kReg1DaliApplicationVersion};
device.parameters().property(PID_PROG_VERSION)->write(program_version);
device.deviceObject().hardwareType(knx_internal::kReg1DaliHardwareType);
device.deviceObject().orderNumber(knx_internal::kReg1DaliOrderNumber);
device.parameters().property(PID_PROG_VERSION)->write(
knx_internal::kReg1DaliProgramVersion);
}
} // namespace
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,798 @@
#include "gateway_knx_private.hpp"
namespace gateway {
GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge,
std::string openknx_namespace)
: bridge_(bridge),
openknx_namespace_(std::move(openknx_namespace)) {
openknx_lock_ = xSemaphoreCreateMutex();
startup_semaphore_ = xSemaphoreCreateBinary();
}
GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() {
stop();
if (startup_semaphore_ != nullptr) {
vSemaphoreDelete(startup_semaphore_);
startup_semaphore_ = nullptr;
}
if (openknx_lock_ != nullptr) {
vSemaphoreDelete(openknx_lock_);
openknx_lock_ = nullptr;
}
}
void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; }
void GatewayKnxTpIpRouter::setCommissioningOnly(bool enabled) {
commissioning_only_ = enabled;
}
void GatewayKnxTpIpRouter::setGroupWriteHandler(GroupWriteHandler handler) {
group_write_handler_ = std::move(handler);
}
void GatewayKnxTpIpRouter::setGroupObjectWriteHandler(GroupObjectWriteHandler handler) {
group_object_write_handler_ = std::move(handler);
}
const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; }
bool GatewayKnxTpIpRouter::tpUartOnline() const { return tp_uart_online_; }
bool GatewayKnxTpIpRouter::programmingMode() {
if (openknx_lock_ == nullptr) {
return false;
}
SemaphoreGuard guard(openknx_lock_);
return ets_device_ != nullptr && ets_device_->programmingMode();
}
esp_err_t GatewayKnxTpIpRouter::setProgrammingMode(bool enabled) {
if (openknx_lock_ == nullptr) {
last_error_ = "KNX runtime lock is unavailable";
return ESP_ERR_INVALID_STATE;
}
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
last_error_ = "KNX OpenKNX runtime is unavailable";
return ESP_ERR_INVALID_STATE;
}
ets_device_->setProgrammingMode(enabled);
setProgrammingLed(enabled);
ESP_LOGI(kTag, "KNX programming mode %s namespace=%s",
enabled ? "enabled" : "disabled", openknx_namespace_.c_str());
return ESP_OK;
}
esp_err_t GatewayKnxTpIpRouter::toggleProgrammingMode() {
return setProgrammingMode(!programmingMode());
}
esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) {
if (started_ || task_handle_ != nullptr) {
return ESP_OK;
}
if (openknx_lock_ == nullptr || startup_semaphore_ == nullptr) {
last_error_ = "failed to allocate KNX runtime synchronization primitives";
return ESP_ERR_NO_MEM;
}
if (!config_.ip_router_enabled) {
last_error_ = "KNXnet/IP router is disabled in config";
return ESP_ERR_NOT_SUPPORTED;
}
stop_requested_ = false;
last_error_.clear();
int log_tp_uart_tx_pin = -1;
int log_tp_uart_rx_pin = -1;
if (config_.tp_uart.uart_port >= 0 && config_.tp_uart.uart_port < SOC_UART_NUM) {
const uart_port_t log_uart_port = static_cast<uart_port_t>(config_.tp_uart.uart_port);
ResolveUartIoPin(log_uart_port, config_.tp_uart.tx_pin, SOC_UART_TX_PIN_IDX,
&log_tp_uart_tx_pin);
ResolveUartIoPin(log_uart_port, config_.tp_uart.rx_pin, SOC_UART_RX_PIN_IDX,
&log_tp_uart_rx_pin);
}
ESP_LOGI(kTag,
"starting KNXnet/IP router namespace=%s udp=%u tunnel=%d multicast=%d group=%s "
"tpUart=%d tx=%s rx=%s nineBit=%d commissioningOnly=%d",
openknx_namespace_.c_str(), static_cast<unsigned>(config_.udp_port),
config_.tunnel_enabled, config_.multicast_enabled,
config_.multicast_address.c_str(), config_.tp_uart.uart_port,
UartPinDescription(config_.tp_uart.tx_pin, log_tp_uart_tx_pin).c_str(),
UartPinDescription(config_.tp_uart.rx_pin, log_tp_uart_rx_pin).c_str(),
config_.tp_uart.nine_bit_mode, commissioning_only_);
if (!configureSocket()) {
return ESP_FAIL;
}
while (xSemaphoreTake(startup_semaphore_, 0) == pdTRUE) {
}
startup_result_ = ESP_ERR_TIMEOUT;
const BaseType_t created = xTaskCreate(&GatewayKnxTpIpRouter::TaskEntry, "gw_knx_ip",
task_stack_size, this, task_priority, &task_handle_);
if (created != pdPASS) {
task_handle_ = nullptr;
closeSockets();
return ESP_ERR_NO_MEM;
}
if (xSemaphoreTake(startup_semaphore_, pdMS_TO_TICKS(10000)) != pdTRUE) {
last_error_ = "timed out starting KNXnet/IP OpenKNX runtime";
stop_requested_ = true;
closeSockets();
return ESP_ERR_TIMEOUT;
}
return startup_result_;
}
esp_err_t GatewayKnxTpIpRouter::stop() {
stop_requested_ = true;
closeSockets();
const TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
for (int attempt = 0; task_handle_ != nullptr && task_handle_ != current_task && attempt < 50;
++attempt) {
vTaskDelay(pdMS_TO_TICKS(10));
}
return ESP_OK;
}
bool GatewayKnxTpIpRouter::started() const { return started_; }
const std::string& GatewayKnxTpIpRouter::lastError() const { return last_error_; }
bool GatewayKnxTpIpRouter::publishDaliStatus(const GatewayKnxDaliTarget& target,
uint8_t actual_level) {
if (!started_ || !config_.ip_router_enabled || !shouldRouteDaliApplicationFrames()) {
return false;
}
uint16_t switch_object = 0;
uint16_t dimm_object = 0;
if (target.kind == GatewayKnxDaliTargetKind::kShortAddress) {
if (target.address < 0 || target.address > 63) {
return false;
}
const uint16_t base = kGwReg1AdrKoOffset +
kGwReg1AdrKoBlockSize * static_cast<uint16_t>(target.address);
switch_object = base + kGwReg1KoSwitchState;
dimm_object = base + kGwReg1KoDimmState;
} else if (target.kind == GatewayKnxDaliTargetKind::kGroup) {
if (target.address < 0 || target.address > 15) {
return false;
}
const uint16_t base = kGwReg1GrpKoOffset +
kGwReg1GrpKoBlockSize * static_cast<uint16_t>(target.address);
switch_object = base + kGwReg1KoSwitchState;
dimm_object = base + kGwReg1KoDimmState;
} else if (target.kind == GatewayKnxDaliTargetKind::kBroadcast) {
switch_object = kGwReg1AppKoBroadcastSwitch;
dimm_object = kGwReg1AppKoBroadcastDimm;
} else {
return false;
}
const uint8_t switch_value = actual_level > 0 ? 1 : 0;
const uint8_t dimm_value = DaliArcLevelToDpt5(actual_level);
bool emitted = emitOpenKnxGroupValue(switch_object, &switch_value, 1);
emitted = emitOpenKnxGroupValue(dimm_object, &dimm_value, 1) || emitted;
return emitted;
}
void GatewayKnxTpIpRouter::TaskEntry(void* arg) {
static_cast<GatewayKnxTpIpRouter*>(arg)->taskLoop();
}
esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
{
SemaphoreGuard guard(openknx_lock_);
auto tp_uart_interface = createOpenKnxTpUartInterface();
if (GatewayKnxConfigUsesTpUart(config_) && tp_uart_interface == nullptr && !last_error_.empty()) {
return ESP_FAIL;
}
ets_device_ = std::make_unique<openknx::EtsDeviceRuntime>(openknx_namespace_,
config_.individual_address,
effectiveTunnelAddress(),
std::move(tp_uart_interface));
bridge_.setRuntimeContext(ets_device_.get());
knx_ip_parameters_ = std::make_unique<IpParameterObject>(
ets_device_->deviceObject(), ets_device_->platform());
openknx_configured_.store(ets_device_->configured());
ESP_LOGI(kTag,
"OpenKNX runtime namespace=%s configured=%d ipInterface=0x%04x "
"device=0x%04x tunnelClient=0x%04x commissioningOnly=%d",
openknx_namespace_.c_str(), ets_device_->configured(),
effectiveIpInterfaceIndividualAddress(), ets_device_->individualAddress(),
ets_device_->tunnelClientAddress(), commissioning_only_);
ets_device_->setFunctionPropertyHandlers(
[this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (!shouldRouteDaliApplicationFrames()) {
return false;
}
return bridge_.handleFunctionPropertyCommand(object_index, property_id, data, len,
response);
},
[this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
if (!shouldRouteDaliApplicationFrames()) {
return false;
}
return bridge_.handleFunctionPropertyState(object_index, property_id, data, len,
response);
});
ets_device_->setFunctionPropertyExtHandlers(
[this](uint16_t object_type, uint8_t object_instance, uint8_t property_id,
const uint8_t* data, size_t len, std::vector<uint8_t>* response) {
return handleFunctionPropertyExtCommand(object_type, object_instance, property_id,
data, len, response);
},
[this](uint16_t object_type, uint8_t object_instance, uint8_t property_id,
const uint8_t* data, size_t len, std::vector<uint8_t>* response) {
return handleFunctionPropertyExtState(object_type, object_instance, property_id,
data, len, response);
});
ets_device_->setGroupWriteHandler(
[this](uint16_t group_address, const uint8_t* data, size_t len) {
if (!shouldRouteDaliApplicationFrames()) {
return;
}
const DaliBridgeResult result = group_write_handler_
? group_write_handler_(group_address, data, len)
: bridge_.handleGroupWrite(group_address, data, len);
if (!result.ok && !result.error.empty()) {
ESP_LOGD(kTag, "secure KNX group write not routed to DALI: %s", result.error.c_str());
}
});
ets_device_->setGroupObjectWriteHandler(
[this](uint16_t group_object_number, const uint8_t* data, size_t len) {
if (!shouldRouteDaliApplicationFrames()) {
return IgnoredResult(
GwReg1GroupAddressForObject(config_.main_group, group_object_number),
group_object_number,
"routing blocked by commissioning-only state");
}
const DaliBridgeResult result = group_object_write_handler_
? group_object_write_handler_(group_object_number,
data, len)
: bridge_.handleGroupObjectWrite(group_object_number,
data, len);
const bool ignored = getObjectBool(result.metadata, "ignored").value_or(false);
if (ignored) {
const auto reason = getObjectString(result.metadata, "reason").value_or("ignored");
ESP_LOGW(kTag, "OpenKNX group object %u accepted by ETS but ignored: %s",
static_cast<unsigned>(group_object_number), reason.c_str());
} else if (!result.ok && !result.error.empty()) {
ESP_LOGW(kTag, "OpenKNX group object %u not routed to DALI: %s",
static_cast<unsigned>(group_object_number), result.error.c_str());
}
return result;
});
ets_device_->setBusFrameSender([this](const uint8_t* data, size_t len) {
sendTunnelIndication(data, len);
sendRoutingIndication(data, len);
});
syncOpenKnxConfigFromDevice();
}
if (!configureTpUart()) {
last_error_ = last_error_.empty() ? "failed to configure KNX TP-UART" : last_error_;
return ESP_FAIL;
}
if (!configureProgrammingGpio()) {
last_error_ = last_error_.empty() ? "failed to configure KNX programming GPIO" : last_error_;
return ESP_FAIL;
}
return ESP_OK;
}
void GatewayKnxTpIpRouter::taskLoop() {
startup_result_ = initializeRuntime();
if (startup_result_ == ESP_OK) {
started_ = true;
}
if (startup_semaphore_ != nullptr) {
xSemaphoreGive(startup_semaphore_);
}
if (startup_result_ != ESP_OK || stop_requested_) {
finishTask();
return;
}
std::array<uint8_t, 768> buffer{};
auto run_maintenance = [this]() {
{
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ != nullptr) {
pollProgrammingButton();
ets_device_->loop();
tp_uart_online_ = ets_device_->tpUartOnline();
updateProgrammingLed();
}
}
};
while (!stop_requested_) {
const TickType_t now = xTaskGetTickCount();
if (network_refresh_tick_ == 0 ||
now - network_refresh_tick_ >= pdMS_TO_TICKS(1000)) {
refreshNetworkInterfaces(false);
pruneStaleTunnelClients();
network_refresh_tick_ = now;
}
fd_set read_fds;
FD_ZERO(&read_fds);
int max_fd = -1;
if (udp_sock_ >= 0) {
FD_SET(udp_sock_, &read_fds);
max_fd = std::max(max_fd, udp_sock_);
}
if (tcp_sock_ >= 0) {
FD_SET(tcp_sock_, &read_fds);
max_fd = std::max(max_fd, tcp_sock_);
}
for (const auto& client : tcp_clients_) {
if (client.sock >= 0) {
FD_SET(client.sock, &read_fds);
max_fd = std::max(max_fd, client.sock);
}
}
timeval timeout{};
timeout.tv_sec = 0;
timeout.tv_usec = 20000;
const int selected = max_fd >= 0 ? select(max_fd + 1, &read_fds, nullptr, nullptr, &timeout)
: 0;
if (selected < 0) {
ESP_LOGW(kTag, "KNXnet/IP socket select failed: errno=%d (%s)", errno,
std::strerror(errno));
run_maintenance();
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
if (selected == 0) {
run_maintenance();
continue;
}
if (tcp_sock_ >= 0 && FD_ISSET(tcp_sock_, &read_fds)) {
handleTcpAccept();
}
for (auto& client : tcp_clients_) {
if (client.sock >= 0 && FD_ISSET(client.sock, &read_fds)) {
handleTcpClient(client);
}
}
sockaddr_in remote{};
socklen_t remote_len = sizeof(remote);
if (udp_sock_ >= 0 && FD_ISSET(udp_sock_, &read_fds)) {
const int received = recvfrom(udp_sock_, buffer.data(), buffer.size(), 0,
reinterpret_cast<sockaddr*>(&remote), &remote_len);
if (received > 0) {
handleUdpDatagram(buffer.data(), static_cast<size_t>(received), remote);
}
}
run_maintenance();
}
finishTask();
}
void GatewayKnxTpIpRouter::finishTask() {
closeSockets();
{
SemaphoreGuard guard(openknx_lock_);
setProgrammingLed(false);
knx_ip_parameters_.reset();
bridge_.setRuntimeContext(nullptr);
ets_device_.reset();
openknx_configured_.store(false);
}
started_ = false;
task_handle_ = nullptr;
vTaskDelete(nullptr);
}
void GatewayKnxTpIpRouter::pollProgrammingButton() {
if (config_.programming_button_gpio < 0 || ets_device_ == nullptr) {
return;
}
const int level = gpio_get_level(static_cast<gpio_num_t>(config_.programming_button_gpio));
const bool pressed = config_.programming_button_active_low ? level == 0 : level != 0;
const TickType_t now = xTaskGetTickCount();
if (pressed && !programming_button_last_pressed_ &&
now - programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) {
ets_device_->toggleProgrammingMode();
ESP_LOGI(kTag, "KNX programming mode %s namespace=%s",
ets_device_->programmingMode() ? "enabled" : "disabled",
openknx_namespace_.c_str());
programming_button_last_toggle_tick_ = now;
}
programming_button_last_pressed_ = pressed;
}
void GatewayKnxTpIpRouter::updateProgrammingLed() {
if (config_.programming_led_gpio < 0 || ets_device_ == nullptr) {
return;
}
const bool programming_mode = ets_device_->programmingMode();
if (programming_mode == programming_led_state_) {
return;
}
setProgrammingLed(programming_mode);
}
void GatewayKnxTpIpRouter::setProgrammingLed(bool on) {
if (config_.programming_led_gpio < 0) {
programming_led_state_ = on;
return;
}
const bool level = config_.programming_led_active_high ? on : !on;
gpio_set_level(static_cast<gpio_num_t>(config_.programming_led_gpio), level ? 1 : 0);
programming_led_state_ = on;
}
void GatewayKnxTpIpRouter::closeSockets() {
if (udp_sock_ >= 0) {
shutdown(udp_sock_, SHUT_RDWR);
close(udp_sock_);
udp_sock_ = -1;
}
if (tcp_sock_ >= 0) {
shutdown(tcp_sock_, SHUT_RDWR);
close(tcp_sock_);
tcp_sock_ = -1;
}
for (auto& client : tcp_clients_) {
closeTcpClient(client);
}
active_tcp_sock_ = -1;
multicast_joined_interfaces_.clear();
network_refresh_tick_ = 0;
for (auto& client : tunnel_clients_) {
resetTunnelClient(client);
}
last_tunnel_channel_id_ = 0;
}
bool GatewayKnxTpIpRouter::configureSocket() {
udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (udp_sock_ < 0) {
last_error_ = ErrnoDetail("failed to create KNXnet/IP UDP socket", errno);
ESP_LOGE(kTag, "%s", last_error_.c_str());
return false;
}
int broadcast = 1;
if (setsockopt(udp_sock_, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) {
ESP_LOGW(kTag, "failed to enable broadcast for KNX UDP port %u: errno=%d (%s)",
static_cast<unsigned>(config_.udp_port), errno, std::strerror(errno));
}
sockaddr_in bind_addr{};
bind_addr.sin_family = AF_INET;
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind_addr.sin_port = htons(config_.udp_port);
if (bind(udp_sock_, reinterpret_cast<sockaddr*>(&bind_addr), sizeof(bind_addr)) < 0) {
const int saved_errno = errno;
last_error_ = ErrnoDetail("failed to bind KNXnet/IP UDP socket on port " +
std::to_string(config_.udp_port),
saved_errno);
ESP_LOGE(kTag, "%s", last_error_.c_str());
closeSockets();
return false;
}
timeval timeout{};
timeout.tv_sec = 0;
timeout.tv_usec = 20000;
if (setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
ESP_LOGW(kTag, "failed to set KNX UDP receive timeout on port %u: errno=%d (%s)",
static_cast<unsigned>(config_.udp_port), errno, std::strerror(errno));
}
if (config_.multicast_enabled) {
uint8_t multicast_loop = 0;
if (setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &multicast_loop,
sizeof(multicast_loop)) < 0) {
ESP_LOGW(kTag, "failed to disable KNX multicast loopback for %s: errno=%d (%s)",
config_.multicast_address.c_str(), errno, std::strerror(errno));
}
refreshNetworkInterfaces(true);
}
tcp_sock_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (tcp_sock_ < 0) {
last_error_ = ErrnoDetail("failed to create KNXnet/IP TCP socket", errno);
ESP_LOGE(kTag, "%s", last_error_.c_str());
closeSockets();
return false;
}
int reuse = 1;
if (setsockopt(tcp_sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
ESP_LOGW(kTag, "failed to enable TCP reuse for KNX port %u: errno=%d (%s)",
static_cast<unsigned>(config_.udp_port), errno, std::strerror(errno));
}
sockaddr_in tcp_bind_addr{};
tcp_bind_addr.sin_family = AF_INET;
tcp_bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
tcp_bind_addr.sin_port = htons(config_.udp_port);
if (bind(tcp_sock_, reinterpret_cast<sockaddr*>(&tcp_bind_addr), sizeof(tcp_bind_addr)) < 0) {
const int saved_errno = errno;
last_error_ = ErrnoDetail("failed to bind KNXnet/IP TCP socket on port " +
std::to_string(config_.udp_port),
saved_errno);
ESP_LOGE(kTag, "%s", last_error_.c_str());
closeSockets();
return false;
}
if (listen(tcp_sock_, static_cast<int>(kMaxTcpClients)) < 0) {
const int saved_errno = errno;
last_error_ = ErrnoDetail("failed to listen on KNXnet/IP TCP port " +
std::to_string(config_.udp_port),
saved_errno);
ESP_LOGE(kTag, "%s", last_error_.c_str());
closeSockets();
return false;
}
ESP_LOGI(kTag, "KNXnet/IP listening on UDP/TCP port %u",
static_cast<unsigned>(config_.udp_port));
return true;
}
void GatewayKnxTpIpRouter::handleTcpAccept() {
sockaddr_in remote{};
socklen_t remote_len = sizeof(remote);
const int client_sock = accept(tcp_sock_, reinterpret_cast<sockaddr*>(&remote), &remote_len);
if (client_sock < 0) {
ESP_LOGW(kTag, "failed to accept KNXnet/IP TCP client: errno=%d (%s)", errno,
std::strerror(errno));
return;
}
TcpClient* slot = nullptr;
for (auto& client : tcp_clients_) {
if (client.sock < 0) {
slot = &client;
break;
}
}
if (slot == nullptr) {
ESP_LOGW(kTag, "reject KNXnet/IP TCP client from %s: no free TCP slots",
EndpointString(remote).c_str());
close(client_sock);
return;
}
slot->sock = client_sock;
slot->remote = remote;
slot->rx_buffer.clear();
slot->last_activity_tick = xTaskGetTickCount();
ESP_LOGI(kTag, "accepted KNXnet/IP TCP client from %s", EndpointString(remote).c_str());
}
void GatewayKnxTpIpRouter::handleTcpClient(TcpClient& client) {
if (client.sock < 0) {
return;
}
std::array<uint8_t, 512> buffer{};
const int received = recv(client.sock, buffer.data(), buffer.size(), 0);
if (received <= 0) {
ESP_LOGI(kTag, "closed KNXnet/IP TCP client from %s", EndpointString(client.remote).c_str());
closeTcpClient(client);
return;
}
client.last_activity_tick = xTaskGetTickCount();
client.rx_buffer.insert(client.rx_buffer.end(), buffer.begin(), buffer.begin() + received);
while (client.rx_buffer.size() >= 6) {
uint16_t service = 0;
uint16_t total_len = 0;
if (!ParseKnxNetIpHeader(client.rx_buffer.data(), client.rx_buffer.size(), &service,
&total_len)) {
ESP_LOGW(kTag, "invalid KNXnet/IP TCP packet from %s; closing stream",
EndpointString(client.remote).c_str());
closeTcpClient(client);
return;
}
if (client.rx_buffer.size() < total_len) {
return;
}
std::vector<uint8_t> packet(client.rx_buffer.begin(), client.rx_buffer.begin() + total_len);
client.rx_buffer.erase(client.rx_buffer.begin(), client.rx_buffer.begin() + total_len);
active_tcp_sock_ = client.sock;
handleUdpDatagram(packet.data(), packet.size(), client.remote);
active_tcp_sock_ = -1;
if (client.sock < 0) {
return;
}
}
}
void GatewayKnxTpIpRouter::closeTcpClient(TcpClient& client) {
if (client.sock < 0) {
client.rx_buffer.clear();
return;
}
const int sock = client.sock;
for (auto& tunnel : tunnel_clients_) {
if (tunnel.connected && tunnel.tcp_sock == sock) {
resetTunnelClient(tunnel);
}
}
if (active_tcp_sock_ == sock) {
active_tcp_sock_ = -1;
}
shutdown(sock, SHUT_RDWR);
close(sock);
client.sock = -1;
client.rx_buffer.clear();
client.last_activity_tick = 0;
}
void GatewayKnxTpIpRouter::refreshNetworkInterfaces(bool force_log) {
if (!config_.multicast_enabled || udp_sock_ < 0) {
return;
}
const auto netifs = ActiveKnxNetifs();
if (netifs.empty()) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ != nullptr) {
ets_device_->setNetworkInterface(nullptr);
}
if (force_log) {
ESP_LOGW(kTag, "KNX multicast group %s not joined yet: no IPv4 interface is up",
config_.multicast_address.c_str());
}
return;
}
{
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ != nullptr) {
ets_device_->setNetworkInterface(netifs.front().netif);
}
}
const uint32_t multicast_address = inet_addr(config_.multicast_address.c_str());
for (const auto& netif : netifs) {
if (std::find(multicast_joined_interfaces_.begin(), multicast_joined_interfaces_.end(),
netif.address) != multicast_joined_interfaces_.end()) {
continue;
}
ip_mreq mreq{};
mreq.imr_multiaddr.s_addr = multicast_address;
mreq.imr_interface.s_addr = netif.address;
if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
ESP_LOGW(kTag,
"failed to join KNX multicast group %s on %s %s UDP port %u: errno=%d (%s)",
config_.multicast_address.c_str(), netif.key, Ipv4String(netif.address).c_str(),
static_cast<unsigned>(config_.udp_port), errno, std::strerror(errno));
continue;
}
multicast_joined_interfaces_.push_back(netif.address);
ESP_LOGI(kTag, "joined KNX multicast group %s on %s %s UDP port %u",
config_.multicast_address.c_str(), netif.key, Ipv4String(netif.address).c_str(),
static_cast<unsigned>(config_.udp_port));
}
}
std::unique_ptr<openknx::TpuartUartInterface>
GatewayKnxTpIpRouter::createOpenKnxTpUartInterface() {
if (!GatewayKnxConfigUsesTpUart(config_)) {
tp_uart_port_ = -1;
tp_uart_tx_pin_ = -1;
tp_uart_rx_pin_ = -1;
tp_uart_online_ = false;
ESP_LOGI(kTag, "KNX TP-UART disabled by UART port; KNXnet/IP uses IP-only runtime");
return nullptr;
}
const auto& serial = config_.tp_uart;
if (serial.uart_port < 0 || serial.uart_port > 2) {
last_error_ = "invalid KNX TP-UART port " + std::to_string(serial.uart_port);
ESP_LOGE(kTag, "%s", last_error_.c_str());
return nullptr;
}
const uart_port_t uart_port = static_cast<uart_port_t>(serial.uart_port);
int tx_pin = UART_PIN_NO_CHANGE;
int rx_pin = UART_PIN_NO_CHANGE;
const bool tx_pin_ok = ResolveUartIoPin(uart_port, serial.tx_pin, SOC_UART_TX_PIN_IDX, &tx_pin);
const bool rx_pin_ok = ResolveUartIoPin(uart_port, serial.rx_pin, SOC_UART_RX_PIN_IDX, &rx_pin);
if (!tx_pin_ok || !rx_pin_ok) {
last_error_ = "KNX TP-UART UART" + std::to_string(serial.uart_port) +
" has no ESP-IDF default " + (!tx_pin_ok ? std::string("TX") : std::string("")) +
(!tx_pin_ok && !rx_pin_ok ? "/" : "") +
(!rx_pin_ok ? std::string("RX") : std::string("")) +
" pin; configure explicit txPin/rxPin values";
ESP_LOGE(kTag, "%s", last_error_.c_str());
return nullptr;
}
tp_uart_port_ = serial.uart_port;
tp_uart_tx_pin_ = tx_pin;
tp_uart_rx_pin_ = rx_pin;
return std::make_unique<openknx::TpuartUartInterface>(
uart_port, serial.tx_pin, serial.rx_pin, serial.rx_buffer_size,
serial.tx_buffer_size, serial.nine_bit_mode);
}
bool GatewayKnxTpIpRouter::configureTpUart() {
if (tp_uart_port_ < 0) {
return true;
}
if (ets_device_ == nullptr || !ets_device_->hasTpUart()) {
last_error_ = "KNX TP-UART interface is unavailable in OpenKNX runtime";
ESP_LOGE(kTag, "%s", last_error_.c_str());
return false;
}
const TickType_t startup_timeout_ticks =
pdMS_TO_TICKS(config_.tp_uart.startup_timeout_ms);
const TickType_t retry_poll_ticks = std::max<TickType_t>(1, pdMS_TO_TICKS(20));
const TickType_t startup_begin_tick = xTaskGetTickCount();
tp_uart_online_ = ets_device_->enableTpUart(true);
while (!tp_uart_online_ && startup_timeout_ticks > 0 &&
(xTaskGetTickCount() - startup_begin_tick) < startup_timeout_ticks) {
vTaskDelay(retry_poll_ticks);
ets_device_->loop();
tp_uart_online_ = ets_device_->tpUartOnline();
}
if (!tp_uart_online_) {
last_error_ = "OpenKNX failed to initialize KNX TP-UART uart=" +
std::to_string(config_.tp_uart.uart_port) + " tx=" +
UartPinDescription(config_.tp_uart.tx_pin, tp_uart_tx_pin_) + " rx=" +
UartPinDescription(config_.tp_uart.rx_pin, tp_uart_rx_pin_);
const bool configured = ets_device_ != nullptr && ets_device_->configured();
ESP_LOGW(kTag,
"%s; continuing KNXnet/IP in %s IP mode while TP-UART stays offline",
last_error_.c_str(), configured ? "configured" : "commissioning-only");
tp_uart_port_ = -1;
tp_uart_online_ = false;
return true;
}
const TickType_t startup_elapsed_ticks = xTaskGetTickCount() - startup_begin_tick;
if (startup_elapsed_ticks > 0) {
ESP_LOGI(kTag, "KNX TP-UART startup settled after %lu ms",
static_cast<unsigned long>(pdTICKS_TO_MS(startup_elapsed_ticks)));
}
ESP_LOGI(kTag, "KNX TP-UART online uart=%d tx=%s rx=%s baud=%u nineBit=%d",
config_.tp_uart.uart_port,
UartPinDescription(config_.tp_uart.tx_pin, tp_uart_tx_pin_).c_str(),
UartPinDescription(config_.tp_uart.rx_pin, tp_uart_rx_pin_).c_str(),
static_cast<unsigned>(config_.tp_uart.baudrate), config_.tp_uart.nine_bit_mode);
return true;
}
bool GatewayKnxTpIpRouter::configureProgrammingGpio() {
programming_button_last_pressed_ = false;
programming_button_last_toggle_tick_ = 0;
programming_led_state_ = false;
if (config_.programming_button_gpio >= 0) {
gpio_config_t button_config{};
button_config.pin_bit_mask = 1ULL << static_cast<uint32_t>(config_.programming_button_gpio);
button_config.mode = GPIO_MODE_INPUT;
button_config.pull_up_en = config_.programming_button_active_low ? GPIO_PULLUP_ENABLE
: GPIO_PULLUP_DISABLE;
button_config.pull_down_en = config_.programming_button_active_low ? GPIO_PULLDOWN_DISABLE
: GPIO_PULLDOWN_ENABLE;
button_config.intr_type = GPIO_INTR_DISABLE;
const esp_err_t err = gpio_config(&button_config);
if (err != ESP_OK) {
last_error_ = EspErrDetail("failed to configure KNX programming button GPIO" +
std::to_string(config_.programming_button_gpio),
err);
ESP_LOGE(kTag, "%s", last_error_.c_str());
return false;
}
}
if (config_.programming_led_gpio >= 0) {
gpio_config_t led_config{};
led_config.pin_bit_mask = 1ULL << static_cast<uint32_t>(config_.programming_led_gpio);
led_config.mode = GPIO_MODE_OUTPUT;
led_config.pull_up_en = GPIO_PULLUP_DISABLE;
led_config.pull_down_en = GPIO_PULLDOWN_DISABLE;
led_config.intr_type = GPIO_INTR_DISABLE;
const esp_err_t err = gpio_config(&led_config);
if (err != ESP_OK) {
last_error_ = EspErrDetail("failed to configure KNX programming LED GPIO" +
std::to_string(config_.programming_led_gpio),
err);
ESP_LOGE(kTag, "%s", last_error_.c_str());
return false;
}
setProgrammingLed(false);
}
return true;
}
} // namespace gateway
@@ -0,0 +1,319 @@
#include "gateway_knx_private.hpp"
namespace gateway {
void GatewayKnxTpIpRouter::selectOpenKnxNetworkInterface(const sockaddr_in& remote) {
const auto netif = SelectKnxNetifForRemote(remote);
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ != nullptr) {
ets_device_->setNetworkInterface(netif.has_value() ? netif->netif : nullptr);
}
}
bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len,
TunnelClient* response_client,
uint16_t response_service,
const uint8_t* suppress_routing_echo,
size_t suppress_routing_echo_len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
std::vector<uint8_t> tunnel_confirmation;
const bool needs_tunnel_confirmation =
response_client != nullptr && response_client->connected &&
response_service == kServiceTunnellingRequest &&
BuildTunnelConfirmationFrame(data, len, &tunnel_confirmation);
bool sent_tunnel_confirmation = false;
const bool consumed = ets_device_->handleTunnelFrame(
data, len,
[this, response_client, response_service, needs_tunnel_confirmation,
&tunnel_confirmation, &sent_tunnel_confirmation,
suppress_routing_echo, suppress_routing_echo_len](const uint8_t* response,
size_t response_len) {
if (response == nullptr || response_len == 0) {
return;
}
const bool routing_context =
response_client == nullptr && response_service == kServiceRoutingIndication;
const auto message_code = CemiMessageCode(response, response_len);
if (routing_context && suppress_routing_echo != nullptr &&
IsLocalRoutingEchoIndication(response, response_len, suppress_routing_echo,
suppress_routing_echo_len)) {
return;
}
if (needs_tunnel_confirmation && !sent_tunnel_confirmation &&
message_code.has_value() && message_code.value() != L_data_con) {
sent_tunnel_confirmation = sendCemiFrameToClient(
*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
const uint16_t service = KnxIpServiceForCemi(response, response_len, response_service);
if (service == kServiceDeviceConfigurationRequest) {
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
} else if (routing_context) {
sendRoutingIndication(response, response_len);
}
return;
}
if (message_code.has_value() && message_code.value() == L_data_con) {
if (routing_context) {
return;
}
if (response_client != nullptr && response_client->connected) {
sent_tunnel_confirmation =
sendCemiFrameToClient(*response_client, service, response, response_len) ||
sent_tunnel_confirmation;
}
return;
}
if (routing_context) {
sendRoutingIndication(response, response_len);
return;
}
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
return;
}
sendTunnelIndication(response, response_len);
});
if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) {
sendCemiFrameToClient(*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
syncOpenKnxConfigFromDevice();
return consumed;
}
bool GatewayKnxTpIpRouter::transmitOpenKnxTpFrame(const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
const bool sent = ets_device_->transmitTpFrame(data, len);
tp_uart_online_ = ets_device_->tpUartOnline();
return sent;
}
bool GatewayKnxTpIpRouter::handleOpenKnxBusFrame(const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
const bool consumed = ets_device_->handleBusFrame(data, len);
syncOpenKnxConfigFromDevice();
return consumed;
}
bool GatewayKnxTpIpRouter::routeOpenKnxGroupWrite(const uint8_t* data, size_t len,
const char* context) {
const auto decoded = DecodeOpenKnxGroupWrite(data, len);
if (!decoded.has_value()) {
return false;
}
if (!shouldRouteDaliApplicationFrames()) {
return true;
}
const DaliBridgeResult result = group_write_handler_
? group_write_handler_(decoded->group_address,
decoded->data.data(),
decoded->data.size())
: bridge_.handleGroupWrite(decoded->group_address,
decoded->data.data(),
decoded->data.size());
if (!result.ok && !result.error.empty()) {
ESP_LOGD(kTag, "%s not routed to DALI: %s", context == nullptr ? "KNX group write" : context,
result.error.c_str());
}
return true;
}
bool GatewayKnxTpIpRouter::handleFunctionPropertyExtCommand(
uint16_t object_type, uint8_t object_instance, uint8_t property_id,
const uint8_t* data, size_t len, std::vector<uint8_t>* response) {
if (response == nullptr || object_type != kGroupObjectTableObjectType ||
property_id != kPidGoDiagnostics) {
return false;
}
const auto decoded = DecodeGoDiagnosticsGroupWrite(data, len);
if (!decoded.has_value()) {
const std::string payload = HexBytes(data, len);
ESP_LOGW(kTag,
"OpenKNX GO diagnostics write malformed objType=0x%04X objInst=%u property=0x%02X len=%u payload=%s",
static_cast<unsigned>(object_type), static_cast<unsigned>(object_instance),
static_cast<unsigned>(property_id), static_cast<unsigned>(len), payload.c_str());
*response = {ReturnCodes::DataVoid};
return true;
}
const std::string group_address_text =
GatewayKnxGroupAddressString(decoded->group_address);
const std::string payload = HexBytes(decoded->payload, decoded->payload_len);
ESP_LOGI(kTag,
"OpenKNX GO diagnostics group write ga=0x%04X (%s) len=%u payload=%s",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str(),
static_cast<unsigned>(decoded->payload_len), payload.c_str());
if (!shouldRouteDaliApplicationFrames()) {
ESP_LOGW(kTag,
"OpenKNX GO diagnostics group write ga=0x%04X (%s) blocked by commissioning-only routing state",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str());
*response = {ReturnCodes::TemporarilyNotAvailable};
return true;
}
const DaliBridgeResult result =
group_write_handler_ ? group_write_handler_(decoded->group_address, decoded->payload,
decoded->payload_len)
: bridge_.handleGroupWrite(decoded->group_address,
decoded->payload,
decoded->payload_len);
const uint8_t return_code = GoDiagnosticsReturnCode(result);
if (return_code == ReturnCodes::AddressVoid) {
ESP_LOGW(kTag,
"OpenKNX GO diagnostics group write ga=0x%04X (%s) returning AddressVoid: %s",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str(),
result.error.empty() ? "unmapped KNX group address"
: result.error.c_str());
} else if (!result.ok) {
ESP_LOGW(kTag,
"OpenKNX GO diagnostics group write ga=0x%04X (%s) failed rc=0x%02X: %s",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str(),
static_cast<unsigned>(return_code),
result.error.empty() ? "command routing failed" : result.error.c_str());
}
response->assign(1, return_code);
return true;
}
bool GatewayKnxTpIpRouter::handleFunctionPropertyExtState(
uint16_t object_type, uint8_t object_instance, uint8_t property_id,
const uint8_t* data, size_t len, std::vector<uint8_t>* response) {
if (response == nullptr || object_type != kGroupObjectTableObjectType ||
property_id != kPidGoDiagnostics) {
return false;
}
const auto decoded = DecodeGoDiagnosticsGroupWrite(data, len);
if (!decoded.has_value()) {
const std::string payload = HexBytes(data, len);
ESP_LOGW(kTag,
"OpenKNX GO diagnostics state request malformed objType=0x%04X objInst=%u property=0x%02X len=%u payload=%s",
static_cast<unsigned>(object_type), static_cast<unsigned>(object_instance),
static_cast<unsigned>(property_id), static_cast<unsigned>(len), payload.c_str());
*response = {ReturnCodes::DataVoid};
return true;
}
const std::string group_address_text =
GatewayKnxGroupAddressString(decoded->group_address);
ESP_LOGW(kTag,
"OpenKNX GO diagnostics state request unsupported ga=0x%04X (%s)",
static_cast<unsigned>(decoded->group_address), group_address_text.c_str());
*response = {ReturnCodes::InvalidCommand};
return true;
}
bool GatewayKnxTpIpRouter::emitOpenKnxGroupValue(uint16_t group_object_number,
const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
const bool emitted = ets_device_->emitGroupValue(
group_object_number, data, len, [this](const uint8_t* frame_data, size_t frame_len) {
sendRoutingIndication(frame_data, frame_len);
sendTunnelIndication(frame_data, frame_len);
if (ets_device_ != nullptr) {
const bool sent_to_tp = ets_device_->transmitTpFrame(frame_data, frame_len);
tp_uart_online_ = sent_to_tp || ets_device_->tpUartOnline();
}
});
syncOpenKnxConfigFromDevice();
return emitted;
}
bool GatewayKnxTpIpRouter::shouldRouteDaliApplicationFrames() const {
if (!commissioning_only_) {
return true;
}
return openknx_configured_.load();
}
uint8_t GatewayKnxTpIpRouter::advertisedMedium() const {
return (config_.tunnel_enabled || tp_uart_online_) ? kKnxMediumTp1 : kKnxMediumIp;
}
void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() {
if (ets_device_ == nullptr) {
return;
}
const auto snapshot = ets_device_->snapshot();
openknx_configured_.store(snapshot.configured);
bool changed = false;
GatewayKnxConfig updated = config_;
if (snapshot.individual_address != 0 && snapshot.individual_address != 0xffff &&
snapshot.individual_address != updated.individual_address) {
updated.individual_address = snapshot.individual_address;
changed = true;
}
if (snapshot.configured || !snapshot.associations.empty()) {
std::vector<GatewayKnxEtsAssociation> associations;
associations.reserve(snapshot.associations.size());
for (const auto& association : snapshot.associations) {
associations.push_back(GatewayKnxEtsAssociation{association.group_address,
association.group_object_number});
}
if (associations.size() != updated.ets_associations.size() ||
!std::equal(associations.begin(), associations.end(), updated.ets_associations.begin(),
[](const GatewayKnxEtsAssociation& lhs,
const GatewayKnxEtsAssociation& rhs) {
return lhs.group_address == rhs.group_address &&
lhs.group_object_number == rhs.group_object_number;
})) {
updated.ets_associations = std::move(associations);
changed = true;
}
}
if (!changed) {
return;
}
config_ = updated;
bridge_.setConfig(config_);
}
uint16_t GatewayKnxTpIpRouter::effectiveIpInterfaceIndividualAddress() const {
if (config_.ip_interface_individual_address != 0 &&
config_.ip_interface_individual_address != 0xffff) {
return config_.ip_interface_individual_address;
}
return 0xff01;
}
uint16_t GatewayKnxTpIpRouter::effectiveKnxDeviceIndividualAddress() const {
if (ets_device_ != nullptr) {
const uint16_t address = ets_device_->individualAddress();
if (address != 0 && address != 0xffff) {
return address;
}
}
return config_.individual_address;
}
uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddress() const {
const uint16_t interface_address = effectiveIpInterfaceIndividualAddress();
uint16_t device = static_cast<uint16_t>((interface_address & 0x00ff) + 1);
if (device == 0 || device > 0xff) {
device = 1;
}
uint16_t address = static_cast<uint16_t>((interface_address & 0xff00) | device);
if (address == 0xffff) {
address = static_cast<uint16_t>((interface_address & 0xff00) | 0x0001);
}
return address;
}
} // namespace gateway
@@ -0,0 +1,421 @@
#include "gateway_knx_private.hpp"
namespace gateway {
void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequence,
uint8_t status, const sockaddr_in& remote) {
sendConnectionHeaderAck(kServiceTunnellingAck, channel_id, sequence, status, remote);
}
void GatewayKnxTpIpRouter::sendDeviceConfigurationAck(uint8_t channel_id, uint8_t sequence,
uint8_t status,
const sockaddr_in& remote) {
sendConnectionHeaderAck(kServiceDeviceConfigurationAck, channel_id, sequence, status, remote);
}
void GatewayKnxTpIpRouter::sendConnectionHeaderAck(uint16_t service, uint8_t channel_id,
uint8_t sequence, uint8_t status,
const sockaddr_in& remote) {
KnxIpTunnelingAck ack;
ack.serviceTypeIdentifier(service);
ack.connectionHeader().length(LEN_CH);
ack.connectionHeader().channelId(channel_id);
ack.connectionHeader().sequenceCounter(sequence);
ack.connectionHeader().status(status);
const std::vector<uint8_t> packet(ack.data(), ack.data() + ack.totalLength());
sendPacket(packet, remote);
}
void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockaddr_in& remote) {
const std::vector<uint8_t> body{status, 0x00};
const auto packet = OpenKnxIpPacket(kServiceSecureSessionStatus, body);
sendPacket(packet, remote);
}
bool GatewayKnxTpIpRouter::sendPacket(const std::vector<uint8_t>& packet,
const sockaddr_in& remote) const {
if (packet.empty()) {
return false;
}
if (active_tcp_sock_ >= 0) {
return SendStream(active_tcp_sock_, packet.data(), packet.size());
}
return udp_sock_ >= 0 && SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
bool GatewayKnxTpIpRouter::sendPacketToTunnelClient(
const TunnelClient& client, const std::vector<uint8_t>& packet) const {
if (packet.empty()) {
return false;
}
if (client.tcp_sock >= 0) {
return SendStream(client.tcp_sock, packet.data(), packet.size());
}
return udp_sock_ >= 0 && SendAll(udp_sock_, packet.data(), packet.size(), client.data_remote);
}
bool GatewayKnxTpIpRouter::currentTransportAllowsTcpHpai() const {
return active_tcp_sock_ >= 0;
}
std::optional<std::array<uint8_t, 8>> GatewayKnxTpIpRouter::localHpaiForRemote(
const sockaddr_in& remote, bool tcp) const {
std::array<uint8_t, 8> hpai{};
hpai[0] = 0x08;
hpai[1] = tcp ? kKnxHpaiIpv4Tcp : kKnxHpaiIpv4Udp;
if (tcp) {
return hpai;
}
const auto netif = SelectKnxNetifForRemote(remote);
if (!netif.has_value()) {
return std::nullopt;
}
WriteIp(hpai.data() + 2, netif->address);
WriteBe16(hpai.data() + 6, config_.udp_port);
return hpai;
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildOpenKnxSearchResponse(
const sockaddr_in& remote) const {
// Use OpenKNX's proven DIB construction via KnxIpSearchResponse.
// Requires ets_device_ to be initialized (DeviceObject + Platform).
if (ets_device_ == nullptr || knx_ip_parameters_ == nullptr) {
ESP_LOGW(kTag, "OpenKNX search response unavailable; falling back to hand-rolled DIBs");
return {};
}
KnxIpSearchResponse response(*knx_ip_parameters_, ets_device_->deviceObject());
return std::vector<uint8_t>(response.data(), response.data() + response.totalLength());
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildOpenKnxDescriptionResponse(
const sockaddr_in& remote) const {
if (ets_device_ == nullptr || knx_ip_parameters_ == nullptr) {
ESP_LOGW(kTag, "OpenKNX description response unavailable; falling back to hand-rolled DIBs");
return {};
}
KnxIpDescriptionResponse response(*knx_ip_parameters_, ets_device_->deviceObject());
return std::vector<uint8_t>(response.data(), response.data() + response.totalLength());
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildDeviceInfoDib(
const sockaddr_in& remote) const {
std::vector<uint8_t> dib(54, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = kKnxDibDeviceInfo;
dib[2] = advertisedMedium();
dib[3] = 0;
WriteBe16(dib.data() + 4, effectiveIpInterfaceIndividualAddress());
WriteBe16(dib.data() + 6, 0);
uint8_t mac[6]{};
if (ReadBaseMac(mac)) {
dib[8] = static_cast<uint8_t>((knx_internal::kReg1DaliManufacturerId >> 8) & 0xff);
dib[9] = static_cast<uint8_t>(knx_internal::kReg1DaliManufacturerId & 0xff);
std::memcpy(dib.data() + 10, mac + 2, 4);
std::memcpy(dib.data() + 18, mac, 6);
}
WriteIp(dib.data() + 14, inet_addr(config_.multicast_address.c_str()));
char friendly[31]{};
std::snprintf(friendly, sizeof(friendly), "DALI GW MG%u %s",
static_cast<unsigned>(config_.main_group), openknx_namespace_.c_str());
std::memcpy(dib.data() + 24, friendly, std::min<size_t>(30, std::strlen(friendly)));
(void)remote;
return dib;
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildExtendedDeviceInfoDib() const {
std::vector<uint8_t> dib(8, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = kKnxDibExtendedDeviceInfo;
dib[2] = 0x01;
dib[3] = 0x00;
WriteBe16(dib.data() + 4, 254);
WriteBe16(dib.data() + 6,
advertisedMedium() == kKnxMediumIp ? kKnxIpOnlyDeviceDescriptor
: kKnxTpIpInterfaceDeviceDescriptor);
return dib;
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildIpConfigDib(const sockaddr_in& remote,
bool current) const {
const auto netif = SelectKnxNetifForRemote(remote);
const uint32_t address = netif.has_value() ? netif->address : htonl(INADDR_ANY);
const uint32_t netmask = netif.has_value() ? netif->netmask : htonl(INADDR_ANY);
const uint32_t gateway = netif.has_value() ? netif->gateway : htonl(INADDR_ANY);
std::vector<uint8_t> dib(current ? 20 : 16, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = current ? kKnxDibCurrentIpConfig : kKnxDibIpConfig;
WriteIp(dib.data() + 2, address);
WriteIp(dib.data() + 6, netmask);
WriteIp(dib.data() + 10, gateway);
if (current) {
WriteIp(dib.data() + 14, htonl(INADDR_ANY));
dib[18] = kKnxIpAssignmentManual;
dib[19] = 0x00;
} else {
dib[14] = kKnxIpCapabilityManual;
dib[15] = kKnxIpAssignmentManual;
}
return dib;
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildKnxAddressesDib() const {
std::vector<uint8_t> dib(4 + kMaxTunnelClients * 2U, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = kKnxDibKnxAddresses;
WriteBe16(dib.data() + 2, effectiveIpInterfaceIndividualAddress());
size_t offset = 4;
for (size_t slot = 0; slot < kMaxTunnelClients; ++slot) {
WriteBe16(dib.data() + offset, effectiveTunnelAddressForSlot(slot));
offset += 2;
}
return dib;
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildTunnelingInfoDib() const {
std::vector<uint8_t> dib(4 + kMaxTunnelClients * 4U, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = kKnxDibTunnellingInfo;
WriteBe16(dib.data() + 2, 254);
size_t offset = 4;
for (size_t slot = 0; slot < kMaxTunnelClients; ++slot) {
const uint16_t address = effectiveTunnelAddressForSlot(slot);
bool used = false;
for (const auto& client : tunnel_clients_) {
if (client.connected && client.individual_address == address) {
used = true;
break;
}
}
uint16_t flags = 0xffff;
if (used) {
flags = static_cast<uint16_t>(flags & ~0x0001U);
flags = static_cast<uint16_t>(flags & ~0x0004U);
}
WriteBe16(dib.data() + offset, address);
WriteBe16(dib.data() + offset + 2, flags);
offset += 4;
}
return dib;
}
std::vector<uint8_t> GatewayKnxTpIpRouter::buildSupportedServiceDib() const {
std::vector<std::pair<uint8_t, uint8_t>> services{
{kKnxServiceFamilyCore, 2},
{kKnxServiceFamilyDeviceManagement, 1},
};
if (config_.tunnel_enabled) {
services.emplace_back(kKnxServiceFamilyTunnelling, 1);
}
if (config_.multicast_enabled) {
services.emplace_back(kKnxServiceFamilyRouting, 1);
}
std::vector<uint8_t> dib(2 + services.size() * 2U, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = kKnxDibSupportedServices;
size_t offset = 2;
for (const auto& service : services) {
dib[offset++] = service.first;
dib[offset++] = service.second;
}
return dib;
}
void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) {
if (data == nullptr || len == 0) {
return;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (!frame.valid()) {
ESP_LOGW(kTag, "not sending invalid OpenKNX tunnel indication len=%u",
static_cast<unsigned>(len));
return;
}
auto is_tunnel_recipient = [](const TunnelClient& client) {
return client.connected && client.connection_type == kKnxConnectionTypeTunnel;
};
auto send_to_client = [this, data, len](TunnelClient& client) {
sendTunnelIndicationToClient(client, data, len);
};
const bool suppress_source_echo =
frame.addressType() == GroupAddress || frame.addressType() == IndividualAddress;
const uint16_t source_address = suppress_source_echo ? frame.sourceAddress() : 0;
if (frame.addressType() == IndividualAddress) {
for (auto& client : tunnel_clients_) {
if (!is_tunnel_recipient(client)) {
continue;
}
if (client.individual_address == source_address) {
continue;
}
if (client.individual_address == frame.destinationAddress()) {
send_to_client(client);
return;
}
}
}
for (auto& client : tunnel_clients_) {
if (!is_tunnel_recipient(client)) {
continue;
}
if (suppress_source_echo && client.individual_address == source_address) {
continue;
}
send_to_client(client);
}
}
void GatewayKnxTpIpRouter::sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data,
size_t len) {
sendCemiFrameToClient(client, kServiceTunnellingRequest, data, len);
}
bool GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t service,
const uint8_t* data, size_t len) {
if (!client.connected || data == nullptr || len == 0) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (!frame.valid()) {
ESP_LOGW(kTag, "not sending invalid OpenKNX cEMI service=0x%04x len=%u to %s",
static_cast<unsigned>(service), static_cast<unsigned>(len),
EndpointString(client.data_remote).c_str());
return false;
}
KnxIpTunnelingRequest request(frame);
request.serviceTypeIdentifier(service);
request.connectionHeader().length(LEN_CH);
request.connectionHeader().channelId(client.channel_id);
const auto message_code = CemiMessageCode(data, len);
const uint8_t send_sequence = client.send_sequence++;
request.connectionHeader().sequenceCounter(send_sequence);
request.connectionHeader().status(kKnxNoError);
const std::vector<uint8_t> packet(request.data(), request.data() + request.totalLength());
if (!sendPacketToTunnelClient(client, packet)) {
ESP_LOGW(kTag, "failed to send KNXnet/IP cEMI service=0x%04x channel=%u seq=%u to %s",
static_cast<unsigned>(service), static_cast<unsigned>(client.channel_id),
static_cast<unsigned>(request.connectionHeader().sequenceCounter()),
EndpointString(client.data_remote).c_str());
return false;
}
if (service == kServiceTunnellingRequest && message_code.has_value() &&
message_code.value() == L_data_con) {
if (IsOpenKnxGroupValueWrite(data, len)) {
client.last_tunnel_confirmation_sequence = 0;
client.last_tunnel_confirmation_packet.clear();
} else {
client.last_tunnel_confirmation_sequence = send_sequence;
client.last_tunnel_confirmation_packet = packet;
}
}
ESP_LOGI(kTag, "sent KNXnet/IP cEMI service=0x%04x channel=%u seq=%u cemi=0x%02x len=%u to %s",
static_cast<unsigned>(service), static_cast<unsigned>(client.channel_id),
static_cast<unsigned>(request.connectionHeader().sequenceCounter()), static_cast<unsigned>(data[0]),
static_cast<unsigned>(len), EndpointString(client.data_remote).c_str());
return true;
}
void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
const sockaddr_in& remote) {
KnxIpStateResponse response(channel_id, status);
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
sendPacket(packet, remote);
}
void GatewayKnxTpIpRouter::sendDisconnectResponse(uint8_t channel_id, uint8_t status,
const sockaddr_in& remote) {
KnxIpDisconnectResponse response(channel_id, status);
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
sendPacket(packet, remote);
}
void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t status,
const sockaddr_in& remote,
uint8_t connection_type,
uint16_t tunnel_address) {
if (status != kKnxNoError) {
KnxIpConnectResponse response(channel_id, status);
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
sendPacket(packet, remote);
ESP_LOGI(kTag, "sent KNXnet/IP connect error channel=%u status=0x%02x to %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(status),
EndpointString(remote).c_str());
return;
}
const auto netif = SelectKnxNetifForRemote(remote);
if (!netif.has_value() || knx_ip_parameters_ == nullptr) {
ESP_LOGW(kTag, "cannot accept KNXnet/IP connect from %s: no active IPv4 interface",
EndpointString(remote).c_str());
KnxIpConnectResponse response(channel_id, kKnxErrorConnectionType);
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
sendPacket(packet, remote);
return;
}
KnxIpConnectResponse response(*knx_ip_parameters_, tunnel_address, config_.udp_port,
channel_id, connection_type);
const bool tcp = currentTransportAllowsTcpHpai();
const uint32_t endpoint_address = ntohl(netif->address);
response.controlEndpoint().code(tcp ? IPV4_TCP : IPV4_UDP);
response.controlEndpoint().ipAddress(tcp ? 0 : endpoint_address);
response.controlEndpoint().ipPortNumber(tcp ? 0 : config_.udp_port);
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
sendPacket(packet, remote);
std::string endpoint_string;
if (tcp) {
endpoint_string = "0.0.0.0:0 (TCP HPAI)";
} else {
sockaddr_in local_endpoint{};
local_endpoint.sin_family = AF_INET;
local_endpoint.sin_port = htons(config_.udp_port);
local_endpoint.sin_addr.s_addr = netif->address;
endpoint_string = EndpointString(local_endpoint);
}
ESP_LOGI(kTag, "sent KNXnet/IP connect response channel=%u type=0x%02x to %s endpoint=%s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(connection_type),
EndpointString(remote).c_str(), endpoint_string.c_str());
}
void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len) {
if (!config_.multicast_enabled || udp_sock_ < 0 || data == nullptr || len == 0) {
return;
}
sockaddr_in remote{};
remote.sin_family = AF_INET;
remote.sin_port = htons(config_.udp_port);
remote.sin_addr.s_addr = inet_addr(config_.multicast_address.c_str());
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (!frame.valid()) {
ESP_LOGW(kTag, "not sending invalid OpenKNX routing cEMI len=%u",
static_cast<unsigned>(len));
return;
}
KnxIpRoutingIndication routing(frame);
const std::vector<uint8_t> packet(routing.data(), routing.data() + routing.totalLength());
const auto netifs = ActiveKnxNetifs();
if (netifs.empty()) {
SendAll(udp_sock_, packet.data(), packet.size(), remote);
return;
}
for (const auto& netif : netifs) {
in_addr multicast_interface{};
multicast_interface.s_addr = netif.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 %s %s: errno=%d (%s)",
netif.key, Ipv4String(netif.address).c_str(), errno, std::strerror(errno));
continue;
}
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
}
} // namespace gateway
@@ -0,0 +1,662 @@
#include "gateway_knx_private.hpp"
namespace gateway {
void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len,
const sockaddr_in& remote) {
uint16_t service = 0;
uint16_t total_len = 0;
if (!ParseKnxNetIpHeader(data, len, &service, &total_len)) {
return;
}
const uint8_t* body = data + 6;
const size_t body_len = total_len - 6;
if (IsKnxNetIpSecureService(service)) {
handleSecureService(service, body, body_len, remote);
return;
}
switch (service) {
case kServiceSearchRequest:
case kServiceSearchRequestExt:
handleSearchRequest(service, data, total_len, remote);
break;
case kServiceDescriptionRequest:
handleDescriptionRequest(data, total_len, remote);
break;
case kServiceDeviceConfigurationRequest:
handleDeviceConfigurationRequest(data, total_len, remote);
break;
case kServiceDeviceConfigurationAck:
case kServiceTunnellingAck:
if (body_len >= 4) {
ESP_LOGD(kTag, "rx KNXnet/IP ack service=0x%04x channel=%u seq=%u status=0x%02x from %s",
static_cast<unsigned>(service), static_cast<unsigned>(body[1]),
static_cast<unsigned>(body[2]), static_cast<unsigned>(body[3]),
EndpointString(remote).c_str());
TunnelClient* client = findTunnelClient(body[1]);
if (client != nullptr) {
client->last_activity_tick = xTaskGetTickCount();
if (service == kServiceTunnellingAck && body[3] == kKnxNoError &&
!client->last_tunnel_confirmation_packet.empty() &&
client->last_tunnel_confirmation_sequence == body[2]) {
client->last_tunnel_confirmation_packet.clear();
}
}
}
break;
case kServiceRoutingIndication:
if (config_.multicast_enabled) {
handleRoutingIndication(data, total_len);
}
break;
case kServiceTunnellingRequest:
if (config_.tunnel_enabled) {
handleTunnellingRequest(data, total_len, remote);
}
break;
case kServiceConnectRequest:
if (config_.tunnel_enabled) {
handleConnectRequest(data, total_len, remote);
}
break;
case kServiceConnectionStateRequest:
handleConnectionStateRequest(data, total_len, remote);
break;
case kServiceDisconnectRequest:
handleDisconnectRequest(data, total_len, remote);
break;
default:
ESP_LOGD(kTag, "ignore KNXnet/IP service=0x%04x len=%u from %s",
static_cast<unsigned>(service), static_cast<unsigned>(body_len),
EndpointString(remote).c_str());
break;
}
}
void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t* packet_data,
size_t len, const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_IPHPAI) {
ESP_LOGW(kTag, "invalid KNXnet/IP search request from %s len=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len));
return;
}
std::vector<uint8_t> request_packet(packet_data, packet_data + len);
KnxIpSearchRequest request(request_packet.data(), static_cast<uint16_t>(request_packet.size()));
auto& request_hpai = request.hpai();
if (OpenKnxHpaiUsesUnsupportedProtocol(request_hpai, currentTransportAllowsTcpHpai())) {
ESP_LOGW(kTag, "ignore KNXnet/IP search request from %s: unsupported HPAI protocol",
EndpointString(remote).c_str());
return;
}
sockaddr_in response_remote = EndpointFromOpenKnxHpai(request_hpai, remote);
selectOpenKnxNetworkInterface(response_remote);
const auto hpai = localHpaiForRemote(response_remote, currentTransportAllowsTcpHpai());
if (!hpai.has_value()) {
ESP_LOGW(kTag, "cannot send KNXnet/IP search response to %s: no active IPv4 interface",
EndpointString(response_remote).c_str());
return;
}
std::vector<uint8_t> body_resp;
body_resp.insert(body_resp.end(), hpai->begin(), hpai->end());
auto dev_dib = buildDeviceInfoDib(response_remote);
body_resp.insert(body_resp.end(), dev_dib.begin(), dev_dib.end());
auto svc_dib = buildSupportedServiceDib();
body_resp.insert(body_resp.end(), svc_dib.begin(), svc_dib.end());
if (service == kServiceSearchRequestExt) {
auto ext_dib = buildExtendedDeviceInfoDib();
body_resp.insert(body_resp.end(), ext_dib.begin(), ext_dib.end());
auto ip_dib = buildIpConfigDib(response_remote, false);
body_resp.insert(body_resp.end(), ip_dib.begin(), ip_dib.end());
auto current_ip_dib = buildIpConfigDib(response_remote, true);
body_resp.insert(body_resp.end(), current_ip_dib.begin(), current_ip_dib.end());
auto addresses_dib = buildKnxAddressesDib();
body_resp.insert(body_resp.end(), addresses_dib.begin(), addresses_dib.end());
auto tunneling_dib = buildTunnelingInfoDib();
body_resp.insert(body_resp.end(), tunneling_dib.begin(), tunneling_dib.end());
}
const auto response_packet = OpenKnxIpPacket(
service == kServiceSearchRequestExt ? kServiceSearchResponseExt : kServiceSearchResponse,
body_resp);
sendPacket(response_packet, response_remote);
ESP_LOGI(kTag, "sent KNXnet/IP search response service=0x%04x namespace=%s mainGroup=%u to %s:%u endpoint=%u.%u.%u.%u:%u",
static_cast<unsigned>(service), openknx_namespace_.c_str(),
static_cast<unsigned>(config_.main_group),
Ipv4String(response_remote.sin_addr.s_addr).c_str(), static_cast<unsigned>(ntohs(response_remote.sin_port)),
static_cast<unsigned>((*hpai)[2]), static_cast<unsigned>((*hpai)[3]),
static_cast<unsigned>((*hpai)[4]), static_cast<unsigned>((*hpai)[5]),
static_cast<unsigned>(config_.udp_port));
}
void GatewayKnxTpIpRouter::handleDescriptionRequest(const uint8_t* packet_data, size_t len,
const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_IPHPAI) {
ESP_LOGW(kTag, "invalid KNXnet/IP description request from %s len=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len));
return;
}
std::vector<uint8_t> request_packet(packet_data, packet_data + len);
KnxIpDescriptionRequest request(request_packet.data(), static_cast<uint16_t>(request_packet.size()));
auto& hpai = request.hpaiCtrl();
if (OpenKnxHpaiUsesUnsupportedProtocol(hpai, currentTransportAllowsTcpHpai())) {
ESP_LOGW(kTag, "ignore KNXnet/IP description request from %s: unsupported HPAI protocol",
EndpointString(remote).c_str());
return;
}
const sockaddr_in response_remote = EndpointFromOpenKnxHpai(hpai, remote);
selectOpenKnxNetworkInterface(response_remote);
auto device = buildDeviceInfoDib(response_remote);
auto services = buildSupportedServiceDib();
std::vector<uint8_t> body_resp;
auto extended = buildExtendedDeviceInfoDib();
auto ip_config = buildIpConfigDib(response_remote, false);
auto current_ip_config = buildIpConfigDib(response_remote, true);
auto addresses = buildKnxAddressesDib();
auto tunneling = buildTunnelingInfoDib();
body_resp.reserve(device.size() + services.size() + extended.size() + ip_config.size() +
current_ip_config.size() + addresses.size() + tunneling.size());
body_resp.insert(body_resp.end(), device.begin(), device.end());
body_resp.insert(body_resp.end(), services.begin(), services.end());
body_resp.insert(body_resp.end(), extended.begin(), extended.end());
body_resp.insert(body_resp.end(), ip_config.begin(), ip_config.end());
body_resp.insert(body_resp.end(), current_ip_config.begin(), current_ip_config.end());
body_resp.insert(body_resp.end(), addresses.begin(), addresses.end());
body_resp.insert(body_resp.end(), tunneling.begin(), tunneling.end());
const auto response_packet = OpenKnxIpPacket(kServiceDescriptionResponse, body_resp);
sendPacket(response_packet, response_remote);
ESP_LOGI(kTag, "sent KNXnet/IP description response namespace=%s medium=0x%02x tpOnline=%d to %s:%u",
openknx_namespace_.c_str(), static_cast<unsigned>(advertisedMedium()),
tp_uart_online_, Ipv4String(response_remote.sin_addr.s_addr).c_str(),
static_cast<unsigned>(ntohs(response_remote.sin_port)));
}
void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* packet_data, size_t len) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2) {
return;
}
std::vector<uint8_t> packet(packet_data, packet_data + len);
KnxIpRoutingIndication routing(packet.data(), static_cast<uint16_t>(packet.size()));
CemiFrame& frame = routing.frame();
if (!frame.valid()) {
ESP_LOGW(kTag, "invalid OpenKNX routing cEMI len=%u", static_cast<unsigned>(len));
return;
}
const uint8_t* cemi = frame.data();
const size_t cemi_len = frame.dataLength();
bool consumed_by_local_application = false;
if (ets_device_ != nullptr && MatchesOpenKnxLocalIndividualAddress(frame, *ets_device_)) {
std::vector<uint8_t> local_tunnel_frame;
if (BuildLocalRoutingTunnelFrame(cemi, cemi_len, &local_tunnel_frame)) {
consumed_by_local_application = handleOpenKnxTunnelFrame(
local_tunnel_frame.data(), local_tunnel_frame.size(), nullptr,
kServiceRoutingIndication,
frame.messageCode() == L_data_ind ? cemi : nullptr,
frame.messageCode() == L_data_ind ? cemi_len : 0);
}
}
if (consumed_by_local_application) {
return;
}
const bool consumed_by_openknx = handleOpenKnxBusFrame(cemi, cemi_len);
const bool routed_to_dali = routeOpenKnxGroupWrite(cemi, cemi_len, "KNX routing indication");
const bool sent_to_tp = transmitOpenKnxTpFrame(cemi, cemi_len);
if (!consumed_by_openknx && !routed_to_dali && !sent_to_tp) {
ESP_LOGD(kTag, "KNX routing indication ignored: no OpenKNX/DALI handler matched");
}
}
GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::findTunnelClient(
uint8_t channel_id) {
if (channel_id == 0) {
return nullptr;
}
for (auto& client : tunnel_clients_) {
if (client.connected && client.channel_id == channel_id) {
return &client;
}
}
return nullptr;
}
const GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::findTunnelClient(
uint8_t channel_id) const {
if (channel_id == 0) {
return nullptr;
}
for (const auto& client : tunnel_clients_) {
if (client.connected && client.channel_id == channel_id) {
return &client;
}
}
return nullptr;
}
void GatewayKnxTpIpRouter::resetTunnelClient(TunnelClient& client) {
if (client.connected) {
ESP_LOGI(kTag, "closed KNXnet/IP tunnel channel=%u type=0x%02x data=%s",
static_cast<unsigned>(client.channel_id),
static_cast<unsigned>(client.connection_type),
EndpointString(client.data_remote).c_str());
}
client = TunnelClient{};
}
uint8_t GatewayKnxTpIpRouter::nextTunnelChannelId() const {
uint8_t candidate = last_tunnel_channel_id_;
for (int attempts = 0; attempts < 255; ++attempts) {
candidate = static_cast<uint8_t>(candidate + 1);
if (candidate == 0) {
candidate = 1;
}
if (findTunnelClient(candidate) == nullptr) {
return candidate;
}
}
return 0;
}
uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddressForSlot(size_t slot) const {
const uint16_t first = effectiveTunnelAddress();
const uint16_t line = first & 0xff00;
uint16_t device = static_cast<uint16_t>((first & 0x00ff) + slot);
if (device == 0 || device > 0xff) {
device = static_cast<uint16_t>(1 + slot);
}
return static_cast<uint16_t>(line | (device & 0x00ff));
}
GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::allocateTunnelClient(
const sockaddr_in& control_remote, const sockaddr_in& data_remote, uint8_t connection_type) {
TunnelClient* free_client = nullptr;
size_t free_index = 0;
for (size_t index = 0; index < tunnel_clients_.size(); ++index) {
auto& client = tunnel_clients_[index];
const bool same_tcp_stream = active_tcp_sock_ >= 0 && client.tcp_sock == active_tcp_sock_;
const bool same_udp_endpoints = EndpointEquals(client.control_remote, control_remote) &&
EndpointEquals(client.data_remote, data_remote);
if (client.connected && client.connection_type == connection_type &&
(same_tcp_stream || same_udp_endpoints)) {
ESP_LOGW(kTag, "replacing existing KNXnet/IP tunnel channel=%u for endpoint %s",
static_cast<unsigned>(client.channel_id), EndpointString(data_remote).c_str());
resetTunnelClient(client);
free_client = &client;
free_index = index;
break;
}
if (!client.connected && free_client == nullptr) {
free_client = &client;
free_index = index;
}
}
if (free_client == nullptr) {
return nullptr;
}
const uint8_t channel_id = nextTunnelChannelId();
if (channel_id == 0) {
return nullptr;
}
free_client->connected = true;
free_client->channel_id = channel_id;
free_client->connection_type = connection_type;
free_client->received_sequence = 255;
free_client->send_sequence = 0;
free_client->individual_address = effectiveTunnelAddressForSlot(free_index);
free_client->last_activity_tick = xTaskGetTickCount();
free_client->control_remote = control_remote;
free_client->data_remote = data_remote;
free_client->tcp_sock = active_tcp_sock_;
last_tunnel_channel_id_ = channel_id;
return free_client;
}
void GatewayKnxTpIpRouter::pruneStaleTunnelClients() {
const TickType_t now = xTaskGetTickCount();
const TickType_t timeout = pdMS_TO_TICKS(120000);
for (auto& client : tunnel_clients_) {
if (!client.connected || client.last_activity_tick == 0) {
continue;
}
if (now - client.last_activity_tick > timeout) {
ESP_LOGW(kTag, "closing stale KNXnet/IP tunnel channel=%u after heartbeat timeout",
static_cast<unsigned>(client.channel_id));
resetTunnelClient(client);
}
}
}
void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* packet_data, size_t len,
const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_CH + 2) {
ESP_LOGW(kTag, "invalid KNXnet/IP tunnelling request from %s len=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len));
return;
}
std::vector<uint8_t> packet(packet_data, packet_data + len);
KnxIpTunnelingRequest tunneling(packet.data(), static_cast<uint16_t>(packet.size()));
auto& header = tunneling.connectionHeader();
if (header.length() != LEN_CH) {
ESP_LOGW(kTag, "invalid KNXnet/IP tunnelling header from %s len=%u chLen=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len),
static_cast<unsigned>(header.length()));
return;
}
const uint8_t channel_id = header.channelId();
const uint8_t sequence = header.sequenceCounter();
TunnelClient* client = findTunnelClient(channel_id);
if (client == nullptr) {
ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u from %s: no connection",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
EndpointString(remote).c_str());
sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote);
return;
}
const bool same_tcp_stream = client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock;
if (!same_tcp_stream && !EndpointEquals(remote, client->data_remote)) {
ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u from %s: expected data endpoint %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
EndpointString(remote).c_str(), EndpointString(client->data_remote).c_str());
sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote);
return;
}
CemiFrame& frame = tunneling.frame();
if (!frame.valid()) {
ESP_LOGW(kTag, "invalid OpenKNX tunnel cEMI channel=%u seq=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
EndpointString(remote).c_str());
return;
}
if (frame.messageCode() == L_data_req && frame.sourceAddress() == 0) {
frame.sourceAddress(client->individual_address);
}
const uint8_t* cemi = frame.data();
const size_t cemi_len = frame.dataLength();
const std::vector<uint8_t> current_cemi(cemi, cemi + cemi_len);
const bool is_group_value_write = IsOpenKnxGroupValueWrite(cemi, cemi_len);
const bool duplicate_sequence = sequence == client->received_sequence;
const bool duplicate_payload = duplicate_sequence && client->last_received_cemi == current_cemi;
if (duplicate_payload && !is_group_value_write) {
ESP_LOGD(kTag, "duplicate KNXnet/IP tunnelling request channel=%u seq=%u",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence));
sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote);
if (!client->last_tunnel_confirmation_packet.empty()) {
if (sendPacketToTunnelClient(*client, client->last_tunnel_confirmation_packet)) {
ESP_LOGI(kTag,
"resent cached KNXnet/IP tunnel confirmation channel=%u confirmSeq=%u after duplicate req seq=%u to %s",
static_cast<unsigned>(channel_id),
static_cast<unsigned>(client->last_tunnel_confirmation_sequence),
static_cast<unsigned>(sequence), EndpointString(client->data_remote).c_str());
} else {
ESP_LOGW(kTag,
"failed to resend cached KNXnet/IP tunnel confirmation channel=%u confirmSeq=%u to %s",
static_cast<unsigned>(channel_id),
static_cast<unsigned>(client->last_tunnel_confirmation_sequence),
EndpointString(client->data_remote).c_str());
}
}
return;
}
if (duplicate_payload) {
ESP_LOGI(kTag,
"reprocessing duplicate KNXnet/IP GroupValueWrite channel=%u seq=%u without cached confirmation replay",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence));
} else if (duplicate_sequence) {
ESP_LOGW(kTag,
"accept KNXnet/IP tunnelling request channel=%u with repeated seq=%u because cEMI payload changed",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence));
} else if (static_cast<uint8_t>(sequence - 1) != client->received_sequence) {
ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u expected=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
static_cast<unsigned>(static_cast<uint8_t>(client->received_sequence + 1)),
EndpointString(remote).c_str());
sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote);
return;
}
client->received_sequence = sequence;
client->last_received_cemi = current_cemi;
client->last_activity_tick = xTaskGetTickCount();
sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote);
ESP_LOGI(kTag, "rx KNXnet/IP tunnelling request channel=%u seq=%u cemiLen=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
static_cast<unsigned>(cemi_len), EndpointString(remote).c_str());
const bool consumed_by_openknx = handleOpenKnxTunnelFrame(
cemi, cemi_len, client, kServiceTunnellingRequest);
const bool routed_to_dali = routeOpenKnxGroupWrite(cemi, cemi_len, "KNX tunnel frame");
const bool sent_to_tp = !consumed_by_openknx && !routed_to_dali &&
transmitOpenKnxTpFrame(cemi, cemi_len);
if ((!consumed_by_openknx && routed_to_dali) || sent_to_tp) {
std::vector<uint8_t> tunnel_confirmation;
if (BuildTunnelConfirmationFrame(cemi, cemi_len, &tunnel_confirmation)) {
sendCemiFrameToClient(*client, kServiceTunnellingRequest, tunnel_confirmation.data(),
tunnel_confirmation.size());
}
}
if (consumed_by_openknx || routed_to_dali || sent_to_tp) {
return;
}
ESP_LOGD(kTag, "KNX tunnel frame ignored: no OpenKNX/DALI/TP handler matched");
}
void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* packet_data, size_t len,
const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_CH + 2) {
ESP_LOGW(kTag, "invalid KNXnet/IP device-configuration request from %s len=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len));
return;
}
std::vector<uint8_t> packet(packet_data, packet_data + len);
KnxIpConfigRequest config_request(packet.data(), static_cast<uint16_t>(packet.size()));
auto& header = config_request.connectionHeader();
if (header.length() != LEN_CH) {
ESP_LOGW(kTag, "invalid KNXnet/IP device-configuration header from %s len=%u chLen=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len),
static_cast<unsigned>(header.length()));
return;
}
const uint8_t channel_id = header.channelId();
const uint8_t sequence = header.sequenceCounter();
TunnelClient* client = findTunnelClient(channel_id);
if (client == nullptr) {
ESP_LOGW(kTag, "reject KNXnet/IP device-configuration request channel=%u seq=%u from %s: no connection",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
EndpointString(remote).c_str());
sendDeviceConfigurationAck(channel_id, sequence, kKnxErrorConnectionId, remote);
return;
}
const bool same_tcp_stream = client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock;
if (!same_tcp_stream && !EndpointEquals(remote, client->data_remote)) {
ESP_LOGW(kTag, "reject KNXnet/IP device-configuration request channel=%u seq=%u from %s: expected data endpoint %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
EndpointString(remote).c_str(), EndpointString(client->data_remote).c_str());
sendDeviceConfigurationAck(channel_id, sequence, kKnxErrorConnectionId, remote);
return;
}
client->last_activity_tick = xTaskGetTickCount();
sendDeviceConfigurationAck(channel_id, sequence, kKnxNoError, client->data_remote);
CemiFrame& frame = config_request.frame();
if (!frame.valid()) {
ESP_LOGW(kTag, "invalid OpenKNX device-configuration cEMI channel=%u seq=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
EndpointString(remote).c_str());
return;
}
const uint8_t* cemi = frame.data();
const size_t cemi_len = frame.dataLength();
ESP_LOGI(kTag, "rx KNXnet/IP device-configuration request channel=%u seq=%u cemiLen=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
static_cast<unsigned>(cemi_len), EndpointString(remote).c_str());
if (!handleOpenKnxTunnelFrame(cemi, cemi_len, client, kServiceDeviceConfigurationRequest)) {
ESP_LOGW(kTag, "KNXnet/IP device-configuration cEMI was not consumed by OpenKNX cemiLen=%u",
static_cast<unsigned>(cemi_len));
}
}
void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* packet_data, size_t len,
const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 * LEN_IPHPAI + 2) {
ESP_LOGW(kTag, "invalid KNXnet/IP connect request from %s len=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len));
return;
}
std::vector<uint8_t> packet(packet_data, packet_data + len);
KnxIpConnectRequest request(packet.data(), static_cast<uint16_t>(packet.size()));
auto& control_hpai = request.hpaiCtrl();
auto& data_hpai = request.hpaiData();
if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai()) ||
OpenKnxHpaiUsesUnsupportedProtocol(data_hpai, currentTransportAllowsTcpHpai())) {
ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: unsupported HPAI protocol",
EndpointString(remote).c_str());
sendConnectResponse(0, kKnxErrorConnectionType, remote, kKnxConnectionTypeTunnel, 0);
return;
}
sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote);
sockaddr_in data_remote = EndpointFromOpenKnxHpai(data_hpai, remote);
selectOpenKnxNetworkInterface(control_remote);
auto& cri = request.cri();
const uint8_t cri_length = cri.length();
const uint8_t connection_type = static_cast<uint8_t>(cri.type());
if (cri_length < 2 || kKnxNetIpHeaderSize + 2 * LEN_IPHPAI + cri_length > len) {
ESP_LOGW(kTag, "invalid KNXnet/IP connect CRI from %s len=%u criLen=%u",
EndpointString(remote).c_str(), static_cast<unsigned>(len),
static_cast<unsigned>(cri_length));
sendConnectResponse(0, kKnxErrorConnectionType, control_remote, kKnxConnectionTypeTunnel, 0);
return;
}
if (connection_type != kKnxConnectionTypeTunnel &&
connection_type != kKnxConnectionTypeDeviceManagement) {
ESP_LOGW(kTag, "reject KNXnet/IP connect from %s unsupported type=0x%02x",
EndpointString(remote).c_str(), static_cast<unsigned>(connection_type));
sendConnectResponse(0, kKnxErrorConnectionType, control_remote, connection_type, 0);
return;
}
if (connection_type == kKnxConnectionTypeTunnel &&
(cri_length < 4 || cri.layer() != kKnxTunnelLayerLink)) {
ESP_LOGW(kTag, "reject KNXnet/IP tunnel connect from %s unsupported layer=0x%02x",
EndpointString(remote).c_str(),
static_cast<unsigned>(cri_length >= 3 ? cri.layer() : 0));
sendConnectResponse(0, kKnxErrorTunnellingLayer, control_remote, connection_type, 0);
return;
}
if (!SelectKnxNetifForRemote(control_remote).has_value()) {
ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: no active IPv4 interface for response",
EndpointString(remote).c_str());
sendConnectResponse(0, kKnxErrorConnectionType, control_remote, connection_type, 0);
return;
}
TunnelClient* client = allocateTunnelClient(control_remote, data_remote, connection_type);
if (client == nullptr) {
ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: no free tunnel client slots",
EndpointString(remote).c_str());
sendConnectResponse(0, kKnxErrorNoMoreConnections, control_remote, connection_type, 0);
return;
}
ESP_LOGI(kTag,
"accepted KNXnet/IP connect namespace=%s channel=%u type=0x%02x tunnelPa=0x%04x ctrl=%s data=%s remote=%s active=%u/%u",
openknx_namespace_.c_str(), static_cast<unsigned>(client->channel_id),
static_cast<unsigned>(connection_type), static_cast<unsigned>(client->individual_address),
EndpointString(control_remote).c_str(), EndpointString(data_remote).c_str(),
EndpointString(remote).c_str(),
static_cast<unsigned>(std::count_if(tunnel_clients_.begin(), tunnel_clients_.end(),
[](const TunnelClient& item) {
return item.connected;
})),
static_cast<unsigned>(tunnel_clients_.size()));
sendConnectResponse(client->channel_id, kKnxNoError, control_remote, connection_type,
client->individual_address);
}
void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* packet_data, size_t len,
const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 + LEN_IPHPAI) {
return;
}
std::vector<uint8_t> packet(packet_data, packet_data + len);
KnxIpStateRequest request(packet.data(), static_cast<uint16_t>(packet.size()));
auto& control_hpai = request.hpaiCtrl();
if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai())) {
ESP_LOGW(kTag,
"reject KNXnet/IP connection-state request from %s: unsupported HPAI protocol",
EndpointString(remote).c_str());
return;
}
const uint8_t channel_id = request.channelId();
const sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote);
TunnelClient* client = findTunnelClient(channel_id);
const bool endpoint_matches = client != nullptr &&
((client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock) ||
EndpointEquals(control_remote, client->control_remote));
const uint8_t status = endpoint_matches ? kKnxNoError : kKnxErrorConnectionId;
if (client != nullptr) {
if (endpoint_matches) {
client->last_activity_tick = xTaskGetTickCount();
}
}
ESP_LOGI(kTag, "rx KNXnet/IP connection-state request channel=%u status=0x%02x from %s ctrl=%s expected=%s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(status),
EndpointString(remote).c_str(), EndpointString(control_remote).c_str(),
client == nullptr ? "none" : EndpointString(client->control_remote).c_str());
sendConnectionStateResponse(
channel_id, status, control_remote);
}
void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* packet_data, size_t len,
const sockaddr_in& remote) {
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 + LEN_IPHPAI) {
return;
}
std::vector<uint8_t> packet(packet_data, packet_data + len);
KnxIpDisconnectRequest request(packet.data(), static_cast<uint16_t>(packet.size()));
auto& control_hpai = request.hpaiCtrl();
if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai())) {
ESP_LOGW(kTag, "reject KNXnet/IP disconnect request from %s: unsupported HPAI protocol",
EndpointString(remote).c_str());
return;
}
const uint8_t channel_id = request.channelId();
const sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote);
TunnelClient* client = findTunnelClient(channel_id);
const bool endpoint_matches = client != nullptr &&
((client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock) ||
EndpointEquals(control_remote, client->control_remote));
const uint8_t status = endpoint_matches ? kKnxNoError : kKnxErrorConnectionId;
const std::string expected = client == nullptr ? "none" : EndpointString(client->control_remote);
if (status == kKnxNoError) {
resetTunnelClient(*client);
}
ESP_LOGI(kTag, "rx KNXnet/IP disconnect request channel=%u status=0x%02x from %s ctrl=%s expected=%s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(status),
EndpointString(remote).c_str(), EndpointString(control_remote).c_str(),
expected.c_str());
sendDisconnectResponse(channel_id, status, control_remote);
}
void GatewayKnxTpIpRouter::handleSecureService(uint16_t service, const uint8_t* body,
size_t len, const sockaddr_in& remote) {
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
switch (service) {
case kServiceSecureSessionRequest:
case kServiceSecureSessionAuth:
ESP_LOGW(kTag, "KNXnet/IP Secure service 0x%04x rejected: secure sessions are not provisioned", service);
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
break;
case kServiceSecureWrapper:
ESP_LOGW(kTag, "KNXnet/IP Secure wrapper rejected: no authenticated secure session");
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
break;
case kServiceSecureGroupSync:
ESP_LOGD(kTag, "KNXnet/IP Secure group sync ignored until secure routing is provisioned");
break;
default:
ESP_LOGD(kTag, "KNXnet/IP Secure service 0x%04x ignored", service);
break;
}
#else
(void)service;
(void)body;
(void)len;
(void)remote;
#endif
}
} // namespace gateway
@@ -560,7 +560,7 @@ std::optional<GatewayModbusConfig> GatewayModbusConfigFromValue(const DaliValue*
getObjectInt(json, "unitId").value_or(getObjectInt(json, "unit_id").value_or(1))));
if (const auto* serial_value = getObjectValue(json, "serial")) {
if (const auto* serial = serial_value->asObject()) {
config.serial.uart_port = clampedInt(*serial, "uartPort", config.serial.uart_port, 0, 2);
config.serial.uart_port = clampedInt(*serial, "uartPort", config.serial.uart_port, -1, 2);
config.serial.tx_pin = clampedInt(*serial, "txPin", config.serial.tx_pin, -1, 48);
config.serial.rx_pin = clampedInt(*serial, "rxPin", config.serial.rx_pin, -1, 48);
config.serial.baudrate = clampedU32(*serial, "baudrate", config.serial.baudrate,
@@ -56,8 +56,10 @@ struct GatewayNetworkServiceConfig {
bool status_led_active_high{true};
int boot_button_gpio{-1};
bool boot_button_active_low{true};
int setup_ap_button_gpio{-1};
bool setup_ap_button_active_low{true};
uint32_t boot_button_long_press_ms{3000};
uint32_t boot_button_task_stack_size{2048};
uint32_t boot_button_task_stack_size{8192};
UBaseType_t boot_button_task_priority{2};
uint32_t udp_task_stack_size{4096};
UBaseType_t udp_task_priority{4};
@@ -909,22 +909,37 @@ esp_err_t GatewayNetworkService::configureStatusLed() {
}
esp_err_t GatewayNetworkService::configureBootButton() {
if (config_.boot_button_gpio < 0) {
if (config_.boot_button_gpio < 0 && config_.setup_ap_button_gpio < 0) {
return ESP_OK;
}
gpio_config_t io_config = {};
io_config.pin_bit_mask = 1ULL << static_cast<uint32_t>(config_.boot_button_gpio);
io_config.mode = GPIO_MODE_INPUT;
io_config.pull_up_en = config_.boot_button_active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE;
io_config.pull_down_en = config_.boot_button_active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE;
io_config.intr_type = GPIO_INTR_DISABLE;
const esp_err_t err = gpio_config(&io_config);
const auto configure_input = [](int gpio, bool active_low, const char* name) -> esp_err_t {
if (gpio < 0) {
return ESP_OK;
}
gpio_config_t io_config = {};
io_config.pin_bit_mask = 1ULL << static_cast<uint32_t>(gpio);
io_config.mode = GPIO_MODE_INPUT;
io_config.pull_up_en = active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE;
io_config.pull_down_en = active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE;
io_config.intr_type = GPIO_INTR_DISABLE;
const esp_err_t err = gpio_config(&io_config);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to configure %s GPIO%d: %s", name, gpio, esp_err_to_name(err));
}
return err;
};
esp_err_t err = configure_input(config_.boot_button_gpio, config_.boot_button_active_low,
"Wi-Fi reset button");
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to configure boot button GPIO%d: %s", config_.boot_button_gpio,
esp_err_to_name(err));
return err;
}
return err;
if (config_.setup_ap_button_gpio == config_.boot_button_gpio) {
return ESP_OK;
}
return configure_input(config_.setup_ap_button_gpio, config_.setup_ap_button_active_low,
"setup AP button");
}
esp_err_t GatewayNetworkService::startHttpServer() {
@@ -985,7 +1000,8 @@ esp_err_t GatewayNetworkService::startUdpTask() {
}
esp_err_t GatewayNetworkService::startBootButtonTask() {
if (config_.boot_button_gpio < 0 || boot_button_task_handle_ != nullptr) {
if ((config_.boot_button_gpio < 0 && config_.setup_ap_button_gpio < 0) ||
boot_button_task_handle_ != nullptr) {
return ESP_OK;
}
@@ -1349,39 +1365,88 @@ void GatewayNetworkService::bootButtonTaskLoop() {
const TickType_t poll_ticks = pdMS_TO_TICKS(100);
const uint32_t long_press_ms = std::max<uint32_t>(config_.boot_button_long_press_ms, 100);
auto is_pressed = [this]() {
const int level = gpio_get_level(static_cast<gpio_num_t>(config_.boot_button_gpio));
return config_.boot_button_active_low ? level == 0 : level != 0;
auto is_pressed = [](int gpio, bool active_low) {
if (gpio < 0) {
return false;
}
const int level = gpio_get_level(static_cast<gpio_num_t>(gpio));
return active_low ? level == 0 : level != 0;
};
while (true) {
if (!is_pressed()) {
vTaskDelay(poll_ticks);
continue;
}
auto wait_release = [&](int gpio, bool active_low) {
uint32_t pressed_ms = 0;
while (is_pressed()) {
while (is_pressed(gpio, active_low)) {
vTaskDelay(poll_ticks);
pressed_ms += 100;
}
return pressed_ms;
};
if (pressed_ms >= long_press_ms) {
ESP_LOGW(kTag, "BOOT long press clears Wi-Fi credentials and restarts");
runtime_.clearWirelessInfo();
stopEspNow();
if (wifi_started_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect());
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop());
auto enter_setup_ap = [this]() {
ESP_LOGI(kTag, "setup AP button enters setup AP mode");
const uint32_t stack_size = std::max<uint32_t>(config_.boot_button_task_stack_size, 8192);
const BaseType_t created = xTaskCreate(
[](void* arg) {
auto* service = static_cast<GatewayNetworkService*>(arg);
service->handleWifiControl(101);
vTaskDelete(nullptr);
},
"gateway_setup_ap", stack_size, this, config_.boot_button_task_priority, nullptr);
if (created != pdPASS) {
ESP_LOGE(kTag, "failed to create setup AP task");
}
};
auto clear_wifi_and_restart = [this]() {
ESP_LOGW(kTag, "Wi-Fi reset button clears Wi-Fi credentials and restarts");
runtime_.clearWirelessInfo();
stopEspNow();
if (wifi_started_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_disconnect());
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop());
}
vTaskDelay(pdMS_TO_TICKS(300));
esp_restart();
};
while (true) {
const bool same_button = config_.boot_button_gpio >= 0 &&
config_.boot_button_gpio == config_.setup_ap_button_gpio;
if (same_button && is_pressed(config_.boot_button_gpio, config_.boot_button_active_low)) {
const uint32_t pressed_ms = wait_release(config_.boot_button_gpio,
config_.boot_button_active_low);
if (pressed_ms >= long_press_ms) {
clear_wifi_and_restart();
} else {
enter_setup_ap();
}
vTaskDelay(pdMS_TO_TICKS(300));
esp_restart();
} else {
ESP_LOGI(kTag, "BOOT short press enters setup AP mode");
handleWifiControl(101);
continue;
}
vTaskDelay(pdMS_TO_TICKS(300));
if (is_pressed(config_.setup_ap_button_gpio, config_.setup_ap_button_active_low)) {
wait_release(config_.setup_ap_button_gpio, config_.setup_ap_button_active_low);
enter_setup_ap();
vTaskDelay(pdMS_TO_TICKS(300));
continue;
}
if (is_pressed(config_.boot_button_gpio, config_.boot_button_active_low)) {
const uint32_t pressed_ms = wait_release(config_.boot_button_gpio,
config_.boot_button_active_low);
if (pressed_ms >= long_press_ms) {
clear_wifi_and_restart();
}
vTaskDelay(pdMS_TO_TICKS(300));
continue;
}
if (config_.setup_ap_button_gpio < 0 && config_.boot_button_gpio < 0) {
vTaskDelete(nullptr);
return;
}
vTaskDelay(poll_ticks);
}
}
@@ -12,6 +12,13 @@ namespace gateway {
namespace {
constexpr const char* kTag = "gateway_usb";
constexpr size_t kCommandFrameMinLen = 7;
std::vector<uint8_t> LegacyRawPayload(const std::vector<uint8_t>& data) {
if (data.size() == 1) {
return {0xBE, data[0]};
}
return data;
}
}
GatewayUsbSetupBridge::GatewayUsbSetupBridge(GatewayController& controller,
@@ -82,6 +89,19 @@ void GatewayUsbSetupBridge::handleBytes(const uint8_t* data, size_t len) {
}
const uint8_t gateway_id = setupGatewayId();
if (data[0] == 0x12) {
const auto response = dali_domain_.transactBridgeFrame(gateway_id, data, len);
if (!response.empty()) {
const int written = usb_serial_jtag_write_bytes(response.data(), response.size(),
pdMS_TO_TICKS(config_.write_timeout_ms));
if (written < 0 || static_cast<size_t>(written) != response.size()) {
ESP_LOGW(kTag, "failed to write USB raw query response channel=%u len=%u",
config_.channel_index, static_cast<unsigned>(response.size()));
}
}
return;
}
if (!dali_domain_.writeBridgeFrame(gateway_id, data, len)) {
ESP_LOGW(kTag, "failed to write USB raw setup frame channel=%u len=%u", config_.channel_index,
static_cast<unsigned>(len));
@@ -93,11 +113,12 @@ void GatewayUsbSetupBridge::handleRawFrame(const DaliRawFrame& frame) {
return;
}
const int written = usb_serial_jtag_write_bytes(frame.data.data(), frame.data.size(),
const auto payload = LegacyRawPayload(frame.data);
const int written = usb_serial_jtag_write_bytes(payload.data(), payload.size(),
pdMS_TO_TICKS(config_.write_timeout_ms));
if (written < 0 || static_cast<size_t>(written) != frame.data.size()) {
if (written < 0 || static_cast<size_t>(written) != payload.size()) {
ESP_LOGW(kTag, "failed to forward USB raw setup frame channel=%u len=%u", frame.channel_index,
static_cast<unsigned>(frame.data.size()));
static_cast<unsigned>(payload.size()));
}
}
+24
View File
@@ -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)
@@ -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 <cstdint>
#include <optional>
#include <vector>
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<uint8_t> queryRaw(DaliTarget target, uint8_t command) const;
// ---- Brightness (arc power level) ----
bool setArc(DaliTarget target, uint8_t arc) const;
std::optional<uint8_t> 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<uint8_t> queryStatus(int short_address) const;
std::optional<uint8_t> queryDeviceType(int short_address) const;
std::optional<uint8_t> queryMinLevel(int short_address) const;
std::optional<uint8_t> queryMaxLevel(int short_address) const;
std::optional<uint8_t> queryPowerOnLevel(int short_address) const;
std::optional<uint8_t> querySystemFailureLevel(int short_address) const;
std::optional<uint8_t> queryFadeTimeRate(int short_address) const;
std::optional<uint8_t> queryFadeTime(int short_address) const;
std::optional<uint16_t> queryGroups(int short_address) const;
std::optional<uint8_t> 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<DaliDomainSnapshot> dt8StatusSnapshot(int short_address) const;
std::optional<DaliDomainSnapshot> 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<uint8_t> 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
@@ -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 <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
// 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> impl_;
};
} // namespace knx_dali_gw
} // namespace gateway
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include "sdkconfig.h"
#ifndef CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID
#define CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID 0x00A4
#endif
#ifndef CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER
#define CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER 0x0001
#endif
#ifndef CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION
#define CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION 0x08
#endif
// 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 (CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID & 0xff)
#define MAIN_ApplicationNumber CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER
#define MAIN_ApplicationVersion CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION
#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_DT2 = 3,
PT_deviceType_DT3 = 4,
PT_deviceType_DT4 = 5,
PT_deviceType_DT5 = 6,
PT_deviceType_DT6 = 7,
PT_deviceType_DT7 = 8,
PT_deviceType_DT8 = 9,
};
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 = 1,
PT_colorSpace_xy = 0,
};
// 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)
// Generated group object layout constants
#define ADR_KoOffset 12
#define GRP_KoOffset 1164
#define HCL_KoOffset 1436
#define ADR_KoBlockSize 18
#define GRP_KoBlockSize 17
#define HCL_KoBlockSize 1
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <cstdint>
struct Ballast {
uint8_t high{0};
uint8_t middle{0};
uint8_t low{0};
uint8_t address{255};
};
+138
View File
@@ -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;
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
#include <cmath>
#include <algorithm>
//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);
};
@@ -0,0 +1,323 @@
#include "dali_gateway_bridge.h"
#include "dali_define.hpp"
#include "dali_comm.hpp"
#include <algorithm>
#include <cmath>
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<uint8_t>((addr << 1) | kShortAddrCmdBase);
}
uint8_t makeShortArcAddr(int addr) {
return static_cast<uint8_t>((addr << 1) | kShortAddrArcBase);
}
uint8_t makeGroupCmdAddr(int group) {
return static_cast<uint8_t>(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<double>(arc) / kArcMax, kLogFactor);
}
uint8_t PercentToArc(double percent) {
if (percent <= 0.0) return 0;
if (percent >= 100.0) return 254;
return static_cast<uint8_t>(
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<uint8_t> 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<uint8_t> 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<uint8_t> DaliGatewayBridge::queryStatus(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_STATUS);
}
std::optional<uint8_t> DaliGatewayBridge::queryDeviceType(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_DEVICE_TYPE);
}
std::optional<uint8_t> DaliGatewayBridge::queryMinLevel(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_MIN_LEVEL);
}
std::optional<uint8_t> DaliGatewayBridge::queryMaxLevel(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_MAX_LEVEL);
}
std::optional<uint8_t> DaliGatewayBridge::queryPowerOnLevel(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_POWER_ON_LEVEL);
}
std::optional<uint8_t> DaliGatewayBridge::querySystemFailureLevel(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_SYSTEM_FAILURE_LEVEL);
}
std::optional<uint8_t> DaliGatewayBridge::queryFadeTimeRate(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_FADE_TIME_FADE_RATE);
}
std::optional<uint8_t> DaliGatewayBridge::queryFadeTime(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_EXTENDED_FADE_TIME);
}
std::optional<uint16_t> DaliGatewayBridge::queryGroups(int short_address) const {
return dali_.queryGroupMask(gateway_id_, short_address);
}
std::optional<uint8_t> 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<DaliDomainSnapshot> DaliGatewayBridge::dt8StatusSnapshot(
int short_address) const {
return dali_.dt8StatusSnapshot(gateway_id_, short_address);
}
std::optional<DaliDomainSnapshot> 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<uint8_t> 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
@@ -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;
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <cstdint>
#include <cmath>
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);
};
+15
View File
@@ -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
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
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
@@ -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<int>(index_)}
: DaliTarget{DaliTargetKind::kShortAddress, static_cast<int>(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<int>(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<int>(index_)}
: DaliTarget{DaliTargetKind::kShortAddress, static_cast<int>(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<int>(index_)}
: DaliTarget{DaliTargetKind::kShortAddress, static_cast<int>(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<int>(index_)}
: DaliTarget{DaliTargetKind::kShortAddress, static_cast<int>(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<int>(index_), current_color_[0],
current_color_[1], current_color_[2]);
}
// ---- KO Handlers ----
void KnxDaliChannel::koHandleSwitch(GroupObject& ko) {
bool on = static_cast<bool>(ko.value());
setSwitchState(on);
}
void KnxDaliChannel::koHandleDimmRel(GroupObject& ko) {
int step = static_cast<int>(static_cast<float>(ko.value()));
if (step > 0) {
dimm_direction_ = DimmDirection::kUp;
dimm_step_ = static_cast<uint8_t>(step);
} else if (step < 0) {
dimm_direction_ = DimmDirection::kDown;
dimm_step_ = static_cast<uint8_t>(-step);
} else {
dimm_direction_ = DimmDirection::kNone;
}
dimm_last_ = esp_timer_get_time() / 1000ULL;
}
void KnxDaliChannel::koHandleDimmAbs(GroupObject& ko) {
uint8_t value = static_cast<uint8_t>(static_cast<float>(ko.value()) * 255.0f / 100.0f);
setDimmState(value);
dimm_direction_ = DimmDirection::kNone;
DaliTarget target = is_group_ ? DaliTarget{DaliTargetKind::kGroup, static_cast<int>(index_)}
: DaliTarget{DaliTargetKind::kShortAddress, static_cast<int>(index_)};
dali_->setArc(target, value);
}
void KnxDaliChannel::koHandleLock(GroupObject& ko) {
bool lock = static_cast<bool>(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
@@ -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 <cstdint>
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
+141
View File
@@ -0,0 +1,141 @@
#include "knx_dali_gw.hpp"
#include "knx/bau07B0.h"
#include "sdkconfig.h"
#include "esp_log.h"
namespace gateway {
namespace knx_dali_gw {
namespace {
constexpr const char* kTag = "knx_dali_gw";
#ifndef CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID
#define CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID 0x00A4
#endif
#ifndef CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER
#define CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER 0x0001
#endif
#ifndef CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION
#define CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION 0x08
#endif
constexpr uint16_t kKnxOemManufacturerId =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID);
constexpr uint16_t kKnxOemApplicationNumber =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OEM_APPLICATION_NUMBER);
constexpr uint8_t kKnxOemApplicationVersion =
static_cast<uint8_t>(CONFIG_GATEWAY_KNX_OEM_APPLICATION_VERSION);
constexpr uint8_t kKnxOemHardwareType[6] = {
0x00,
0x00,
static_cast<uint8_t>(kKnxOemManufacturerId & 0xff),
static_cast<uint8_t>(kKnxOemApplicationNumber & 0xff),
kKnxOemApplicationVersion,
0x00};
constexpr uint8_t kKnxOemProgramVersion[5] = {
static_cast<uint8_t>((kKnxOemManufacturerId >> 8) & 0xff),
static_cast<uint8_t>(kKnxOemManufacturerId & 0xff),
static_cast<uint8_t>((kKnxOemApplicationNumber >> 8) & 0xff),
static_cast<uint8_t>(kKnxOemApplicationNumber & 0xff),
kKnxOemApplicationVersion};
} // 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(kKnxOemManufacturerId);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
device.deviceObject().hardwareType(kKnxOemHardwareType);
const uint8_t order_number[10] = {
'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0};
device.deviceObject().orderNumber(order_number);
device.parameters().property(PID_PROG_VERSION)->write(kKnxOemProgramVersion);
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<Impl>(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
@@ -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 <algorithm>
#include <cstdio>
#include <cstring>
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<uint8_t>(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<int>(msg.para1)}; break;
case 1: target = {DaliTargetKind::kGroup, static_cast<int>(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<uint8_t>((adr_search_ >> 16) & 0xFF));
adr_state_ = AddressingState::kSearchMid;
break;
case AddressingState::kSearchMid:
dali_->searchAddrM(static_cast<uint8_t>((adr_search_ >> 8) & 0xFF));
adr_state_ = AddressingState::kSearchLow;
break;
case AddressingState::kSearchLow:
dali_->searchAddrL(static_cast<uint8_t>(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<int64_t>(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<uint8_t>((adr_search_ >> 16) & 0xFF);
ballasts_[adr_found_].middle = static_cast<uint8_t>((adr_search_ >> 8) & 0xFF);
ballasts_[adr_found_].low = static_cast<uint8_t>(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<uint8_t>(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<uint8_t>((adr_search_ >> 16) & 0xFF));
ass_state_ = AssigningState::kSearchMid;
break;
case AssigningState::kSearchMid:
dali_->searchAddrM(static_cast<uint8_t>((adr_search_ >> 8) & 0xFF));
ass_state_ = AssigningState::kSearchLow;
break;
case AssigningState::kSearchLow:
dali_->searchAddrL(static_cast<uint8_t>(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<int>(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<int>(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<uint8_t>(grp_idx);
}
return;
}
}
// HCL KOs
int hcl_relative = static_cast<int>(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<uint16_t>(static_cast<float>(val));
for (int i = 0; i < 64; i++) {
channels_[i].setHcl(static_cast<uint8_t>(curve_idx), kelvin, 255);
}
for (int i = 0; i < 16; i++) {
groups_[i].setHcl(static_cast<uint8_t>(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<uint64_t>(data[1]) << 16) |
(static_cast<uint64_t>(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<uint8_t>(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<bool>(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<uint8_t>(static_cast<float>(val) * 255.0f / 100.0f);
uint8_t arc = PercentToArc(static_cast<double>(percent) * 100.0 / 255.0);
if (dali_ != nullptr) {
dali_->setArc({DaliTargetKind::kBroadcast, 0}, arc);
}
}
void KnxDaliModule::koHandleDayNight(GroupObject& ko) {
is_night_ = static_cast<bool>(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<uint8_t>(static_cast<float>(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<uint8_t>(ko.value());
if (dali_ != nullptr) {
dali_->goToScene({DaliTargetKind::kBroadcast, 0}, scene);
}
}
} // namespace knx_dali_gw
} // namespace gateway
@@ -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 <cstdint>
#include <functional>
#include <string>
#include <vector>
// 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
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
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};
};
@@ -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];
}
@@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
#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;
};
-78
View File
@@ -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)
-59
View File
@@ -1,59 +0,0 @@
#pragma once
#include <stdint.h>
#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);
@@ -1,66 +0,0 @@
#pragma once
#include "knx/platform.h"
#include "esp_netif.h"
#include "lwip/sockets.h"
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
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<uint8_t> eeprom_;
std::string nvs_namespace_;
bool eeprom_loaded_{false};
OutboundCemiFrameCallback outbound_cemi_frame_callback_{nullptr};
void* outbound_cemi_frame_context_{nullptr};
};
} // namespace gateway::openknx
@@ -1,71 +0,0 @@
#pragma once
#include "openknx_idf/esp_idf_platform.h"
#include "openknx_idf/ets_memory_loader.h"
#include "knx/bau07B0.h"
#include "knx/cemi_frame.h"
#include <cstddef>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace gateway::openknx {
class EtsDeviceRuntime {
public:
using CemiFrameSender = std::function<void(const uint8_t* data, size_t len)>;
using GroupWriteHandler = std::function<void(uint16_t group_address, const uint8_t* data,
size_t len)>;
using FunctionPropertyHandler = std::function<bool(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response)>;
EtsDeviceRuntime(std::string nvs_namespace, uint16_t fallback_individual_address);
~EtsDeviceRuntime();
uint16_t individualAddress() const;
uint16_t tunnelClientAddress() const;
bool configured() const;
EtsMemorySnapshot snapshot() const;
void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler,
FunctionPropertyHandler state_handler);
void setGroupWriteHandler(GroupWriteHandler handler);
bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender);
bool handleBusFrame(const uint8_t* data, size_t len);
bool emitGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len,
CemiFrameSender sender);
void loop();
private:
static bool HandleOutboundCemiFrame(CemiFrame& frame, void* context);
static void EmitTunnelFrame(CemiFrame& frame, void* context);
static void HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data,
uint8_t data_length, void* context);
static bool HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length);
static bool HandleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length);
static uint16_t DefaultTunnelClientAddress(uint16_t individual_address);
static bool DispatchFunctionProperty(FunctionPropertyHandler* handler, uint8_t object_index,
uint8_t property_id, uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length);
bool shouldConsumeTunnelFrame(CemiFrame& frame) const;
bool shouldConsumeBusFrame(CemiFrame& frame) const;
std::string nvs_namespace_;
EspIdfPlatform platform_;
Bau07B0 device_;
CemiFrameSender sender_;
GroupWriteHandler group_write_handler_;
FunctionPropertyHandler command_handler_;
FunctionPropertyHandler state_handler_;
};
} // namespace gateway::openknx
@@ -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<EspIdfPlatform, Bau07B0>;
} // namespace gateway::openknx
@@ -1,19 +0,0 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
namespace gateway::openknx {
struct FactoryFdskInfo {
bool available{false};
std::string serialNumber;
std::string label;
std::string qrCode;
};
bool LoadFactoryFdsk(uint8_t* data, size_t len);
FactoryFdskInfo LoadFactoryFdskInfo();
} // namespace gateway::openknx
@@ -1,41 +0,0 @@
#pragma once
#include "TPUart/Interface/Abstract.h"
#include "driver/uart.h"
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <functional>
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);
~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<bool()> callback) override;
private:
uart_port_t uart_port_;
int tx_pin_;
int rx_pin_;
size_t rx_buffer_size_;
size_t tx_buffer_size_;
std::atomic_bool overflow_{false};
std::function<bool()> callback_;
};
} // namespace gateway::openknx
@@ -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 <array>
#include <cstdio>
namespace {
std::array<voidFuncPtr, GPIO_NUM_MAX> g_gpio_callbacks{};
bool g_isr_service_installed = false;
void IRAM_ATTR gpioIsrThunk(void* arg) {
const auto pin = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(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<unsigned long long>(value));
} else {
std::printf("%lld", value);
}
}
} // namespace
uint32_t millis() { return static_cast<uint32_t>(esp_timer_get_time() / 1000ULL); }
uint32_t micros() { return static_cast<uint32_t>(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<gpio_num_t>(pin), value == LOW ? 0 : 1);
}
}
uint32_t digitalRead(uint32_t pin) {
if (pin >= GPIO_NUM_MAX) {
return LOW;
}
return gpio_get_level(static_cast<gpio_num_t>(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<gpio_num_t>(pin), toGpioInterrupt(mode));
gpio_isr_handler_remove(static_cast<gpio_num_t>(pin));
g_gpio_callbacks[pin] = callback;
if (callback != nullptr) {
gpio_isr_handler_add(static_cast<gpio_num_t>(pin), gpioIsrThunk,
reinterpret_cast<void*>(static_cast<uintptr_t>(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"); }
@@ -1,286 +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 <algorithm>
#include <cerrno>
#include <cstring>
#include <unistd.h>
namespace gateway::openknx {
namespace {
constexpr const char* kTag = "openknx_idf";
constexpr const char* kEepromKey = "eeprom";
esp_netif_t* findDefaultNetif() {
if (auto* sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF")) {
return sta;
}
if (auto* eth = esp_netif_get_handle_from_ifkey("ETH_DEF")) {
return eth;
}
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 (esp_read_mac(data, ESP_MAC_WIFI_STA) != ESP_OK) {
std::memset(data, 0, 6);
}
}
uint32_t EspIdfPlatform::uniqueSerialNumber() {
uint8_t mac[6]{};
macAddress(mac);
return (static_cast<uint32_t>(mac[0]) << 24) | (static_cast<uint32_t>(mac[1]) << 16) |
(static_cast<uint32_t>(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_IP);
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;
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind_addr.sin_port = htons(port);
if (bind(udp_sock_, reinterpret_cast<sockaddr*>(&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 = currentIpAddress();
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);
}
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<sockaddr*>(&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<sockaddr*>(&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<sockaddr*>(&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<uint8_t> 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
@@ -1,315 +0,0 @@
#include "openknx_idf/ets_device_runtime.h"
#include "knx/cemi_server.h"
#include "knx/secure_application_layer.h"
#include "knx/property.h"
#include <algorithm>
#include <cstdint>
#include <utility>
#include <vector>
namespace gateway::openknx {
namespace {
thread_local EtsDeviceRuntime* active_function_property_runtime = nullptr;
class ActiveFunctionPropertyRuntimeScope {
public:
explicit ActiveFunctionPropertyRuntimeScope(EtsDeviceRuntime* runtime)
: previous_(active_function_property_runtime) {
active_function_property_runtime = runtime;
}
~ActiveFunctionPropertyRuntimeScope() { active_function_property_runtime = previous_; }
private:
EtsDeviceRuntime* previous_;
};
constexpr uint16_t kInvalidIndividualAddress = 0xffff;
constexpr uint16_t kReg1DaliManufacturerId = 0x00a4;
constexpr uint8_t kReg1DaliApplicationNumber = 0x01;
constexpr uint8_t kReg1DaliApplicationVersion = 0x05;
constexpr uint8_t kReg1DaliOrderNumber[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0};
bool IsUsableIndividualAddress(uint16_t address) {
return address != 0 && address != kInvalidIndividualAddress;
}
void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) {
device.deviceObject().manufacturerId(kReg1DaliManufacturerId);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
device.deviceObject().orderNumber(kReg1DaliOrderNumber);
const uint8_t program_version[5] = {0x00, 0xa4, 0x00, kReg1DaliApplicationNumber,
kReg1DaliApplicationVersion};
device.parameters().property(PID_PROG_VERSION)->write(program_version);
}
} // namespace
EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace,
uint16_t fallback_individual_address)
: nvs_namespace_(std::move(nvs_namespace)),
platform_(nullptr, nvs_namespace_.c_str()),
device_(platform_) {
platform_.outboundCemiFrameCallback(&EtsDeviceRuntime::HandleOutboundCemiFrame, this);
ApplyReg1DaliIdentity(device_, platform_);
if (IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
}
device_.readMemory();
if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) &&
IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
}
if (auto* server = device_.getCemiServer()) {
server->clientAddress(DefaultTunnelClientAddress(device_.deviceObject().individualAddress()));
server->tunnelFrameCallback(&EtsDeviceRuntime::EmitTunnelFrame, this);
}
device_.functionPropertyCallback(&EtsDeviceRuntime::HandleFunctionPropertyCommand);
device_.functionPropertyStateCallback(&EtsDeviceRuntime::HandleFunctionPropertyState);
#ifdef USE_DATASECURE
device_.secureGroupWriteCallback(&EtsDeviceRuntime::HandleSecureGroupWrite, this);
#endif
}
EtsDeviceRuntime::~EtsDeviceRuntime() {
platform_.outboundCemiFrameCallback(nullptr, nullptr);
#ifdef USE_DATASECURE
device_.secureGroupWriteCallback(nullptr, nullptr);
#endif
device_.functionPropertyCallback(nullptr);
device_.functionPropertyStateCallback(nullptr);
if (auto* server = device_.getCemiServer()) {
server->tunnelFrameCallback(nullptr, nullptr);
}
}
uint16_t EtsDeviceRuntime::individualAddress() const {
return const_cast<Bau07B0&>(device_).deviceObject().individualAddress();
}
uint16_t EtsDeviceRuntime::tunnelClientAddress() const {
if (auto* server = const_cast<Bau07B0&>(device_).getCemiServer()) {
return server->clientAddress();
}
return DefaultTunnelClientAddress(individualAddress());
}
bool EtsDeviceRuntime::configured() const { return const_cast<Bau07B0&>(device_).configured(); }
EtsMemorySnapshot EtsDeviceRuntime::snapshot() const {
EtsMemorySnapshot out;
auto& device = const_cast<Bau07B0&>(device_);
out.configured = device.configured();
out.individual_address = device.deviceObject().individualAddress();
device.forEachEtsAssociation(
[](uint16_t group_address, uint16_t group_object_number, void* context) {
auto* associations = static_cast<std::vector<EtsAssociation>*>(context);
if (associations != nullptr) {
associations->push_back(EtsAssociation{group_address, group_object_number});
}
},
&out.associations);
std::sort(out.associations.begin(), out.associations.end(),
[](const EtsAssociation& lhs, const EtsAssociation& rhs) {
if (lhs.group_address != rhs.group_address) {
return lhs.group_address < rhs.group_address;
}
return lhs.group_object_number < rhs.group_object_number;
});
out.associations.erase(
std::unique(out.associations.begin(), out.associations.end(),
[](const EtsAssociation& lhs, const EtsAssociation& rhs) {
return lhs.group_address == rhs.group_address &&
lhs.group_object_number == rhs.group_object_number;
}),
out.associations.end());
return out;
}
void EtsDeviceRuntime::setFunctionPropertyHandlers(FunctionPropertyHandler command_handler,
FunctionPropertyHandler state_handler) {
command_handler_ = std::move(command_handler);
state_handler_ = std::move(state_handler);
}
void EtsDeviceRuntime::setGroupWriteHandler(GroupWriteHandler handler) {
group_write_handler_ = std::move(handler);
}
bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len,
CemiFrameSender sender) {
auto* server = device_.getCemiServer();
if (server == nullptr || data == nullptr || len < 2) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
const bool consumed = shouldConsumeTunnelFrame(frame);
if (!consumed) {
return false;
}
sender_ = std::move(sender);
ActiveFunctionPropertyRuntimeScope callback_scope(this);
server->frameReceived(frame);
loop();
sender_ = nullptr;
return consumed;
}
bool EtsDeviceRuntime::handleBusFrame(const uint8_t* data, size_t len) {
auto* data_link_layer = device_.getDataLinkLayer();
if (data_link_layer == nullptr || data == nullptr || len < 2) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
const bool consumed = shouldConsumeBusFrame(frame);
if (!consumed) {
return false;
}
data_link_layer->externalFrameReceived(frame);
loop();
return consumed;
}
bool EtsDeviceRuntime::emitGroupValue(uint16_t group_object_number, const uint8_t* data,
size_t len, CemiFrameSender sender) {
if (group_object_number == 0 || data == nullptr || !sender || !device_.configured()) {
return false;
}
auto& table = device_.groupObjectTable();
if (group_object_number > table.entryCount()) {
return false;
}
auto& group_object = table.get(group_object_number);
if (len != group_object.valueSize() || group_object.valueRef() == nullptr) {
return false;
}
if (group_object.sizeInTelegram() == 0) {
group_object.valueRef()[0] = data[0] & 0x01;
} else {
std::copy_n(data, len, group_object.valueRef());
}
sender_ = std::move(sender);
group_object.objectWritten();
loop();
sender_ = nullptr;
return true;
}
void EtsDeviceRuntime::loop() { device_.loop(); }
bool EtsDeviceRuntime::HandleOutboundCemiFrame(CemiFrame& frame, void* context) {
auto* self = static_cast<EtsDeviceRuntime*>(context);
if (self == nullptr || !self->sender_) {
return false;
}
self->sender_(frame.data(), frame.dataLength());
return true;
}
void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) {
auto* self = static_cast<EtsDeviceRuntime*>(context);
if (self == nullptr || !self->sender_) {
return;
}
self->sender_(frame.data(), frame.dataLength());
}
void EtsDeviceRuntime::HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data,
uint8_t data_length, void* context) {
auto* self = static_cast<EtsDeviceRuntime*>(context);
if (self == nullptr || !self->group_write_handler_) {
return;
}
self->group_write_handler_(group_address, data, data_length);
}
bool EtsDeviceRuntime::HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data,
uint8_t& result_length) {
if (active_function_property_runtime == nullptr) {
return false;
}
return DispatchFunctionProperty(&active_function_property_runtime->command_handler_, object_index,
property_id, length, data, result_data, result_length);
}
bool EtsDeviceRuntime::HandleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data,
uint8_t& result_length) {
if (active_function_property_runtime == nullptr) {
return false;
}
return DispatchFunctionProperty(&active_function_property_runtime->state_handler_, object_index,
property_id, length, data, result_data, result_length);
}
bool EtsDeviceRuntime::DispatchFunctionProperty(FunctionPropertyHandler* handler,
uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length) {
if (handler == nullptr || !*handler || result_data == nullptr) {
return false;
}
std::vector<uint8_t> response;
if (!(*handler)(object_index, property_id, data, length, &response)) {
return false;
}
result_length = static_cast<uint8_t>(std::min<size_t>(response.size(), result_length));
if (result_length > 0) {
std::copy_n(response.begin(), result_length, result_data);
}
return true;
}
uint16_t EtsDeviceRuntime::DefaultTunnelClientAddress(uint16_t individual_address) {
if (!IsUsableIndividualAddress(individual_address)) {
return 0x1101;
}
const uint16_t line_base = individual_address & 0xff00;
uint16_t device = static_cast<uint16_t>((individual_address & 0x00ff) + 1);
if (device == 0 || device > 0xff) {
device = 1;
}
return static_cast<uint16_t>(line_base | device);
}
bool EtsDeviceRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const {
switch (frame.messageCode()) {
case M_PropRead_req:
case M_PropWrite_req:
case M_Reset_req:
case M_FuncPropCommand_req:
case M_FuncPropStateRead_req:
return true;
case L_data_req:
if (frame.addressType() == IndividualAddress &&
frame.destinationAddress() == individualAddress()) {
return true;
}
#ifdef USE_DATASECURE
return frame.addressType() == GroupAddress && frame.apdu().type() == SecureService;
#else
return false;
#endif
default:
return false;
}
}
bool EtsDeviceRuntime::shouldConsumeBusFrame(CemiFrame& frame) const {
#ifdef USE_DATASECURE
return frame.messageCode() == L_data_ind && frame.addressType() == GroupAddress &&
frame.apdu().type() == SecureService;
#else
return false;
#endif
}
} // namespace gateway::openknx
@@ -1,181 +0,0 @@
#include "openknx_idf/security_storage.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_random.h"
#include "nvs.h"
#include "nvs_flash.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
namespace {
constexpr const char* kTag = "openknx_sec";
constexpr const char* kNamespace = "knx_sec";
constexpr const char* kFactoryFdskKey = "factory_fdsk";
constexpr size_t kFdskSize = 16;
constexpr size_t kSerialSize = 6;
constexpr size_t kFdskQrSize = 36;
constexpr uint8_t kCrc4Tab[16] = {
0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9,
0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2,
};
constexpr char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
constexpr char kHexAlphabet[] = "0123456789ABCDEF";
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;
}
bool plausibleKey(const uint8_t* data) {
const bool all_zero = std::all_of(data, data + kFdskSize, [](uint8_t value) {
return value == 0x00;
});
const bool all_ff = std::all_of(data, data + kFdskSize, [](uint8_t value) {
return value == 0xff;
});
return !all_zero && !all_ff;
}
void generateKey(uint8_t* data) {
do {
esp_fill_random(data, kFdskSize);
} while (!plausibleKey(data));
}
uint8_t crc4Array(const uint8_t* data, size_t len) {
uint8_t crc = 0;
for (size_t i = 0; i < len; ++i) {
crc = kCrc4Tab[crc ^ (data[i] >> 4)];
crc = kCrc4Tab[crc ^ (data[i] & 0x0f)];
}
return crc;
}
std::string toBase32NoPadding(const uint8_t* data, size_t len) {
std::string result;
result.reserve(((len * 8) + 4) / 5);
uint32_t buffer = 0;
int bits_left = 0;
for (size_t i = 0; i < len; ++i) {
buffer = (buffer << 8) | data[i];
bits_left += 8;
while (bits_left >= 5) {
const uint8_t index = static_cast<uint8_t>((buffer >> (bits_left - 5)) & 0x1f);
result.push_back(kBase32Alphabet[index]);
bits_left -= 5;
}
}
if (bits_left > 0) {
const uint8_t index = static_cast<uint8_t>((buffer << (5 - bits_left)) & 0x1f);
result.push_back(kBase32Alphabet[index]);
}
return result;
}
std::string toHex(const uint8_t* data, size_t len) {
std::string result;
result.reserve(len * 2);
for (size_t i = 0; i < len; ++i) {
result.push_back(kHexAlphabet[(data[i] >> 4) & 0x0f]);
result.push_back(kHexAlphabet[data[i] & 0x0f]);
}
return result;
}
std::string generateFdskQrCode(const uint8_t* serial, const uint8_t* key) {
std::array<uint8_t, kSerialSize + kFdskSize + 1> buffer{};
std::copy(serial, serial + kSerialSize, buffer.begin());
std::copy(key, key + kFdskSize, buffer.begin() + kSerialSize);
buffer[kSerialSize + kFdskSize] = static_cast<uint8_t>((crc4Array(buffer.data(), buffer.size() - 1) << 4) & 0xff);
std::string encoded = toBase32NoPadding(buffer.data(), buffer.size());
if (encoded.size() > kFdskQrSize) {
encoded.resize(kFdskQrSize);
}
return encoded;
}
std::string formatFdskLabel(const std::string& qr_code) {
std::string label;
label.reserve(qr_code.size() + (qr_code.size() / 6));
for (size_t i = 0; i < qr_code.size(); ++i) {
if (i != 0 && (i % 6) == 0) {
label.push_back('-');
}
label.push_back(qr_code[i]);
}
return label;
}
} // namespace
namespace gateway::openknx {
bool LoadFactoryFdsk(uint8_t* data, size_t len) {
if (data == nullptr || len < kFdskSize || !ensureNvsReady()) {
return false;
}
nvs_handle_t handle = 0;
esp_err_t err = nvs_open(kNamespace, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err));
return false;
}
size_t stored_size = kFdskSize;
err = nvs_get_blob(handle, kFactoryFdskKey, data, &stored_size);
if (err == ESP_OK && stored_size == kFdskSize && plausibleKey(data)) {
nvs_close(handle);
return true;
}
generateKey(data);
err = nvs_set_blob(handle, kFactoryFdskKey, data, kFdskSize);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to store generated KNX factory FDSK: %s", esp_err_to_name(err));
return false;
}
return true;
}
FactoryFdskInfo LoadFactoryFdskInfo() {
FactoryFdskInfo info;
std::array<uint8_t, kFdskSize> key{};
std::array<uint8_t, kSerialSize> serial{};
if (!LoadFactoryFdsk(key.data(), key.size()) ||
esp_read_mac(serial.data(), ESP_MAC_WIFI_STA) != ESP_OK) {
return info;
}
info.available = true;
info.serialNumber = toHex(serial.data(), serial.size());
info.qrCode = generateFdskQrCode(serial.data(), key.data());
info.label = formatFdskLabel(info.qrCode);
return info;
}
} // namespace gateway::openknx
extern "C" bool knx_platform_get_fdsk(uint8_t* data, size_t len) {
return gateway::openknx::LoadFactoryFdsk(data, len);
}
@@ -1,114 +0,0 @@
#include "openknx_idf/tpuart_uart_interface.h"
#include "esp_log.h"
#include <utility>
namespace gateway::openknx {
namespace {
constexpr const char* kTag = "openknx_tpuart";
} // namespace
TpuartUartInterface::TpuartUartInterface(uart_port_t uart_port, int tx_pin, int rx_pin,
size_t rx_buffer_size, size_t tx_buffer_size)
: uart_port_(uart_port),
tx_pin_(tx_pin),
rx_pin_(rx_pin),
rx_buffer_size_(rx_buffer_size),
tx_buffer_size_(tx_buffer_size) {}
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 = UART_PARITY_EVEN;
config.stop_bits = UART_STOP_BITS_1;
config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
config.source_clk = UART_SCLK_DEFAULT;
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_ < 0 ? UART_PIN_NO_CHANGE : tx_pin_,
rx_pin_ < 0 ? UART_PIN_NO_CHANGE : rx_pin_, UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to route UART%d pins: %s", uart_port_, 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<bool()> callback) {
callback_ = std::move(callback);
}
} // namespace gateway::openknx
+1 -1
Submodule knx updated: 1549366447...af8e8a6204
Submodule knx_dali_gw deleted from 6064d84520
+1 -1
Submodule tpuart updated: f8c01e6a32...7510e616f9