Signed-off-by: Tony <tonylu@tony-cloud.com>
Gateway Rewrite
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.gateway_core/: boot profile and top-level role bootstrap.dali/: vendored ESP-IDF DALI HAL/backend reused from LuatOS, including native raw receive fan-out.dali_domain/: native DALI domain facade overdali_cppand raw frame sinks.gateway_cache/: ESP-IDF NVS adapter for the portabledali_cppcache, plus gateway-internal scene/group definitions.gateway_bridge/: per-channel bridge provisioning, command execution, protocol startup, and HTTP bridge actions.openknx_idf/: ESP-IDF port layer for the OpenKNXgateway/knxandgateway/tpuartsubmodules, 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 onFFF1/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 port2020command/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_matter/: ESP-Matter 1.5.1 adapter that discovers DALI control gear, persists endpoint bindings in normal NVS, and exposes supported DALI lighting behavior through dynamic Matter endpoints.gateway_runtime/: persistent runtime state, command queueing, and device info services.gateway_485_control/: optional 485 control bridge at9600 8N1; disabled by default because UART0 must be moved off the ESP-IDF console first. C++ gateway commands on this raw multi-client bus use the session-scoped frame described below.gateway_usb_setup/: optional USB Serial/JTAG setup bridge; disabled by default so USB remains available for debug at boot.gateway_tridonic_hid/: optional native USB OTG HID device that emulates the Tridonic DALI USB (VID:PID17b5:0020) protocol for masterConfigurator.
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
dali_cpp::DaliGatewayCache owns the shared DALI cache and is the authoritative
read surface for App, BLE, IP, Modbus, BACnet, KNX, and local control paths. It
interprets direct, group, broadcast, scene, DTR, and settings commands; owns
presence and reconciliation state; versions persisted address snapshots; and
emits platform-neutral DaliGatewayStatusUpdate callbacks.
The ESP-IDF gateway_cache component no longer contains DALI cache behavior.
It binds the portable cache persistence callbacks to NVS and continues to own
only gateway-internal scene/group definitions. Device settings, group masks,
scene levels, known flags, and the last runtime status are batched through the
portable cache using GATEWAY_CACHE_FLUSH_INTERVAL_MS, which defaults to
10000 ms. Runtime status loaded from disk is marked stale until a bus command
or the background refresher verifies it again.
Protocol adapters register for semantic status updates instead of decoding raw DALI commands independently. The current KNX adapter maps the callback target, status, and affected short-address list into its group-object status updates. Transparent/setup raw forwarding paths remain bypass-oriented, while visible raw DALI bus traffic still feeds the portable cache.
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 keeps the Lua-compatible low-byte feature bits,
advertises cache support with bit 0x40, advertises gateway/channel name
support with bit 0x80, and advertises the native C++ gateway type with bit
0x0100. Bit 0x0200 advertises the generic gateway operation protocol, and
bit 0x0400 advertises explicit raw-report leases. Gateway opcode 0x05 reads
and writes user-facing gateway identity: operation 0x00 reads the channel
name, 0x01 writes the channel name, 0x02 reads the physical gateway device
name, and 0x03 writes the physical gateway device name. Empty writes reset to
the default name. Gateway opcode 0x39 returns cache summary and target
snapshots so frontend clients can read cached state without issuing live DALI
queries on supported gateways.
Gateway opcode 0x09 with address/data 0x00/0x00 is a chip-level channel-id
report. It returns the enabled DALI channel ids so clients do not need to probe
every possible gateway id one by one. For native C++ gateways, channel number is
the fixed 1-based Kconfig slot (1 to 16) and channel id is the persisted,
Lua-compatible gateway id used by normal 0x28 0x01 ... command frames.
Physical gateway ids are reserved to 0..127. Raw control transports reserve
128..254 for gateway group targets and 255 for all-gateway fan-out.
Gateway opcode 0x0B is the serial-scoped channel command. The serial is the
last three bytes of the ESP base MAC. Operation 0x00 reports serial plus
(channel number, channel id) pairs. Operations 0x01 and 0x02 get and set
the channel id for the fixed channel number in the command frame gateway byte.
Operation 0x03 wraps an existing gateway command and dispatches it by fixed
channel number after the serial matches, so BLE/Wi-Fi configuration and DALI
send/query commands can target a channel even when its variable channel id is
unknown.
Operations 0x04 and 0x05 get and set a channel gateway group id. Group 0
means no group membership; group ids 128..254 can be used as raw-control
targets. Gateway opcode 0x6A exposes the same group setting by physical
gateway id: operation 0x00 reads the group and operation 0x01 writes group
0 or 128..254.
Ethernet control
Ethernet is the preferred default route whenever the W5500 has an address.
Wi-Fi remains a fallback; the gateway does not start Wi-Fi provisioning at
startup. GATEWAY_MATTER_WIFI_PROVISION_BUTTON_GPIO defaults to -1; assigning
a GPIO allows a long press to start ESP-Touch SmartConfig and reopen the Matter
commissioning window. The long-press duration and active level are configurable
beside the GPIO in main/Kconfig.projbuild.
The network service accepts Lua-style raw gateway frames on UDP port 2020 and,
when enabled, TCP port 2020. It also accepts JSON control frames on the same
ports:
{"type":"discover"}returnsdiscoverRespwith device identity, UDP/TCP control ports, Ethernet status/config, and channel gateway/group data.{"type":"dali","data":"<hex gateway frame>"}enqueues a raw gateway command.{"type":"macDali","mac":"<ethernet MAC or 3-byte serial>","data":"<hex gateway frame>"}enqueues a raw command only on the matching gateway. The gateway broadcasts command responses back to the sender's UDP port, allowing local-link control when its IPv4 address is unknown or configured for the wrong subnet.{"type":"setconfig","data":{"ip":"...","mask":"...","gw":"...","dns":"..."}}stores and applies static Ethernet IPv4 settings.ipempty or0.0.0.0clears static config and returns Ethernet to DHCP.
Raw UDP, raw TCP, and UART0 session control all expand gateway target 255 to
all physical gateway ids and targets 128..254 to channels whose configured
gateway group matches the target.
Matter-to-DALI development bridge
GATEWAY_MATTER_SUPPORTED enables the Matter bridge by default. It creates one
aggregator and owns a pool of 32 bridged DALI endpoints. ESP-Matter is configured
for 34 dynamic entries: the root, the aggregator, and the 32-slot DALI pool.
Every enabled channel receives a broadcast endpoint first, then cached non-empty
groups (including manually retained empty groups), explicitly included short
addresses, and automatic online short addresses in ascending order. Allocation
stops at 32 and reports every dropped target; this means the highest automatic
short addresses are removed first. Broadcast and group targets cannot be
disabled. On a single DALI channel, the pool can therefore retain broadcast,
all 16 groups, and 15 direct addresses when every group is present.
The portable DALI cache persists device-type masks and DT8 color-feature bits in addition to group, scene, settings, and runtime state. The planner infers the highest required multi-target capability in the order color, color temperature, dimmer, then switch. DT8 feature bits select XY, RGBCW, or both. Per-channel versioned configuration is stored in normal NVS by fixed channel index and can override the automatic short-address policy, sparse direct-address inclusion, and each broadcast/group type and color method. Existing direct-only bindings are migrated to stable channel-index plus target-kind bindings. Reconciliation retains unchanged endpoints and recreates one only when its Matter device type changes.
Matter On/Off, Level Control, Color Control (mirek, XY, and hue/saturation converted to equivalent DALI writes), Groups, Scenes, and Identify are the only application clusters retained. Root-node commissioning, credentials, descriptor, access-control, diagnostics, and the ESP-Matter internal Binding manager are retained because the server requires them; unrelated device-type clusters, Thread, OTA, and the Matter shell are excluded.
The checked-in ESP32-S3 profile uses only ESP-IDF 5.5.4-supported PSRAM paths:
CHIP, ESP-Matter, mDNS, TLS, NVS cache, Wi-Fi/lwIP allocations, and eligible BSS
prefer external RAM. The mDNS task and gateway-owned BACnet and on-demand DALI
operation stacks can use PSRAM with internal-RAM fallback. Tasks that write NVS,
change network settings, run the DALI PHY, or have timing/DMA constraints remain
in internal RAM; in particular, the Matter DALI scan stack stays internal because
the scan commits its discovered capabilities. Eligible non-ISR FreeRTOS code is
placed in flash to leave internal RAM headroom. This selective policy does not depend on
the unreleased global FREERTOS_PLACE_TASK_STACKS_IN_EXT_RAM option. Wi-Fi IRAM
throughput optimizations are disabled because Ethernet is the preferred data
path and Wi-Fi is retained as a provisioning and fallback path.
CHIP owns the single NimBLE controller and host while Matter is enabled. The
legacy gateway_ble transport registers its distinct FFF7 service in CHIP's
GATT database through ESP-Matter's extra-service hook, and adds the FFF7 UUID plus
gateway name to the Matter scan response. A passive GAP listener tracks gateway
subscriptions without replacing CHIP's callbacks. Matter commissioning
advertising and gateway BLE therefore coexist; after Matter advertising stops,
the gateway continues advertising from the same host. CHIP's BLE endpoint pool
matches NimBLE's four-connection capacity so a gateway client does not make
Matter commissioning advertising non-connectable. NimBLE also publishes the
standard GATT Database Hash so clients can invalidate the former standalone
gateway attribute table after firmware updates. USB, Ethernet, Wi-Fi,
KNX, Modbus, BACnet, and the portable DALI behavior remain in their existing
components. ESP-Matter's Ethernet platform path is compiled so
CHIP receives Ethernet address events, while its Ethernet Network Commissioning
driver stays disabled because wired Ethernet needs no credentials.
CONFIG_CUSTOM_NETWORK_CONFIG removes the root-node Network Commissioning
cluster and CHIP's Wi-Fi station platform. The gateway's station-only ESP-Touch
SmartConfig path starts only through the configured long-press button; it is
disabled at startup, and SoftAP support is compiled out. When both interfaces
have addresses, gateway_network selects Ethernet as the default route. The
project build also omits ESP-Matter's unused network-commissioning integration
source because the ESP32-S3 gateway's W5500 and Wi-Fi lifecycles remain owned by
gateway_network.
This configuration is development-only. The ESP-Matter factory, device
instance, DAC, and commissionable-data providers read the normal nvs
partition. Flash encryption, NVS encryption, and secure boot remain disabled
so credentials and state are visible during debugging. DaliMaster's ESP burner
uses the local packages/matter_mfg_tool package to generate a fresh P-256 test
DAC and ESP-IDF NVS v2 image, then inserts it at 0x9000. The package hardcodes
the ESP-Matter test PAI signing key/certificate and Certification Declaration;
it does not invoke an external manufacturing executable. The burner reads a selected partition-table image
at 0x8000 and fills the declared normal NVS size (0x14000 in the checked-in
4 MiB profile or 0x6000 in the 16 MiB default layout) so stale NVS pages are
erased. Each image contains the test VID/PID FFF2/8001, its unique test DAC
private key/certificate, PAI, certification declaration, passcode 20202021,
and discriminator 3840. Flashing that image replaces all existing normal NVS
data and must never be used for production hardware.
DaliMaster's Matter Gateway page uses the transport-independent bridge action
surface to read live Matter state, open or close the five-minute commissioning
window, configure endpoints, and rescan DALI control gear. matter_status
returns schema version, effective 32-slot capacity, candidates, saved
configuration, active and dropped allocations, target kind/address, inferred or
manual source, and the last partial-apply error. matter_onboarding returns the
live serial number, VID/PID, QR payload, and manual pairing code from the active
ESP-Matter factory providers; controllers must treat this response as sensitive
and DaliMaster keeps it only in page memory. matter_config applies a selected
channel patch, while matter_config_reset restores automatic selection.
matter_open_commissioning, matter_close_commissioning, matter_rescan,
matter_config, and matter_config_reset are POST actions. The development-only
matter_factory_data action validates and writes a compact test-factory payload
into chip-factory without erasing unrelated NVS state. It accepts only the
fixed FFF2/8001 Matter Test PAA profile, verifies the embedded PAI and
Certification Declaration fingerprints, and verifies the DAC signature and
certificate SHA-256 before committing the DAC/private/public keys, PAI,
Certification Declaration, SPAKE2+ data, and device identity. Its response reports
factoryData.restartRequired; matter_restart schedules a delayed reboot so
the transport can deliver its response before ESP-Matter reloads the providers.
These actions are available through the active
BLE/USB/IP/KNX/cloud bridge transport as well as the local HTTP /bridge
endpoint, so Matter setup does not require a working IP profile.
Gateway operation protocol
Opcode 0x67 starts, aborts, and polls gateway-executed high-level DALI
operations. Clients send one compact gateway command and the controller runs the
required DTR setup, device-type select, query chain, retry, or address iteration
on the gateway. The command frame is:
- Start:
28 01 <gw> 67 01 <requestId> <operationIdLo> <operationIdHi> <payloadLenLo> <payloadLenHi> <TLV...> <checksum>. - Abort:
28 01 <gw> 67 02 <requestId> 00 00 00 00 <checksum>. - Status:
28 01 <gw> 67 03 <requestId> 00 00 00 00 <checksum>.
Multi-byte fields are little-endian. TLV payload fields use
{fieldId:u8, type:u8, len:u8, value...}. Types are 0x01 u8, 0x02 u16,
0x03 u32, 0x04 signed i32, 0x05 bool, and 0x06 byte list. Dynamic
snapshot metadata is returned as repeated byte-list entries using compact
key=value payloads.
Operation events are emitted as
22 67 <gw> <requestId> <operationIdLo> <operationIdHi> <event> <status> <progress> <target> <count> <checksum>.
Event values are accepted 0x00, progress 0x01, item result 0x02,
completed 0x03, aborted 0x04, and error 0x05. Status values are ok
0x00, busy 0x01, invalid 0x02, unsupported 0x03, no response 0x04,
failed 0x05, and aborted 0x06.
Large result payloads are emitted as chunks:
22 68 <gw> <requestId> <operationIdLo> <operationIdHi> <total> <index> <chunkLen> <TLV...> <checksum>.
Clients assemble chunks by gateway, request id, and operation id before
processing result TLVs.
The initial C++ executor accepts the shared BridgeOperation numeric ids for
raw send/query, brightness/on/off/recall, color-temperature and DT8 RGB/XY/RGBW
setters, DT1/DT4/DT5/DT6/DT8 snapshots, group masks, scene levels/maps, address
settings, short-address range search, and short-address allocation/reset/stop.
Unsupported ids return an unsupported operation error so old clients can fall
back to their app-side workflow. Legacy opcodes such as 0x12, 0x13, 0x14,
0x30, 0x32, 0x60-0x65, and 0x39 remain available for compatibility.
Operation id 84 performs a direct-short-address DALI identity memory batch
read. The start TLV must include target field 0x01 as a u8 short address
0..63; group and broadcast reads are rejected. Kind field 0x20 as u8
selects compact (0) or extended (nonzero) identity coverage. Results are
repeated field 0x30 byte-list entries, each with payload
[bank, location, value]. Missing byte replies are omitted; a completed result
with no entries reports no response so clients can fall back to single-byte
READ MEMORY LOCATION reads.
Opcode 0x66 controls passive raw-report leases. Command
28 01 <gw> 66 01 <enabled> <ttlLo> <ttlHi> <checksum> enables or disables a
volatile per-gateway lease; ttl=0 disables. The response is
22 66 <gw> <status> <enabled> <ttlLo> <ttlHi> <checksum>. Passive raw DALI
notifications over UDP, BLE raw characteristics, ESP-NOW setup UART mirroring,
and gateway notification opcodes 0x01 and 0x65 are suppressed unless the
lease is active. Direct command/query responses still work without a raw-report
lease.
Gateway runtime settings
GET /bridge?action=gateway_settings returns the versioned runtime-settings
document. POST /bridge?action=gateway_settings accepts a partial patch inside
settings. The mutable fields are the device name, BLE/cache switches, Wi-Fi
credentials, Ethernet DHCP/static IPv4 configuration, the boot USB personality,
and channel gateway ids/groups/names. Wi-Fi passwords are write-only: reads
contain wifi.passwordSet, never the password itself. Device and channel names
must contain at least one non-whitespace character and be at most 32 bytes.
{
"schemaVersion": 1,
"settings": {
"deviceName": "DALI Gateway",
"bleEnabled": true,
"cacheEnabled": true,
"wifi": {"configured": true, "ssid": "example", "passwordSet": true},
"ethernet": {"dhcp": true, "ip": "", "mask": "", "gateway": "", "dns": ""},
"usb": {"mode": "debug", "channelIndex": 0, "restartRequired": false},
"channels": [{"channelIndex": 0, "gatewayId": 3, "gatewayGroup": 0,
"name": "Gateway_3", "phy": "native"}]
}
}
USB mode is one of debug, setup, or tridonic. New devices default to
debug; Tridonic HID is never the runtime default. The desired USB mode and
channel are stored in NVS, then applied only after restart because USB
descriptors cannot be changed while attached. A response sets
settings.usb.restartRequired and top-level restartRequired when the desired
value differs from the personality running at boot. Channel indexes are
zero-based. capabilities.usb.channelIndices is the authoritative list of
configured DALI channel indexes, for example:
{
"capabilities": {
"usb": {
"supported": true,
"modes": ["debug", "setup", "tridonic"],
"channelIndices": [0, 2]
}
}
}
For setup and tridonic, settings.usb.channelIndex must be one of those
values. An unavailable channel is rejected before it is persisted with an
error that points clients to capabilities.usb.channelIndices; it is not a
generic 0 through 15 selector. At boot, legacy invalid persisted USB
channels are normalized to the first configured channel when NVS is writable;
the current boot runs in safe debug mode rather than starting a USB
personality on an unintended bus. Hardware PHY/pin mappings, task sizing, and
other Kconfig build settings are exposed only as build capabilities/defaults;
they are not mutable runtime settings.
When the MQTT cloud bridge is provisioned, the same document is available on
the existing <topicPrefix>/<deviceID>/down topic. Send
{"type":"gatewaySettingsGet","requestId":"..."} or
{"type":"gatewaySettingsSet","requestId":"...","settings":{...}}.
The gateway publishes type:"gatewaySettings", the request id, a status of
ok on success or error, and the sanitized current
settings/capabilities/build document on the normal up topic.
C++ 485 session frames
The optional raw 485 control bridge requires a one-byte random session hash for C++ gateway commands so multiple unmanaged clients can distinguish responses and safely retry after collisions. The 485 command envelope is:
28 01 <gw> 69 <hash> <innerOpcode> <innerPayload...> <checksum>.
The bridge strips 69 <hash> and dispatches the inner command through the
normal controller. A repeated frame with the same hash and identical inner
command is treated as the same session and is not executed again. Session
status replies are:
22 69 <gw> <hash> <status> <resultLenLo> <resultLenHi> <result...> <checksum>.
Status values are failed 0x00, queued 0x01, in process 0x02, and finished
0x03. result contains the normal 0x22 ... checksum response frame bytes
when the command produces a response, and is empty for write-only commands.
The session cache is volatile and keeps recent sessions long enough for clients
to retry the same request after a bus collision. BLE, UDP/IP, USB setup, and
LAMMIN legacy/new 485 paths continue to use their existing framing.
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, setup AP GPIO entry, and optional Wi-Fi credential reset GPIO handling, and an optional gateway_485_control bridge that claims UART0 for C++ session-scoped 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 persisted USB personality starts in debug mode unless it was explicitly changed to setup or Tridonic before reboot, 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. KNXnet/IP Secure now recognizes the secure service family, performs secure session setup/authentication with provisioned tunnel user keys, wraps secure tunnel responses, handles secure group-sync frames, and wraps/unpacks secure multicast routing frames when an OAM backbone key is active. 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 shared KNXnet/IP endpoint can also be provisioned with an OAM-compatible IP-Router persona by enabling GATEWAY_KNX_OAM_ROUTER_SUPPORTED and the nested knx.oamRouter config. This second logical application is part of the same KNX/IP router endpoint: it does not open a second UDP/TCP listener and it does not own a second TP-UART driver. When OAM is enabled, KNXnet/IP search, description, tunnel, and multicast identity are advertised as the OAM IP router and follow the OAM individual address instead of a DALI-channel namespace. The gateway hosts a BAU091A/OAM router runtime beside the REG1-Dali BAU07B0 runtime, with separate individual/tunnel addresses, separate programming button/LED GPIOs, separate ETS/security storage, and a KNX serial derived from the ESP base MAC plus one. Secure tunnels are assigned to the OAM persona, and non-secure IP management/tunnel connections opened while OAM programming mode is active are also assigned to the OAM persona so ETS can modify and verify the OAM individual address. OAM-addressed management frames are dispatched to the BAU091A runtime while DALI group/function-property traffic stays on the REG1-Dali application. Physical TP ingress is brokered at the shared TP-UART owner: OAM-addressed TP telegrams and broadcast management telegrams are delivered to the matching local logical runtime, and OAM responses are transmitted through the same TP-UART and mirrored to KNXnet/IP once. Normal REG1-Dali TP group/application traffic remains on the existing REG1 data-link path so ETS group-object dispatch and DALI routing do not duplicate. The default OAM identity follows the OAM-IP-Router release database (0x00FA manufacturer, 0xA11F application number, version 0x07) unless overridden in Kconfig.
OAM IP Secure keyring preparation uses the knx_oam_sec NVS namespace. Development HTTP actions can read/generate/reset/export the OAM factory setup key and store already-extracted IP Secure keyring material (backboneKeyHex, tunnel user keys, and an optional device-authentication key). Stored OAM credentials are reported in knx.security.oamRouter.ipSecureCredentials; the tunnel user keys authenticate secure sessions and the backbone key protects secure routing/group-sync traffic. The routing sequence counter is persisted back to NVS after secure routing sends or authenticated sync updates.
Cloud KNX remote-access preparation is part of the knx.oamRouter.cloudRemote config. The status JSON reports the selected mode (mqtt, relay, or UDP punch-through-oriented deployments), whether secure tunnels are required, and whether relay endpoint, MQTT topic prefix, and token-reference fields are configured. The firmware does not start an external relay client yet; this config is the stable handoff surface for a future UDP relay/MQTT tunnel transport that will reuse the secure OAM tunnel path.
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. Local logical-device responses are de-duplicated by exact cEMI payload at the tunnel egress, so the same response is not replayed through tunnel, routing, and TP echo paths while distinct source addresses remain visible as separate devices. 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 chip-level KNXnet/IP endpoint per physical gateway module 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 matching channel bridges instead of starting one UDP socket or multicast responder per DALI channel. If a non-owner channel router was already running, starting the shared endpoint stops it so KNXnet/IP search and description requests receive one response from the chip-level interface.
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 the OAM router persona is enabled, use GATEWAY_KNX_OAM_PROGRAMMING_BUTTON_GPIO and GATEWAY_KNX_OAM_PROGRAMMING_LED_GPIO for its separate programming controls. The bridge config validator rejects duplicate REG1-Dali and OAM programming button or LED GPIO assignments so ETS programming-mode selection remains unambiguous.
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
Modbus TCP, RTU, and ASCII are owned by gateway/components/gateway_modbus and started through the per-channel bridge service. The gateway keeps the existing bridge config JSON shape with a top-level modbus object containing transport, host, port, and unitID, and now adds nested serial UART settings for RTU/ASCII. Parsing and runtime behavior live in the gateway project rather than in dali_cpp.
See MODBUS.md for transport setup, UART0 policy, RS485 wiring, runtime @DALIGW management commands, supported function codes, and the full generated address map with address formulas.
The first generated map slice creates stable points for every DALI short address 0-63 whether the device is online, offline, or never seen. Per short address, the generated map reserves a 32-point stride in each Modbus space:
- Coils: command triggers such as on, off, recall max, and recall min.
- Discrete inputs: inventory, online, supported device-type, cache-known, and base status bit positions.
- Holding registers: writable brightness, color temperature, group mask, power-on level, system-failure level, min/max level, and fade time.
- Input registers: read-only inventory state, primary type, type mask, cached actual level, scene id, raw status placeholder, group mask, and cached settings.
Unknown numeric values read as 0xFFFF; booleans read as false unless inventory or cache state proves otherwise. Provisioned Modbus models still work as overrides at their configured Modbus point, and normal generated reads prefer gateway cache state to avoid DALI bus polling.
An extension discrete-input range starts immediately after the legacy 0-63 short-address block. It publishes decoded DALI status, failure, and feature bits as individual booleans for base status, DT1 emergency, DT4/5/6 control-gear feature/failure status, and DT8 color status/features. This keeps existing Modbus addresses stable while making bit-level diagnostics readable without consumers masking packed status registers.
BACnet/IP
BACnet/IP is owned by gateway/components/gateway_bacnet and is started through the per-channel bridge service. Runtime BACnet server settings live under top-level bacnetServer in bridge config:
{
"bacnetServer": {
"deviceInstance": 4194303,
"localAddress": "",
"udpPort": 47808
}
}
Provisioned BACnet models still use generic BridgeModel fields such as object type, object instance, property, and optional bitIndex. Query-style models refresh BACnet Present_Value from live DALI reads, and binary models with bitIndex expose a single packed status bit.
For discovered DALI short addresses, the gateway also mirrors the generated Modbus discrete diagnostics as BACnet binary-input objects. Object instances are allocated in a gateway-owned generated range using the channel index plus the generated Modbus discrete-input offset, so generated objects stay deterministic while avoiding the provisioned-object address space in normal deployments.