# Gateway Modbus The native gateway exposes each DALI channel as a Modbus server. Supported transports are: - `tcp-server`: Modbus TCP server, default TCP port `1502`. - `rtu-server`: Modbus RTU slave/server on an ESP-IDF UART. - `ascii-server`: Modbus ASCII slave/server on an ESP-IDF UART. The generated Modbus map is identical for TCP, RTU, and ASCII. Only the frame transport changes. ## Configuration Compile-time defaults live in `apps/gateway/main/Kconfig.projbuild`: - `GATEWAY_MODBUS_DEFAULT_TRANSPORT_TCP` - `GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU` - `GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII` - `GATEWAY_MODBUS_TCP_PORT` - `GATEWAY_MODBUS_UNIT_ID` - `GATEWAY_MODBUS_SERIAL_UART_PORT` - `GATEWAY_MODBUS_SERIAL_TX_PIN` - `GATEWAY_MODBUS_SERIAL_RX_PIN` - `GATEWAY_MODBUS_SERIAL_BAUDRATE` - `GATEWAY_MODBUS_SERIAL_RESPONSE_TIMEOUT_MS` - `GATEWAY_MODBUS_SERIAL_RS485_ENABLED` - `GATEWAY_MODBUS_SERIAL_RS485_DE_PIN` - `GATEWAY_MODBUS_ALLOW_UART0` UART0 is reserved for the ESP-IDF console by default. Select `GATEWAY_MODBUS_ALLOW_UART0` only when the console has been moved away from UART0 or the deployment intentionally repurposes it. The app validates the Kconfig default UART against DALI serial PHY assignments at boot. Runtime bridge config keeps the existing top-level `modbus` object. The serial fields are nested under `serial`: ```json { "modbus": { "transport": "rtu-server", "unitID": 1, "port": 1502, "serial": { "uartPort": 1, "txPin": 17, "rxPin": 18, "baudrate": 9600, "dataBits": 8, "parity": "none", "stopBits": 1, "rxBufferBytes": 512, "txBufferBytes": 512, "responseTimeoutMs": 20, "interFrameGapUs": 4000, "rs485": { "enabled": true, "dePin": 16 } } } } ``` `unitID` accepts aliases `unitId` and `unit_id`. A configured `unitID` of `0` keeps the gateway compatibility behavior where incoming unit ids are accepted as a wildcard. Incoming serial requests addressed to unit id `0` are treated as Modbus broadcast and do not generate a response. ## UART Management Commands When a Modbus serial transport is active, the UART also accepts management lines before Modbus frame parsing. A management line starts with `@DALIGW `, contains JSON, and ends with newline. Examples: ```text @DALIGW {"action":"modbus_status","gw":3} @DALIGW {"action":"modbus_stop","gw":3} @DALIGW {"action":"modbus_config","gw":3,"modbus":{"transport":"ascii-server","unitID":1,"serial":{"uartPort":1,"txPin":17,"rxPin":18,"baudrate":9600,"rs485":{"enabled":true,"dePin":16}}}} ``` Responses are newline-terminated `@DALIGW` JSON lines. A successful `modbus_config` command is saved to NVS and restarts the Modbus transport after the response is written. HTTP still supports the bridge routes: - `GET /bridge?action=status&gw=N` - `GET /bridge?action=config&gw=N` - `GET /bridge?action=modbus&gw=N` - `POST /bridge?action=config&gw=N` - `POST /bridge?action=modbus_start&gw=N` - `POST /bridge?action=modbus_stop&gw=N` ## Function Codes Supported function codes are: | Code | Name | Map space | | --- | --- | --- | | `0x01` | Read coils | Coils | | `0x02` | Read discrete inputs | Discrete inputs | | `0x03` | Read holding registers | Holding registers | | `0x04` | Read input registers | Input registers | | `0x05` | Write single coil | Coils | | `0x06` | Write single holding register | Holding registers | | `0x0F` | Write multiple coils | Coils | | `0x10` | Write multiple holding registers | Holding registers | Limits are `2000` read bits, `125` read registers, `1968` write bits, `123` write registers, and `252` PDU bytes. Exception codes used by the gateway: | Code | Meaning | | --- | --- | | `0x01` | Unsupported function | | `0x02` | Unmapped address or read failure | | `0x03` | Invalid quantity, value, or request shape | | `0x04` | DALI execution failure | | `0x0B` | Unit id mismatch | ## Address Calculation Modbus protocol frames use zero-based wire offsets. Most tools show human addresses with traditional bases: | Space | Human base | Function codes | | --- | ---: | --- | | Coil | `1` | `0x01`, `0x05`, `0x0F` | | Discrete input | `10001` | `0x02` | | Input register | `30001` | `0x04` | | Holding register | `40001` | `0x03`, `0x06`, `0x10` | For the main generated slice, every DALI short address `0-63` gets a stride of `32` points in each space: ```text human = base + shortAddress * 32 + offset wire = human - base ``` Example: short address `5` brightness is holding-register offset `0`: ```text human = 40001 + 5 * 32 + 0 = 40161 wire = 40161 - 40001 = 160 ``` The diagnostic discrete-input extension starts after the first `64 * 32` discrete inputs: ```text diagnosticBase = 10001 + 64 * 32 = 12049 human = 12049 + shortAddress * 128 + diagnosticOffset wire = human - 10001 ``` Example: short address `5`, diagnostic offset `105` (`DT8 xy out of range`): ```text human = 12049 + 5 * 128 + 105 = 12794 wire = 12794 - 10001 = 2793 ``` ## Main Generated Map Generated points exist for all short addresses `0-63`, even when no device has been discovered. Unknown numeric values read as `0xFFFF`; unknown booleans read as false unless inventory or cache state proves otherwise. Generated reads prefer gateway cache state and do not poll the DALI bus for every Modbus read. ### Coils | Offset | Address formula | Access | Function | | ---: | --- | --- | --- | | `0` | `1 + short * 32 + 0` | Write | On / recall max | | `1` | `1 + short * 32 + 1` | Write | Off | | `2` | `1 + short * 32 + 2` | Write | Recall max | | `3` | `1 + short * 32 + 3` | Write | Recall min | ### Discrete Inputs | Offset | Address formula | Function | | ---: | --- | --- | | `0` | `10001 + short * 32 + 0` | Discovered | | `1` | `10001 + short * 32 + 1` | Online | | `2` | `10001 + short * 32 + 2` | Supports DT1 | | `3` | `10001 + short * 32 + 3` | Supports DT4 | | `4` | `10001 + short * 32 + 4` | Supports DT5 | | `5` | `10001 + short * 32 + 5` | Supports DT6 | | `6` | `10001 + short * 32 + 6` | Supports DT8 | | `7` | `10001 + short * 32 + 7` | Group mask known | | `8` | `10001 + short * 32 + 8` | Actual level known | | `9` | `10001 + short * 32 + 9` | Scene known | | `10` | `10001 + short * 32 + 10` | Settings known | | `16` | `10001 + short * 32 + 16` | Control gear present | | `17` | `10001 + short * 32 + 17` | Lamp failure | | `18` | `10001 + short * 32 + 18` | Lamp power on | | `19` | `10001 + short * 32 + 19` | Limit error | | `20` | `10001 + short * 32 + 20` | Fading completed | | `21` | `10001 + short * 32 + 21` | Reset state | | `22` | `10001 + short * 32 + 22` | Missing short address | | `23` | `10001 + short * 32 + 23` | Power supply fault | ### Holding Registers | Offset | Address formula | Access | Function | | ---: | --- | --- | --- | | `0` | `40001 + short * 32 + 0` | Read/write | Brightness | | `1` | `40001 + short * 32 + 1` | Write, cache-read if known | Color temperature | | `2` | `40001 + short * 32 + 2` | Read/write | Group mask | | `3` | `40001 + short * 32 + 3` | Read/write | Power-on level | | `4` | `40001 + short * 32 + 4` | Read/write | System-failure level | | `5` | `40001 + short * 32 + 5` | Read/write | Minimum level | | `6` | `40001 + short * 32 + 6` | Read/write | Maximum level | | `7` | `40001 + short * 32 + 7` | Read/write | Fade time | | `8` | `40001 + short * 32 + 8` | Read/write | Fade rate | ### Input Registers | Offset | Address formula | Function | | ---: | --- | --- | | `0` | `30001 + short * 32 + 0` | Inventory state: `0` never seen, `1` offline, `2` online | | `1` | `30001 + short * 32 + 1` | Primary DALI device type | | `2` | `30001 + short * 32 + 2` | Device type mask | | `3` | `30001 + short * 32 + 3` | Actual level | | `4` | `30001 + short * 32 + 4` | Scene id | | `5` | `30001 + short * 32 + 5` | Raw status | | `6` | `30001 + short * 32 + 6` | Group mask | | `7` | `30001 + short * 32 + 7` | Power-on level | | `8` | `30001 + short * 32 + 8` | System-failure level | | `9` | `30001 + short * 32 + 9` | Minimum level | | `10` | `30001 + short * 32 + 10` | Maximum level | | `11` | `30001 + short * 32 + 11` | Fade time | | `12` | `30001 + short * 32 + 12` | Fade rate | ## Diagnostic Discrete Inputs Diagnostic addresses use `12049 + short * 128 + offset`. | Offset | Function | | ---: | --- | | `0` | DT1 circuit failure | | `1` | DT1 battery duration failure | | `2` | DT1 battery failure | | `3` | DT1 emergency lamp failure | | `4` | DT1 function test max delay exceeded | | `5` | DT1 duration test max delay exceeded | | `6` | DT1 function test failed | | `7` | DT1 duration test failed | | `8` | DT1 inhibit mode | | `9` | DT1 function test result valid | | `10` | DT1 duration test result valid | | `11` | DT1 battery fully charged | | `12` | DT1 function test request pending | | `13` | DT1 duration test request pending | | `14` | DT1 identification active | | `15` | DT1 physically selected | | `16` | DT1 rest mode active | | `17` | DT1 normal mode active | | `18` | DT1 emergency mode active | | `19` | DT1 extended emergency mode active | | `20` | DT1 function test in progress | | `21` | DT1 duration test in progress | | `22` | DT1 hardwired inhibit active | | `23` | DT1 hardwired switch on | | `24` | DT1 integral emergency gear | | `25` | DT1 maintained gear | | `26` | DT1 switched maintained gear | | `27` | DT1 auto test capability | | `28` | DT1 adjustable emergency level | | `29` | DT1 hardwired inhibit supported | | `30` | DT1 physical selection supported | | `31` | DT1 relight in rest mode supported | | `32` | DT4 leading edge running | | `33` | DT4 trailing edge running | | `34` | DT4 reference measurement running | | `35` | DT4 non-log curve active | | `36` | DT4 can query load over-current shutdown | | `37` | DT4 can query open circuit | | `38` | DT4 can query load decrease | | `39` | DT4 can query load increase | | `40` | DT4 can query thermal shutdown | | `41` | DT4 can query thermal overload | | `42` | DT4 physical selection supported | | `43` | DT4 can query temperature | | `44` | DT4 can query supply voltage | | `45` | DT4 can query supply frequency | | `46` | DT4 can query load voltage | | `47` | DT4 can query load current | | `48` | DT4 can query real load power | | `49` | DT4 can query load rating | | `50` | DT4 can query current overload | | `51` | DT4 can select non-log curve | | `52` | DT4 can query unsuitable load | | `53` | DT4 load over-current shutdown | | `54` | DT4 open circuit detected | | `55` | DT4 load decrease detected | | `56` | DT4 load increase detected | | `57` | DT4 thermal shutdown | | `58` | DT4 thermal overload reduction | | `59` | DT4 reference failed | | `60` | DT4 unsuitable load | | `61` | DT4 supply voltage out of limits | | `62` | DT4 supply frequency out of limits | | `63` | DT4 load voltage out of limits | | `64` | DT4 load current overload | | `65` | DT5 output range selectable | | `66` | DT5 pull-up selectable | | `67` | DT5 fault detection selectable | | `68` | DT5 mains relay | | `69` | DT5 output level queryable | | `70` | DT5 non-log curve supported | | `71` | DT5 output-loss selection supported | | `72` | DT5 selection switch supported | | `73` | DT5 output fault detected | | `74` | DT5 0-10V operation | | `75` | DT5 pull-up on | | `76` | DT5 non-log curve active | | `77` | DT6 power supply integrated | | `78` | DT6 LED module integrated | | `79` | DT6 AC supply possible | | `80` | DT6 DC supply possible | | `81` | DT6 PWM possible | | `82` | DT6 AM possible | | `83` | DT6 current control possible | | `84` | DT6 high current possible | | `85` | DT6 can query short circuit | | `86` | DT6 can query open circuit | | `87` | DT6 can query load decrease | | `88` | DT6 can query load increase | | `89` | DT6 can query current protector | | `90` | DT6 can query thermal shutdown | | `91` | DT6 can query thermal overload | | `92` | DT6 short circuit | | `93` | DT6 open circuit | | `94` | DT6 load decrease | | `95` | DT6 load increase | | `96` | DT6 current protector active | | `97` | DT6 thermal shutdown | | `98` | DT6 thermal overload | | `99` | DT6 reference failed | | `100` | DT6 PWM active | | `101` | DT6 AM active | | `102` | DT6 current controlled output | | `103` | DT6 high current active | | `104` | DT6 non-log curve active | | `105` | DT8 xy out of range | | `106` | DT8 color-temperature out of range | | `107` | DT8 auto calibration active | | `108` | DT8 auto calibration success | | `109` | DT8 xy active | | `110` | DT8 color-temperature active | | `111` | DT8 primary-N active | | `112` | DT8 RGBWAF active | | `113` | DT8 xy capable | | `114` | DT8 color-temperature capable | | `115` | DT8 primary-N capable | | `116` | DT8 RGBWAF capable | | `117` | DT6 physical selection supported | | `118` | DT6 current protector enabled | | `119` | DT1 control gear failure | ## Provisioned Overrides Provisioned bridge models are applied after the generated map. If a Modbus model uses the same space and human address as a generated point, the model replaces that generated point. For Modbus models: - `external.objectType` must be `coil`, `discrete_input`, `input_register`, or `holding_register`. - `external.registerAddress` is the human address, not the zero-based wire offset. - `external.bitIndex` exposes one bit from a numeric read result for boolean objects. - DALI targets use short addresses `0-63`, groups as `64 + group`, and broadcast as `127`. - DALI query/read operations must target short addresses only. - `valueTransform` scaling, offset, rounding, and clamps are applied by the bridge engine. Use `GET /bridge?action=modbus&gw=N` to inspect the effective generated and provisioned bindings served by a running gateway.