Add serial configuration support to Gateway Modbus
- Introduced GatewayModbusSerialConfig structure to encapsulate serial communication settings. - Added clamping functions for integer and size values to ensure valid configuration ranges. - Updated GatewayModbusConfigFromValue to parse serial configuration from JSON input. - Implemented transport type checking functions for TCP, RTU, ASCII, and Serial. - Enhanced GatewayModbusConfigToValue to include serial configuration in output. Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
# 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.
|
||||
@@ -12,7 +12,7 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
|
||||
- `dali_domain/`: native DALI domain facade over `dali_cpp` and raw frame sinks.
|
||||
- `gateway_cache/`: DALI scene/group/settings/runtime cache used by controller reconciliation and protocol bridges.
|
||||
- `gateway_bridge/`: per-channel bridge provisioning, command execution, protocol startup, and HTTP bridge actions.
|
||||
- `gateway_modbus/`: gateway-owned Modbus TCP config, generated DALI point tables, and provisioned Modbus model override dispatch.
|
||||
- `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.
|
||||
@@ -24,9 +24,11 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
|
||||
|
||||
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, and 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`, 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. Startup behavior is configured in `main/Kconfig.projbuild`: BLE is enabled by default, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, and the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected. 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.
|
||||
|
||||
## Modbus TCP
|
||||
## Modbus
|
||||
|
||||
Modbus TCP is 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`, but parsing and runtime behavior now live in the gateway project rather than in `dali_cpp`.
|
||||
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:
|
||||
|
||||
|
||||
@@ -387,20 +387,104 @@ config GATEWAY_BRIDGE_SUPPORTED
|
||||
Enables per-channel dali_cpp bridge model provisioning, execution, and protocol adapter state.
|
||||
|
||||
config GATEWAY_MODBUS_BRIDGE_SUPPORTED
|
||||
bool "Modbus TCP bridge is supported"
|
||||
depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED
|
||||
bool "Modbus bridge is supported"
|
||||
depends on GATEWAY_BRIDGE_SUPPORTED
|
||||
default y
|
||||
help
|
||||
Enables the gateway-owned per-channel Modbus TCP server, generated DALI point map,
|
||||
and provisioned model overrides. Runtime startup still requires persisted bridge
|
||||
config with Modbus settings.
|
||||
Enables the gateway-owned per-channel Modbus TCP, RTU, or ASCII server,
|
||||
generated DALI point map, and provisioned model overrides.
|
||||
|
||||
config GATEWAY_START_MODBUS_BRIDGE_ENABLED
|
||||
bool "Start Modbus TCP bridge at startup"
|
||||
bool "Start Modbus bridge at startup"
|
||||
depends on GATEWAY_MODBUS_BRIDGE_SUPPORTED
|
||||
default n
|
||||
help
|
||||
Starts configured Modbus TCP listeners at boot. Disabled by default so ports are opened only after provisioning or explicit runtime start.
|
||||
Starts the configured or Kconfig-default Modbus listener at boot. Disabled
|
||||
by default so ports are opened only after provisioning or explicit runtime start.
|
||||
|
||||
choice GATEWAY_MODBUS_DEFAULT_TRANSPORT
|
||||
prompt "Default Modbus transport"
|
||||
depends on GATEWAY_MODBUS_BRIDGE_SUPPORTED
|
||||
default GATEWAY_MODBUS_DEFAULT_TRANSPORT_TCP
|
||||
help
|
||||
Selects the default Modbus transport used before runtime bridge config is saved.
|
||||
|
||||
config GATEWAY_MODBUS_DEFAULT_TRANSPORT_TCP
|
||||
bool "TCP server"
|
||||
depends on GATEWAY_WIFI_SUPPORTED
|
||||
|
||||
config GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU
|
||||
bool "RTU server on UART"
|
||||
|
||||
config GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII
|
||||
bool "ASCII server on UART"
|
||||
|
||||
endchoice
|
||||
|
||||
config GATEWAY_MODBUS_TCP_PORT
|
||||
int "Default Modbus TCP port"
|
||||
depends on GATEWAY_MODBUS_BRIDGE_SUPPORTED && GATEWAY_MODBUS_DEFAULT_TRANSPORT_TCP
|
||||
range 1 65535
|
||||
default 1502
|
||||
|
||||
config GATEWAY_MODBUS_UNIT_ID
|
||||
int "Default Modbus unit id"
|
||||
depends on GATEWAY_MODBUS_BRIDGE_SUPPORTED
|
||||
range 0 247
|
||||
default 1
|
||||
help
|
||||
Unit id used by the default Modbus server. A value of 0 keeps the existing
|
||||
gateway wildcard behavior for incoming requests.
|
||||
|
||||
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
|
||||
default 1
|
||||
|
||||
config GATEWAY_MODBUS_ALLOW_UART0
|
||||
bool "Allow Modbus/setup to claim UART0"
|
||||
depends on GATEWAY_MODBUS_BRIDGE_SUPPORTED && (GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU || GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII)
|
||||
default n
|
||||
help
|
||||
UART0 is normally reserved for the ESP-IDF console. Enable only when the
|
||||
console has been moved away from UART0 or the deployment intentionally
|
||||
repurposes it.
|
||||
|
||||
config GATEWAY_MODBUS_SERIAL_TX_PIN
|
||||
int "Default Modbus serial TX pin"
|
||||
depends on GATEWAY_MODBUS_BRIDGE_SUPPORTED && (GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU || GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII)
|
||||
range -1 48
|
||||
default -1
|
||||
|
||||
config GATEWAY_MODBUS_SERIAL_RX_PIN
|
||||
int "Default Modbus serial RX pin"
|
||||
depends on GATEWAY_MODBUS_BRIDGE_SUPPORTED && (GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU || GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII)
|
||||
range -1 48
|
||||
default -1
|
||||
|
||||
config GATEWAY_MODBUS_SERIAL_BAUDRATE
|
||||
int "Default Modbus serial baudrate"
|
||||
depends on GATEWAY_MODBUS_BRIDGE_SUPPORTED && (GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU || GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII)
|
||||
range 1200 921600
|
||||
default 9600
|
||||
|
||||
config GATEWAY_MODBUS_SERIAL_RESPONSE_TIMEOUT_MS
|
||||
int "Default Modbus serial response timeout ms"
|
||||
depends on GATEWAY_MODBUS_BRIDGE_SUPPORTED && (GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU || GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII)
|
||||
range 1 1000
|
||||
default 20
|
||||
|
||||
config GATEWAY_MODBUS_SERIAL_RS485_ENABLED
|
||||
bool "Enable Modbus RS485 half-duplex mode"
|
||||
depends on GATEWAY_MODBUS_BRIDGE_SUPPORTED && (GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU || GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII)
|
||||
default n
|
||||
|
||||
config GATEWAY_MODBUS_SERIAL_RS485_DE_PIN
|
||||
int "Default Modbus RS485 DE/RTS pin"
|
||||
depends on GATEWAY_MODBUS_SERIAL_RS485_ENABLED
|
||||
range -1 48
|
||||
default -1
|
||||
|
||||
config GATEWAY_BACNET_BRIDGE_SUPPORTED
|
||||
bool "BACnet/IP bridge is supported"
|
||||
|
||||
@@ -67,6 +67,38 @@
|
||||
#define CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_PRIORITY 4
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_MODBUS_TCP_PORT
|
||||
#define CONFIG_GATEWAY_MODBUS_TCP_PORT 1502
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_MODBUS_UNIT_ID
|
||||
#define CONFIG_GATEWAY_MODBUS_UNIT_ID 1
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_MODBUS_SERIAL_UART_PORT
|
||||
#define CONFIG_GATEWAY_MODBUS_SERIAL_UART_PORT 1
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_MODBUS_SERIAL_TX_PIN
|
||||
#define CONFIG_GATEWAY_MODBUS_SERIAL_TX_PIN -1
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_MODBUS_SERIAL_RX_PIN
|
||||
#define CONFIG_GATEWAY_MODBUS_SERIAL_RX_PIN -1
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_MODBUS_SERIAL_BAUDRATE
|
||||
#define CONFIG_GATEWAY_MODBUS_SERIAL_BAUDRATE 9600
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_MODBUS_SERIAL_RESPONSE_TIMEOUT_MS
|
||||
#define CONFIG_GATEWAY_MODBUS_SERIAL_RESPONSE_TIMEOUT_MS 20
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_MODBUS_SERIAL_RS485_DE_PIN
|
||||
#define CONFIG_GATEWAY_MODBUS_SERIAL_RS485_DE_PIN -1
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE
|
||||
#define CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE 8192
|
||||
#endif
|
||||
@@ -212,6 +244,25 @@ constexpr gateway::GatewayCachePriorityMode kCachePriorityMode =
|
||||
gateway::GatewayCachePriorityMode::kOutsideBusFirst;
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU) || \
|
||||
defined(CONFIG_GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII)
|
||||
constexpr bool kModbusDefaultSerialTransport = true;
|
||||
#else
|
||||
constexpr bool kModbusDefaultSerialTransport = false;
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GATEWAY_MODBUS_ALLOW_UART0
|
||||
constexpr bool kModbusAllowUart0 = true;
|
||||
#else
|
||||
constexpr bool kModbusAllowUart0 = false;
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_GATEWAY_MODBUS_SERIAL_RS485_ENABLED
|
||||
constexpr bool kModbusSerialRs485Enabled = true;
|
||||
#else
|
||||
constexpr bool kModbusSerialRs485Enabled = false;
|
||||
#endif
|
||||
|
||||
std::unique_ptr<gateway::DaliDomainService> s_dali_domain;
|
||||
std::unique_ptr<gateway::GatewayRuntime> s_runtime;
|
||||
std::unique_ptr<gateway::GatewayCache> s_cache;
|
||||
@@ -322,6 +373,21 @@ bool ValidateChannelBindings() {
|
||||
}
|
||||
}
|
||||
|
||||
if (kModbusBridgeSupported && kModbusDefaultSerialTransport) {
|
||||
const int modbus_uart = CONFIG_GATEWAY_MODBUS_SERIAL_UART_PORT;
|
||||
if (modbus_uart == 0 && !kModbusAllowUart0) {
|
||||
ESP_LOGE(kTag, "Modbus serial is configured on UART0, but UART0 is reserved for console");
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < CONFIG_GATEWAY_CHANNEL_COUNT; ++i) {
|
||||
if (channels[i].enabled && channels[i].serial_phy && channels[i].uart_port == modbus_uart) {
|
||||
ESP_LOGE(kTag, "Modbus serial UART%d conflicts with DALI channel %d serial PHY",
|
||||
modbus_uart, i + 1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!any_enabled) {
|
||||
ESP_LOGE(kTag, "no DALI PHY is configured; enable at least one native or serial channel");
|
||||
return false;
|
||||
@@ -474,9 +540,8 @@ extern "C" void app_main(void) {
|
||||
if (kBridgeSupported) {
|
||||
gateway::GatewayBridgeServiceConfig bridge_config;
|
||||
bridge_config.bridge_enabled = true;
|
||||
bridge_config.modbus_enabled = profile.enable_wifi && kModbusBridgeSupported;
|
||||
bridge_config.modbus_startup_enabled = profile.enable_wifi && kModbusBridgeSupported &&
|
||||
kModbusBridgeStartupEnabled;
|
||||
bridge_config.modbus_enabled = kModbusBridgeSupported;
|
||||
bridge_config.modbus_startup_enabled = kModbusBridgeSupported && kModbusBridgeStartupEnabled;
|
||||
bridge_config.bacnet_enabled = profile.enable_wifi && kBacnetBridgeSupported;
|
||||
bridge_config.bacnet_startup_enabled = profile.enable_wifi && kBacnetBridgeSupported &&
|
||||
kBacnetBridgeStartupEnabled;
|
||||
@@ -487,6 +552,44 @@ extern "C" void app_main(void) {
|
||||
static_cast<uint32_t>(CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE);
|
||||
bridge_config.modbus_task_priority =
|
||||
static_cast<UBaseType_t>(CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_PRIORITY);
|
||||
bridge_config.allow_modbus_uart0 = kModbusAllowUart0;
|
||||
if (!kModbusAllowUart0) {
|
||||
bridge_config.reserved_uart_ports.push_back(0);
|
||||
}
|
||||
#if CONFIG_GATEWAY_CHANNEL1_PHY_UART1
|
||||
bridge_config.reserved_uart_ports.push_back(1);
|
||||
#elif CONFIG_GATEWAY_CHANNEL1_PHY_UART2
|
||||
bridge_config.reserved_uart_ports.push_back(2);
|
||||
#endif
|
||||
#if CONFIG_GATEWAY_CHANNEL_COUNT >= 2
|
||||
#if CONFIG_GATEWAY_CHANNEL2_PHY_UART1
|
||||
bridge_config.reserved_uart_ports.push_back(1);
|
||||
#elif CONFIG_GATEWAY_CHANNEL2_PHY_UART2
|
||||
bridge_config.reserved_uart_ports.push_back(2);
|
||||
#endif
|
||||
#endif
|
||||
if (kModbusBridgeSupported) {
|
||||
gateway::GatewayModbusConfig default_modbus;
|
||||
#if defined(CONFIG_GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU)
|
||||
default_modbus.transport = "rtu-server";
|
||||
#elif defined(CONFIG_GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII)
|
||||
default_modbus.transport = "ascii-server";
|
||||
#else
|
||||
default_modbus.transport = "tcp-server";
|
||||
#endif
|
||||
default_modbus.port = static_cast<uint16_t>(CONFIG_GATEWAY_MODBUS_TCP_PORT);
|
||||
default_modbus.unit_id = static_cast<uint8_t>(CONFIG_GATEWAY_MODBUS_UNIT_ID);
|
||||
default_modbus.serial.uart_port = CONFIG_GATEWAY_MODBUS_SERIAL_UART_PORT;
|
||||
default_modbus.serial.tx_pin = CONFIG_GATEWAY_MODBUS_SERIAL_TX_PIN;
|
||||
default_modbus.serial.rx_pin = CONFIG_GATEWAY_MODBUS_SERIAL_RX_PIN;
|
||||
default_modbus.serial.baudrate =
|
||||
static_cast<uint32_t>(CONFIG_GATEWAY_MODBUS_SERIAL_BAUDRATE);
|
||||
default_modbus.serial.response_timeout_ms =
|
||||
static_cast<uint32_t>(CONFIG_GATEWAY_MODBUS_SERIAL_RESPONSE_TIMEOUT_MS);
|
||||
default_modbus.serial.rs485.enabled = kModbusSerialRs485Enabled;
|
||||
default_modbus.serial.rs485.de_pin = CONFIG_GATEWAY_MODBUS_SERIAL_RS485_DE_PIN;
|
||||
bridge_config.default_modbus_config = default_modbus;
|
||||
}
|
||||
bridge_config.bacnet_task_stack_size =
|
||||
static_cast<uint32_t>(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE);
|
||||
bridge_config.bacnet_task_priority =
|
||||
|
||||
@@ -647,6 +647,11 @@ CONFIG_GATEWAY_SMARTCONFIG_TIMEOUT_SEC=60
|
||||
CONFIG_GATEWAY_BRIDGE_SUPPORTED=y
|
||||
CONFIG_GATEWAY_MODBUS_BRIDGE_SUPPORTED=y
|
||||
# CONFIG_GATEWAY_START_MODBUS_BRIDGE_ENABLED is not set
|
||||
CONFIG_GATEWAY_MODBUS_DEFAULT_TRANSPORT_TCP=y
|
||||
# CONFIG_GATEWAY_MODBUS_DEFAULT_TRANSPORT_RTU is not set
|
||||
# CONFIG_GATEWAY_MODBUS_DEFAULT_TRANSPORT_ASCII is not set
|
||||
CONFIG_GATEWAY_MODBUS_TCP_PORT=1502
|
||||
CONFIG_GATEWAY_MODBUS_UNIT_ID=1
|
||||
CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y
|
||||
# CONFIG_GATEWAY_START_BACNET_BRIDGE_ENABLED is not set
|
||||
CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y
|
||||
|
||||
@@ -2,6 +2,7 @@ set(GATEWAY_BRIDGE_REQUIRES
|
||||
dali_domain
|
||||
dali_cpp
|
||||
espressif__cjson
|
||||
esp_driver_uart
|
||||
freertos
|
||||
gateway_cache
|
||||
gateway_modbus
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "gateway_modbus.hpp"
|
||||
|
||||
namespace gateway {
|
||||
|
||||
@@ -24,6 +26,9 @@ struct GatewayBridgeServiceConfig {
|
||||
bool cloud_startup_enabled{false};
|
||||
uint32_t modbus_task_stack_size{6144};
|
||||
UBaseType_t modbus_task_priority{4};
|
||||
std::optional<GatewayModbusConfig> default_modbus_config;
|
||||
bool allow_modbus_uart0{false};
|
||||
std::vector<int> reserved_uart_ports;
|
||||
uint32_t bacnet_task_stack_size{8192};
|
||||
UBaseType_t bacnet_task_priority{5};
|
||||
};
|
||||
|
||||
@@ -16,12 +16,16 @@
|
||||
#include "gateway_provisioning.hpp"
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "driver/uart.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "lwip/inet.h"
|
||||
#include "lwip/sockets.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -29,6 +33,7 @@
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string_view>
|
||||
#include <sys/time.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -48,6 +53,7 @@ constexpr uint32_t kBacnetGeneratedBinaryInputChannelStride = 32768;
|
||||
constexpr uint32_t kBacnetMaxObjectInstance = 4194303;
|
||||
constexpr uint32_t kBacnetReliabilityNoFaultDetected = 0;
|
||||
constexpr uint32_t kBacnetReliabilityCommunicationFailure = 12;
|
||||
constexpr const char* kModbusManagementPrefix = "@DALIGW";
|
||||
|
||||
struct GatewayBridgeStoredConfig {
|
||||
BridgeRuntimeConfig bridge;
|
||||
@@ -1014,10 +1020,106 @@ bool SendModbusFrame(int sock, const uint8_t* mbap, const std::vector<uint8_t>&
|
||||
return SendAll(sock, frame.data(), frame.size());
|
||||
}
|
||||
|
||||
bool SendModbusException(int sock, const uint8_t* mbap, uint8_t function_code,
|
||||
uint8_t exception_code) {
|
||||
const std::vector<uint8_t> pdu{static_cast<uint8_t>(function_code | 0x80), exception_code};
|
||||
return SendModbusFrame(sock, mbap, pdu);
|
||||
std::vector<uint8_t> ModbusExceptionPdu(uint8_t function_code, uint8_t exception_code) {
|
||||
return {static_cast<uint8_t>(function_code | 0x80), exception_code};
|
||||
}
|
||||
|
||||
uint16_t ModbusCrc16(const uint8_t* data, size_t len) {
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
crc ^= data[i];
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
if ((crc & 0x0001) != 0) {
|
||||
crc = static_cast<uint16_t>((crc >> 1) ^ 0xA001);
|
||||
} else {
|
||||
crc = static_cast<uint16_t>(crc >> 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
uint8_t ModbusAsciiLrc(const uint8_t* data, size_t len) {
|
||||
uint8_t sum = 0;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
sum = static_cast<uint8_t>(sum + data[i]);
|
||||
}
|
||||
return static_cast<uint8_t>(-sum);
|
||||
}
|
||||
|
||||
std::optional<uint8_t> HexNibble(char ch) {
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
return static_cast<uint8_t>(ch - '0');
|
||||
}
|
||||
if (ch >= 'A' && ch <= 'F') {
|
||||
return static_cast<uint8_t>(ch - 'A' + 10);
|
||||
}
|
||||
if (ch >= 'a' && ch <= 'f') {
|
||||
return static_cast<uint8_t>(ch - 'a' + 10);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::vector<uint8_t>> DecodeModbusAsciiLine(std::string_view line) {
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) {
|
||||
line.remove_suffix(1);
|
||||
}
|
||||
if (line.size() < 7 || line.front() != ':' || ((line.size() - 1) % 2) != 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::vector<uint8_t> bytes;
|
||||
bytes.reserve((line.size() - 1) / 2);
|
||||
for (size_t i = 1; i + 1 < line.size(); i += 2) {
|
||||
const auto high = HexNibble(line[i]);
|
||||
const auto low = HexNibble(line[i + 1]);
|
||||
if (!high.has_value() || !low.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
bytes.push_back(static_cast<uint8_t>((high.value() << 4) | low.value()));
|
||||
}
|
||||
uint8_t sum = 0;
|
||||
for (const auto byte : bytes) {
|
||||
sum = static_cast<uint8_t>(sum + byte);
|
||||
}
|
||||
if (sum != 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::string EncodeModbusAsciiLine(const std::vector<uint8_t>& bytes) {
|
||||
constexpr char kHex[] = "0123456789ABCDEF";
|
||||
std::string out;
|
||||
out.reserve(1 + bytes.size() * 2 + 2);
|
||||
out.push_back(':');
|
||||
for (const auto byte : bytes) {
|
||||
out.push_back(kHex[(byte >> 4) & 0x0F]);
|
||||
out.push_back(kHex[byte & 0x0F]);
|
||||
}
|
||||
out.append("\r\n");
|
||||
return out;
|
||||
}
|
||||
|
||||
bool LineStartsWith(std::string_view line, std::string_view prefix) {
|
||||
return line.size() >= prefix.size() && line.substr(0, prefix.size()) == prefix;
|
||||
}
|
||||
|
||||
uart_word_length_t UartWordLength(int bits) {
|
||||
return bits <= 7 ? UART_DATA_7_BITS : UART_DATA_8_BITS;
|
||||
}
|
||||
|
||||
uart_parity_t UartParity(const std::string& parity) {
|
||||
if (parity == "even") {
|
||||
return UART_PARITY_EVEN;
|
||||
}
|
||||
if (parity == "odd") {
|
||||
return UART_PARITY_ODD;
|
||||
}
|
||||
return UART_PARITY_DISABLE;
|
||||
}
|
||||
|
||||
uart_stop_bits_t UartStopBits(int bits) {
|
||||
return bits >= 2 ? UART_STOP_BITS_2 : UART_STOP_BITS_1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1062,6 +1164,12 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
bool modbus_started{false};
|
||||
bool bacnet_started{false};
|
||||
TaskHandle_t modbus_task_handle{nullptr};
|
||||
std::atomic_bool modbus_stop_requested{false};
|
||||
std::atomic_bool modbus_restart_requested{false};
|
||||
int modbus_listen_sock{-1};
|
||||
int modbus_client_sock{-1};
|
||||
int modbus_uart_port{-1};
|
||||
std::string modbus_last_error;
|
||||
|
||||
struct DiagnosticSnapshotCacheEntry {
|
||||
DaliDomainSnapshot snapshot;
|
||||
@@ -1709,10 +1817,29 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
if (modbus_json != nullptr) {
|
||||
cJSON_AddBoolToObject(modbus_json, "enabled", service_config.modbus_enabled);
|
||||
cJSON_AddBoolToObject(modbus_json, "started", modbus_started);
|
||||
cJSON_AddStringToObject(modbus_json, "lastError", modbus_last_error.c_str());
|
||||
if (modbus_config.has_value()) {
|
||||
cJSON_AddStringToObject(modbus_json, "transport", modbus_config->transport.c_str());
|
||||
cJSON_AddNumberToObject(modbus_json, "port", modbus_config->port);
|
||||
cJSON_AddNumberToObject(modbus_json, "unitID", modbus_config->unit_id);
|
||||
if (GatewayModbusTransportIsSerial(modbus_config->transport)) {
|
||||
cJSON* serial_json = cJSON_CreateObject();
|
||||
if (serial_json != nullptr) {
|
||||
cJSON_AddNumberToObject(serial_json, "uartPort", modbus_config->serial.uart_port);
|
||||
cJSON_AddNumberToObject(serial_json, "txPin", modbus_config->serial.tx_pin);
|
||||
cJSON_AddNumberToObject(serial_json, "rxPin", modbus_config->serial.rx_pin);
|
||||
cJSON_AddNumberToObject(serial_json, "baudrate", modbus_config->serial.baudrate);
|
||||
cJSON_AddStringToObject(serial_json, "parity", modbus_config->serial.parity.c_str());
|
||||
cJSON_AddNumberToObject(serial_json, "stopBits", modbus_config->serial.stop_bits);
|
||||
cJSON* rs485_json = cJSON_CreateObject();
|
||||
if (rs485_json != nullptr) {
|
||||
cJSON_AddBoolToObject(rs485_json, "enabled", modbus_config->serial.rs485.enabled);
|
||||
cJSON_AddNumberToObject(rs485_json, "dePin", modbus_config->serial.rs485.de_pin);
|
||||
cJSON_AddItemToObject(serial_json, "rs485", rs485_json);
|
||||
}
|
||||
cJSON_AddItemToObject(modbus_json, "serial", serial_json);
|
||||
}
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToObject(root, "modbus", modbus_json);
|
||||
}
|
||||
@@ -2404,7 +2531,190 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
return result.ok;
|
||||
}
|
||||
|
||||
esp_err_t startModbus(std::set<uint16_t>* used_ports = nullptr) {
|
||||
std::optional<GatewayModbusConfig> activeModbusConfigLocked() const {
|
||||
if (modbus_config.has_value()) {
|
||||
return modbus_config;
|
||||
}
|
||||
return service_config.default_modbus_config;
|
||||
}
|
||||
|
||||
std::optional<GatewayModbusConfig> activeModbusConfig() const {
|
||||
LockGuard guard(lock);
|
||||
return activeModbusConfigLocked();
|
||||
}
|
||||
|
||||
bool isReservedUartLocked(int uart_port) const {
|
||||
return std::find(service_config.reserved_uart_ports.begin(),
|
||||
service_config.reserved_uart_ports.end(), uart_port) !=
|
||||
service_config.reserved_uart_ports.end();
|
||||
}
|
||||
|
||||
esp_err_t validateSerialModbusConfigLocked(const GatewayModbusConfig& config) const {
|
||||
const int uart_port = config.serial.uart_port;
|
||||
if (uart_port < 0 || uart_port > 2) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (uart_port == 0 && !service_config.allow_modbus_uart0) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (isReservedUartLocked(uart_port)) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t saveModbusConfig(const GatewayModbusConfig& config) {
|
||||
LockGuard guard(lock);
|
||||
BridgeProvisioningStore store(bridgeNamespace());
|
||||
const esp_err_t err = store.saveObject(
|
||||
kBridgeConfigKey,
|
||||
GatewayBridgeStoredConfigToValue(bridge_config, config, bacnet_server_config));
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
modbus_config = config;
|
||||
bridge_config_loaded = true;
|
||||
if (modbus != nullptr) {
|
||||
modbus->setConfig(config);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> processModbusPdu(const GatewayModbusConfig& config,
|
||||
uint8_t unit_id,
|
||||
const std::vector<uint8_t>& pdu) {
|
||||
if (pdu.empty()) {
|
||||
return {};
|
||||
}
|
||||
if (config.unit_id != 0 && unit_id != config.unit_id) {
|
||||
return ModbusExceptionPdu(pdu[0], 0x0B);
|
||||
}
|
||||
|
||||
if ((pdu[0] == 0x01 || pdu[0] == 0x02) && pdu.size() == 5) {
|
||||
const auto space = GatewayModbusReadSpaceForFunction(pdu[0]);
|
||||
const uint16_t start_address = ReadBe16(&pdu[1]);
|
||||
const uint16_t quantity = ReadBe16(&pdu[3]);
|
||||
if (!space.has_value() || quantity == 0 || quantity > kGatewayModbusMaxReadBits) {
|
||||
return ModbusExceptionPdu(pdu[0], 0x03);
|
||||
}
|
||||
const uint8_t byte_count = static_cast<uint8_t>((quantity + 7U) / 8U);
|
||||
std::vector<uint8_t> response(2 + byte_count, 0);
|
||||
response[0] = pdu[0];
|
||||
response[1] = byte_count;
|
||||
for (uint16_t index = 0; index < quantity; ++index) {
|
||||
const auto human_address = static_cast<uint16_t>(
|
||||
GatewayModbusHumanAddressFromWire(space.value(), start_address + index));
|
||||
const auto value = readModbusBoolPoint(space.value(), human_address);
|
||||
if (!value.has_value()) {
|
||||
return ModbusExceptionPdu(pdu[0], 0x02);
|
||||
}
|
||||
if (value.value()) {
|
||||
response[2 + (index / 8)] |= static_cast<uint8_t>(1U << (index % 8));
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
if ((pdu[0] == 0x03 || pdu[0] == 0x04) && pdu.size() == 5) {
|
||||
const auto space = GatewayModbusReadSpaceForFunction(pdu[0]);
|
||||
const uint16_t start_address = ReadBe16(&pdu[1]);
|
||||
const uint16_t quantity = ReadBe16(&pdu[3]);
|
||||
if (!space.has_value() || quantity == 0 || quantity > kGatewayModbusMaxReadRegisters) {
|
||||
return ModbusExceptionPdu(pdu[0], 0x03);
|
||||
}
|
||||
std::vector<uint8_t> response(2 + quantity * 2);
|
||||
response[0] = pdu[0];
|
||||
response[1] = static_cast<uint8_t>(quantity * 2);
|
||||
for (uint16_t index = 0; index < quantity; ++index) {
|
||||
const auto human_address = static_cast<uint16_t>(
|
||||
GatewayModbusHumanAddressFromWire(space.value(), start_address + index));
|
||||
const auto value = readModbusRegisterPoint(space.value(), human_address);
|
||||
if (!value.has_value()) {
|
||||
return ModbusExceptionPdu(pdu[0], 0x02);
|
||||
}
|
||||
WriteBe16(&response[2 + index * 2], value.value());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
if (pdu[0] == 0x05 && pdu.size() == 5) {
|
||||
const uint16_t wire_address = ReadBe16(&pdu[1]);
|
||||
const uint16_t raw_value = ReadBe16(&pdu[3]);
|
||||
if (raw_value != 0x0000 && raw_value != 0xFF00) {
|
||||
return ModbusExceptionPdu(pdu[0], 0x03);
|
||||
}
|
||||
const auto coil = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
||||
GatewayModbusSpace::kCoil, wire_address));
|
||||
if (!writeModbusCoilPoint(coil, raw_value == 0xFF00)) {
|
||||
return ModbusExceptionPdu(pdu[0], 0x04);
|
||||
}
|
||||
return pdu;
|
||||
}
|
||||
|
||||
if (pdu[0] == 0x06 && pdu.size() == 5) {
|
||||
const uint16_t wire_register = ReadBe16(&pdu[1]);
|
||||
const uint16_t value = ReadBe16(&pdu[3]);
|
||||
const auto holding_register = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
||||
GatewayModbusSpace::kHoldingRegister, wire_register));
|
||||
if (!writeModbusRegisterPoint(holding_register, value)) {
|
||||
return ModbusExceptionPdu(pdu[0], 0x04);
|
||||
}
|
||||
return pdu;
|
||||
}
|
||||
|
||||
if (pdu[0] == 0x0F && pdu.size() >= 6) {
|
||||
const uint16_t start_address = ReadBe16(&pdu[1]);
|
||||
const uint16_t quantity = ReadBe16(&pdu[3]);
|
||||
const uint8_t byte_count = pdu[5];
|
||||
if (quantity == 0 || quantity > kGatewayModbusMaxWriteBits ||
|
||||
pdu.size() != static_cast<size_t>(6 + byte_count) ||
|
||||
byte_count != static_cast<uint8_t>((quantity + 7U) / 8U)) {
|
||||
return ModbusExceptionPdu(pdu[0], 0x03);
|
||||
}
|
||||
for (uint16_t index = 0; index < quantity; ++index) {
|
||||
const bool value = (pdu[6 + (index / 8)] & (1U << (index % 8))) != 0;
|
||||
const auto coil = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
||||
GatewayModbusSpace::kCoil, start_address + index));
|
||||
if (!writeModbusCoilPoint(coil, value)) {
|
||||
return ModbusExceptionPdu(pdu[0], 0x04);
|
||||
}
|
||||
}
|
||||
std::vector<uint8_t> response(5);
|
||||
response[0] = pdu[0];
|
||||
WriteBe16(&response[1], start_address);
|
||||
WriteBe16(&response[3], quantity);
|
||||
return response;
|
||||
}
|
||||
|
||||
if (pdu[0] == 0x10 && pdu.size() >= 6) {
|
||||
const uint16_t start_register = ReadBe16(&pdu[1]);
|
||||
const uint16_t quantity = ReadBe16(&pdu[3]);
|
||||
const uint8_t byte_count = pdu[5];
|
||||
if (quantity == 0 || quantity > kGatewayModbusMaxWriteRegisters ||
|
||||
pdu.size() != static_cast<size_t>(6 + byte_count) || byte_count != quantity * 2) {
|
||||
return ModbusExceptionPdu(pdu[0], 0x03);
|
||||
}
|
||||
for (uint16_t index = 0; index < quantity; ++index) {
|
||||
const size_t offset = 6 + (index * 2);
|
||||
const uint16_t value = ReadBe16(&pdu[offset]);
|
||||
const auto holding_register = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
||||
GatewayModbusSpace::kHoldingRegister, start_register + index));
|
||||
if (!writeModbusRegisterPoint(holding_register, value)) {
|
||||
return ModbusExceptionPdu(pdu[0], 0x04);
|
||||
}
|
||||
}
|
||||
std::vector<uint8_t> response(5);
|
||||
response[0] = pdu[0];
|
||||
WriteBe16(&response[1], start_register);
|
||||
WriteBe16(&response[3], quantity);
|
||||
return response;
|
||||
}
|
||||
|
||||
return ModbusExceptionPdu(pdu[0], 0x01);
|
||||
}
|
||||
|
||||
esp_err_t startModbus(std::set<uint16_t>* used_ports = nullptr,
|
||||
std::set<int>* used_uarts = nullptr) {
|
||||
LockGuard guard(lock);
|
||||
if (!service_config.modbus_enabled) {
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
@@ -2412,19 +2722,43 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
if (modbus_started || modbus_task_handle != nullptr) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (!modbus_config.has_value()) {
|
||||
const auto config = activeModbusConfigLocked();
|
||||
if (!config.has_value()) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
const uint16_t port = modbus_config->port == 0 ? kGatewayModbusDefaultTcpPort
|
||||
: modbus_config->port;
|
||||
if (used_ports != nullptr) {
|
||||
if (GatewayModbusTransportIsSerial(config->transport)) {
|
||||
const esp_err_t serial_err = validateSerialModbusConfigLocked(config.value());
|
||||
if (serial_err != ESP_OK) {
|
||||
modbus_last_error = "invalid or reserved Modbus serial UART";
|
||||
return serial_err;
|
||||
}
|
||||
if (used_uarts != nullptr) {
|
||||
const int uart_port = config->serial.uart_port;
|
||||
if (used_uarts->find(uart_port) != used_uarts->end()) {
|
||||
ESP_LOGW(kTag, "gateway=%u skips duplicate Modbus serial UART%d", channel.gateway_id,
|
||||
uart_port);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
used_uarts->insert(uart_port);
|
||||
}
|
||||
}
|
||||
const uint16_t port = config->port == 0 ? kGatewayModbusDefaultTcpPort : config->port;
|
||||
if (GatewayModbusTransportIsTcp(config->transport) && used_ports != nullptr) {
|
||||
if (used_ports->find(port) != used_ports->end()) {
|
||||
ESP_LOGW(kTag, "gateway=%u skips duplicate Modbus TCP port %u", channel.gateway_id, port);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
used_ports->insert(port);
|
||||
}
|
||||
const BaseType_t created = xTaskCreate(&ChannelRuntime::ModbusTaskEntry, "gw_modbus_tcp",
|
||||
modbus_stop_requested = false;
|
||||
modbus_restart_requested = false;
|
||||
modbus_last_error.clear();
|
||||
const char* task_name = GatewayModbusTransportIsTcp(config->transport)
|
||||
? "gw_modbus_tcp"
|
||||
: (GatewayModbusTransportIsAscii(config->transport)
|
||||
? "gw_modbus_ascii"
|
||||
: "gw_modbus_rtu");
|
||||
const BaseType_t created = xTaskCreate(&ChannelRuntime::ModbusTaskEntry, task_name,
|
||||
service_config.modbus_task_stack_size, this,
|
||||
service_config.modbus_task_priority,
|
||||
&modbus_task_handle);
|
||||
@@ -2436,21 +2770,74 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t stopModbus() {
|
||||
LockGuard guard(lock);
|
||||
if (!modbus_started && modbus_task_handle == nullptr) {
|
||||
return ESP_OK;
|
||||
}
|
||||
modbus_stop_requested = true;
|
||||
modbus_restart_requested = false;
|
||||
if (modbus_client_sock >= 0) {
|
||||
shutdown(modbus_client_sock, SHUT_RDWR);
|
||||
}
|
||||
if (modbus_listen_sock >= 0) {
|
||||
shutdown(modbus_listen_sock, SHUT_RDWR);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void modbusTaskLoop() {
|
||||
const uint16_t port = modbus_config.has_value() && modbus_config->port != 0
|
||||
? modbus_config->port
|
||||
: kGatewayModbusDefaultTcpPort;
|
||||
const auto config = activeModbusConfig();
|
||||
if (!config.has_value()) {
|
||||
modbus_last_error = "missing Modbus config";
|
||||
finishModbusTask();
|
||||
return;
|
||||
}
|
||||
if (GatewayModbusTransportIsTcp(config->transport)) {
|
||||
modbusTcpTaskLoop(config.value());
|
||||
} else if (GatewayModbusTransportIsSerial(config->transport)) {
|
||||
modbusSerialTaskLoop(config.value());
|
||||
} else {
|
||||
modbus_last_error = "unsupported Modbus transport";
|
||||
ESP_LOGE(kTag, "gateway=%u unsupported Modbus transport %s", channel.gateway_id,
|
||||
config->transport.c_str());
|
||||
}
|
||||
finishModbusTask();
|
||||
}
|
||||
|
||||
void finishModbusTask() {
|
||||
const bool restart = modbus_restart_requested.exchange(false);
|
||||
{
|
||||
LockGuard guard(lock);
|
||||
modbus_started = false;
|
||||
modbus_task_handle = nullptr;
|
||||
modbus_stop_requested = false;
|
||||
modbus_listen_sock = -1;
|
||||
modbus_client_sock = -1;
|
||||
modbus_uart_port = -1;
|
||||
}
|
||||
if (restart) {
|
||||
startModbus();
|
||||
}
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
void modbusTcpTaskLoop(const GatewayModbusConfig& config) {
|
||||
const uint16_t port = config.port != 0 ? config.port : kGatewayModbusDefaultTcpPort;
|
||||
const int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
||||
if (listen_sock < 0) {
|
||||
ESP_LOGE(kTag, "gateway=%u failed to create Modbus socket", channel.gateway_id);
|
||||
modbus_started = false;
|
||||
modbus_task_handle = nullptr;
|
||||
vTaskDelete(nullptr);
|
||||
modbus_last_error = "failed to create Modbus TCP socket";
|
||||
return;
|
||||
}
|
||||
modbus_listen_sock = listen_sock;
|
||||
|
||||
int reuse = 1;
|
||||
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
|
||||
timeval timeout{};
|
||||
timeout.tv_sec = 1;
|
||||
timeout.tv_usec = 0;
|
||||
setsockopt(listen_sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
|
||||
sockaddr_in address = {};
|
||||
address.sin_family = AF_INET;
|
||||
@@ -2460,15 +2847,13 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
if (bind(listen_sock, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0 ||
|
||||
listen(listen_sock, 2) != 0) {
|
||||
ESP_LOGE(kTag, "gateway=%u failed to bind Modbus TCP port %u", channel.gateway_id, port);
|
||||
modbus_last_error = "failed to bind Modbus TCP port";
|
||||
close(listen_sock);
|
||||
modbus_started = false;
|
||||
modbus_task_handle = nullptr;
|
||||
vTaskDelete(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(kTag, "gateway=%u Modbus TCP listening on port %u", channel.gateway_id, port);
|
||||
while (true) {
|
||||
while (!modbus_stop_requested) {
|
||||
sockaddr_in client_address = {};
|
||||
socklen_t client_len = sizeof(client_address);
|
||||
const int client_sock = accept(listen_sock, reinterpret_cast<sockaddr*>(&client_address),
|
||||
@@ -2476,14 +2861,19 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
if (client_sock < 0) {
|
||||
continue;
|
||||
}
|
||||
handleModbusClient(client_sock);
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
modbus_client_sock = client_sock;
|
||||
handleModbusClient(config, client_sock);
|
||||
modbus_client_sock = -1;
|
||||
close(client_sock);
|
||||
}
|
||||
close(listen_sock);
|
||||
modbus_listen_sock = -1;
|
||||
}
|
||||
|
||||
void handleModbusClient(int client_sock) {
|
||||
void handleModbusClient(const GatewayModbusConfig& config, int client_sock) {
|
||||
uint8_t header[7] = {};
|
||||
while (RecvAll(client_sock, header, sizeof(header))) {
|
||||
while (!modbus_stop_requested && RecvAll(client_sock, header, sizeof(header))) {
|
||||
const uint16_t protocol_id = ReadBe16(&header[2]);
|
||||
const uint16_t length = ReadBe16(&header[4]);
|
||||
if (protocol_id != 0 || length < 2 || length > kGatewayModbusMaxPduBytes) {
|
||||
@@ -2494,173 +2884,263 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
if (!RecvAll(client_sock, pdu.data(), pdu.size()) || pdu.empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (modbus_config.has_value() && modbus_config->unit_id != 0 &&
|
||||
header[6] != modbus_config->unit_id) {
|
||||
SendModbusException(client_sock, header, pdu[0], 0x0B);
|
||||
continue;
|
||||
SendModbusFrame(client_sock, header, processModbusPdu(config, header[6], pdu));
|
||||
}
|
||||
}
|
||||
|
||||
if ((pdu[0] == 0x01 || pdu[0] == 0x02) && pdu.size() == 5) {
|
||||
const auto space = GatewayModbusReadSpaceForFunction(pdu[0]);
|
||||
const uint16_t start_address = ReadBe16(&pdu[1]);
|
||||
const uint16_t quantity = ReadBe16(&pdu[3]);
|
||||
if (!space.has_value() || quantity == 0 || quantity > kGatewayModbusMaxReadBits) {
|
||||
SendModbusException(client_sock, header, pdu[0], 0x03);
|
||||
continue;
|
||||
esp_err_t installSerialModbus(const GatewayModbusConfig& config) {
|
||||
const auto uart_port = static_cast<uart_port_t>(config.serial.uart_port);
|
||||
uart_config_t uart_config{};
|
||||
uart_config.baud_rate = static_cast<int>(config.serial.baudrate);
|
||||
uart_config.data_bits = UartWordLength(config.serial.data_bits);
|
||||
uart_config.parity = UartParity(config.serial.parity);
|
||||
uart_config.stop_bits = UartStopBits(config.serial.stop_bits);
|
||||
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
|
||||
uart_config.source_clk = UART_SCLK_DEFAULT;
|
||||
|
||||
esp_err_t err = uart_param_config(uart_port, &uart_config);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
const uint8_t byte_count = static_cast<uint8_t>((quantity + 7U) / 8U);
|
||||
std::vector<uint8_t> response(2 + byte_count, 0);
|
||||
response[0] = pdu[0];
|
||||
response[1] = byte_count;
|
||||
bool ok = true;
|
||||
for (uint16_t index = 0; index < quantity; ++index) {
|
||||
const auto human_address = static_cast<uint16_t>(
|
||||
GatewayModbusHumanAddressFromWire(space.value(), start_address + index));
|
||||
const auto value = readModbusBoolPoint(space.value(), human_address);
|
||||
if (!value.has_value()) {
|
||||
ok = false;
|
||||
break;
|
||||
const int rts_pin = config.serial.rs485.enabled ? config.serial.rs485.de_pin
|
||||
: UART_PIN_NO_CHANGE;
|
||||
err = uart_set_pin(uart_port, config.serial.tx_pin, config.serial.rx_pin, rts_pin,
|
||||
UART_PIN_NO_CHANGE);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
if (value.value()) {
|
||||
response[2 + (index / 8)] |= static_cast<uint8_t>(1U << (index % 8));
|
||||
err = uart_driver_install(uart_port, static_cast<int>(config.serial.rx_buffer_size),
|
||||
static_cast<int>(config.serial.tx_buffer_size), 0, nullptr, 0);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
if (config.serial.rs485.enabled) {
|
||||
err = uart_set_mode(uart_port, UART_MODE_RS485_HALF_DUPLEX);
|
||||
if (err != ESP_OK) {
|
||||
uart_driver_delete(uart_port);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
SendModbusException(client_sock, header, pdu[0], 0x02);
|
||||
continue;
|
||||
}
|
||||
SendModbusFrame(client_sock, header, response);
|
||||
continue;
|
||||
modbus_uart_port = config.serial.uart_port;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if ((pdu[0] == 0x03 || pdu[0] == 0x04) && pdu.size() == 5) {
|
||||
const auto space = GatewayModbusReadSpaceForFunction(pdu[0]);
|
||||
const uint16_t start_address = ReadBe16(&pdu[1]);
|
||||
const uint16_t quantity = ReadBe16(&pdu[3]);
|
||||
if (!space.has_value() || quantity == 0 || quantity > kGatewayModbusMaxReadRegisters) {
|
||||
SendModbusException(client_sock, header, pdu[0], 0x03);
|
||||
continue;
|
||||
void modbusSerialTaskLoop(const GatewayModbusConfig& config) {
|
||||
const esp_err_t err = installSerialModbus(config);
|
||||
if (err != ESP_OK) {
|
||||
modbus_last_error = "failed to install Modbus serial UART";
|
||||
ESP_LOGE(kTag, "gateway=%u failed to install Modbus serial UART%d: %s",
|
||||
channel.gateway_id, config.serial.uart_port, esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
std::vector<uint8_t> response(2 + quantity * 2);
|
||||
response[0] = pdu[0];
|
||||
response[1] = static_cast<uint8_t>(quantity * 2);
|
||||
bool ok = true;
|
||||
for (uint16_t index = 0; index < quantity; ++index) {
|
||||
const auto human_address = static_cast<uint16_t>(
|
||||
GatewayModbusHumanAddressFromWire(space.value(), start_address + index));
|
||||
const auto value = readModbusRegisterPoint(space.value(), human_address);
|
||||
if (!value.has_value()) {
|
||||
ok = false;
|
||||
break;
|
||||
ESP_LOGI(kTag, "gateway=%u Modbus %s listening on UART%d baud=%lu", channel.gateway_id,
|
||||
GatewayModbusTransportIsAscii(config.transport) ? "ASCII" : "RTU",
|
||||
config.serial.uart_port, static_cast<unsigned long>(config.serial.baudrate));
|
||||
if (GatewayModbusTransportIsAscii(config.transport)) {
|
||||
modbusAsciiTaskLoop(config);
|
||||
} else {
|
||||
modbusRtuTaskLoop(config);
|
||||
}
|
||||
WriteBe16(&response[2 + index * 2], value.value());
|
||||
}
|
||||
if (!ok) {
|
||||
SendModbusException(client_sock, header, pdu[0], 0x02);
|
||||
continue;
|
||||
}
|
||||
SendModbusFrame(client_sock, header, response);
|
||||
continue;
|
||||
uart_driver_delete(static_cast<uart_port_t>(config.serial.uart_port));
|
||||
}
|
||||
|
||||
if (pdu[0] == 0x05 && pdu.size() == 5) {
|
||||
const uint16_t wire_address = ReadBe16(&pdu[1]);
|
||||
const uint16_t raw_value = ReadBe16(&pdu[3]);
|
||||
if (raw_value != 0x0000 && raw_value != 0xFF00) {
|
||||
SendModbusException(client_sock, header, pdu[0], 0x03);
|
||||
void modbusRtuTaskLoop(const GatewayModbusConfig& config) {
|
||||
const auto uart_port = static_cast<uart_port_t>(config.serial.uart_port);
|
||||
std::vector<uint8_t> frame;
|
||||
std::array<uint8_t, 128> read_buffer{};
|
||||
const TickType_t timeout = pdMS_TO_TICKS(config.serial.response_timeout_ms);
|
||||
while (!modbus_stop_requested) {
|
||||
const int read_len = uart_read_bytes(uart_port, read_buffer.data(), read_buffer.size(), timeout);
|
||||
if (read_len > 0) {
|
||||
frame.insert(frame.end(), read_buffer.begin(), read_buffer.begin() + read_len);
|
||||
if (!frame.empty() && frame.front() == '@' &&
|
||||
std::find(frame.begin(), frame.end(), '\n') != frame.end()) {
|
||||
const std::string line(frame.begin(), frame.end());
|
||||
handleModbusManagementLine(config.serial.uart_port, line);
|
||||
frame.clear();
|
||||
} else if (frame.size() > 512) {
|
||||
frame.clear();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const auto coil = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
||||
GatewayModbusSpace::kCoil, wire_address));
|
||||
if (!writeModbusCoilPoint(coil, raw_value == 0xFF00)) {
|
||||
SendModbusException(client_sock, header, pdu[0], 0x04);
|
||||
if (frame.empty() || frame.front() == '@') {
|
||||
continue;
|
||||
}
|
||||
SendModbusFrame(client_sock, header, pdu);
|
||||
continue;
|
||||
handleModbusRtuFrame(config, frame);
|
||||
frame.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (pdu[0] == 0x06 && pdu.size() == 5) {
|
||||
const uint16_t wire_register = ReadBe16(&pdu[1]);
|
||||
const uint16_t value = ReadBe16(&pdu[3]);
|
||||
const auto holding_register = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
||||
GatewayModbusSpace::kHoldingRegister, wire_register));
|
||||
if (!writeModbusRegisterPoint(holding_register, value)) {
|
||||
SendModbusException(client_sock, header, pdu[0], 0x04);
|
||||
continue;
|
||||
void handleModbusRtuFrame(const GatewayModbusConfig& config, const std::vector<uint8_t>& frame) {
|
||||
if (frame.size() < 4) {
|
||||
return;
|
||||
}
|
||||
SendModbusFrame(client_sock, header, pdu);
|
||||
continue;
|
||||
const uint16_t received_crc = static_cast<uint16_t>(frame[frame.size() - 2] |
|
||||
(frame[frame.size() - 1] << 8));
|
||||
if (ModbusCrc16(frame.data(), frame.size() - 2) != received_crc) {
|
||||
return;
|
||||
}
|
||||
const uint8_t unit_id = frame[0];
|
||||
if (unit_id == 0) {
|
||||
return;
|
||||
}
|
||||
const std::vector<uint8_t> pdu(frame.begin() + 1, frame.end() - 2);
|
||||
const auto response_pdu = processModbusPdu(config, unit_id, pdu);
|
||||
if (response_pdu.empty()) {
|
||||
return;
|
||||
}
|
||||
std::vector<uint8_t> response;
|
||||
response.reserve(1 + response_pdu.size() + 2);
|
||||
response.push_back(unit_id);
|
||||
response.insert(response.end(), response_pdu.begin(), response_pdu.end());
|
||||
const uint16_t crc = ModbusCrc16(response.data(), response.size());
|
||||
response.push_back(static_cast<uint8_t>(crc & 0xFF));
|
||||
response.push_back(static_cast<uint8_t>((crc >> 8) & 0xFF));
|
||||
uart_write_bytes(static_cast<uart_port_t>(config.serial.uart_port), response.data(),
|
||||
response.size());
|
||||
}
|
||||
|
||||
if (pdu[0] == 0x0F && pdu.size() >= 6) {
|
||||
const uint16_t start_address = ReadBe16(&pdu[1]);
|
||||
const uint16_t quantity = ReadBe16(&pdu[3]);
|
||||
const uint8_t byte_count = pdu[5];
|
||||
if (quantity == 0 || quantity > kGatewayModbusMaxWriteBits ||
|
||||
pdu.size() != static_cast<size_t>(6 + byte_count) ||
|
||||
byte_count != static_cast<uint8_t>((quantity + 7U) / 8U)) {
|
||||
SendModbusException(client_sock, header, pdu[0], 0x03);
|
||||
void modbusAsciiTaskLoop(const GatewayModbusConfig& config) {
|
||||
const auto uart_port = static_cast<uart_port_t>(config.serial.uart_port);
|
||||
std::string line;
|
||||
std::array<uint8_t, 128> read_buffer{};
|
||||
const TickType_t timeout = pdMS_TO_TICKS(config.serial.response_timeout_ms);
|
||||
while (!modbus_stop_requested) {
|
||||
const int read_len = uart_read_bytes(uart_port, read_buffer.data(), read_buffer.size(), timeout);
|
||||
if (read_len <= 0) {
|
||||
continue;
|
||||
}
|
||||
bool ok = true;
|
||||
for (uint16_t index = 0; index < quantity; ++index) {
|
||||
const bool value = (pdu[6 + (index / 8)] & (1U << (index % 8))) != 0;
|
||||
const auto coil = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
||||
GatewayModbusSpace::kCoil, start_address + index));
|
||||
if (!writeModbusCoilPoint(coil, value)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
SendModbusException(client_sock, header, pdu[0], 0x04);
|
||||
for (int i = 0; i < read_len; ++i) {
|
||||
const char ch = static_cast<char>(read_buffer[i]);
|
||||
if (line.empty()) {
|
||||
if (ch != ':' && ch != '@') {
|
||||
continue;
|
||||
}
|
||||
std::vector<uint8_t> response(5);
|
||||
response[0] = pdu[0];
|
||||
WriteBe16(&response[1], start_address);
|
||||
WriteBe16(&response[3], quantity);
|
||||
SendModbusFrame(client_sock, header, response);
|
||||
continue;
|
||||
}
|
||||
line.push_back(ch);
|
||||
if (ch == '\n') {
|
||||
if (LineStartsWith(line, kModbusManagementPrefix)) {
|
||||
handleModbusManagementLine(config.serial.uart_port, line);
|
||||
} else if (!line.empty() && line.front() == ':') {
|
||||
handleModbusAsciiFrame(config, line);
|
||||
}
|
||||
line.clear();
|
||||
} else if (line.size() > 1024) {
|
||||
line.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pdu[0] == 0x10 && pdu.size() >= 6) {
|
||||
const uint16_t start_register = ReadBe16(&pdu[1]);
|
||||
const uint16_t quantity = ReadBe16(&pdu[3]);
|
||||
const uint8_t byte_count = pdu[5];
|
||||
if (quantity == 0 || quantity > kGatewayModbusMaxWriteRegisters ||
|
||||
pdu.size() != static_cast<size_t>(6 + byte_count) ||
|
||||
byte_count != quantity * 2) {
|
||||
SendModbusException(client_sock, header, pdu[0], 0x03);
|
||||
continue;
|
||||
void handleModbusAsciiFrame(const GatewayModbusConfig& config, std::string_view line) {
|
||||
const auto decoded = DecodeModbusAsciiLine(line);
|
||||
if (!decoded.has_value() || decoded->size() < 4) {
|
||||
return;
|
||||
}
|
||||
bool ok = true;
|
||||
for (uint16_t index = 0; index < quantity; ++index) {
|
||||
const size_t offset = 6 + (index * 2);
|
||||
const uint16_t value = ReadBe16(&pdu[offset]);
|
||||
const auto holding_register = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
||||
GatewayModbusSpace::kHoldingRegister, start_register + index));
|
||||
if (!writeModbusRegisterPoint(holding_register, value)) {
|
||||
ok = false;
|
||||
break;
|
||||
const uint8_t unit_id = decoded->front();
|
||||
if (unit_id == 0) {
|
||||
return;
|
||||
}
|
||||
const std::vector<uint8_t> pdu(decoded->begin() + 1, decoded->end() - 1);
|
||||
const auto response_pdu = processModbusPdu(config, unit_id, pdu);
|
||||
if (response_pdu.empty()) {
|
||||
return;
|
||||
}
|
||||
if (!ok) {
|
||||
SendModbusException(client_sock, header, pdu[0], 0x04);
|
||||
continue;
|
||||
}
|
||||
std::vector<uint8_t> response(5);
|
||||
response[0] = pdu[0];
|
||||
WriteBe16(&response[1], start_register);
|
||||
WriteBe16(&response[3], quantity);
|
||||
SendModbusFrame(client_sock, header, response);
|
||||
continue;
|
||||
std::vector<uint8_t> response;
|
||||
response.reserve(1 + response_pdu.size() + 1);
|
||||
response.push_back(unit_id);
|
||||
response.insert(response.end(), response_pdu.begin(), response_pdu.end());
|
||||
response.push_back(ModbusAsciiLrc(response.data(), response.size()));
|
||||
const std::string encoded = EncodeModbusAsciiLine(response);
|
||||
uart_write_bytes(static_cast<uart_port_t>(config.serial.uart_port), encoded.data(),
|
||||
encoded.size());
|
||||
}
|
||||
|
||||
SendModbusException(client_sock, header, pdu[0], 0x01);
|
||||
void handleModbusManagementLine(int uart_port, std::string_view line) {
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) {
|
||||
line.remove_suffix(1);
|
||||
}
|
||||
if (!LineStartsWith(line, kModbusManagementPrefix)) {
|
||||
return;
|
||||
}
|
||||
line.remove_prefix(std::char_traits<char>::length(kModbusManagementPrefix));
|
||||
while (!line.empty() && line.front() == ' ') {
|
||||
line.remove_prefix(1);
|
||||
}
|
||||
cJSON* root = line.empty() ? cJSON_CreateObject() : cJSON_ParseWithLength(line.data(), line.size());
|
||||
if (root == nullptr || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
writeModbusManagementResponse(uart_port, false, "unknown", "invalid JSON");
|
||||
return;
|
||||
}
|
||||
const auto gateway_id = JsonGatewayId(root);
|
||||
if (gateway_id.has_value() && gateway_id.value() != channel.gateway_id) {
|
||||
cJSON_Delete(root);
|
||||
writeModbusManagementResponse(uart_port, false, "unknown", "gateway id mismatch");
|
||||
return;
|
||||
}
|
||||
const char* action_raw = JsonString(root, "action");
|
||||
const std::string action = action_raw == nullptr ? "modbus_status" : action_raw;
|
||||
if (action == "modbus_config") {
|
||||
const cJSON* modbus_node = cJSON_GetObjectItemCaseSensitive(root, "modbus");
|
||||
if (modbus_node == nullptr) {
|
||||
modbus_node = root;
|
||||
}
|
||||
const DaliValue modbus_value = FromCjson(modbus_node);
|
||||
const auto parsed = GatewayModbusConfigFromValue(&modbus_value);
|
||||
if (!parsed.has_value()) {
|
||||
cJSON_Delete(root);
|
||||
writeModbusManagementResponse(uart_port, false, action.c_str(), "invalid modbus config");
|
||||
return;
|
||||
}
|
||||
const esp_err_t err = saveModbusConfig(parsed.value());
|
||||
cJSON_Delete(root);
|
||||
if (err != ESP_OK) {
|
||||
writeModbusManagementResponse(uart_port, false, action.c_str(), esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
writeModbusManagementResponse(uart_port, true, action.c_str(), nullptr);
|
||||
modbus_restart_requested = true;
|
||||
modbus_stop_requested = true;
|
||||
return;
|
||||
}
|
||||
if (action == "modbus_stop") {
|
||||
cJSON_Delete(root);
|
||||
writeModbusManagementResponse(uart_port, true, action.c_str(), nullptr);
|
||||
modbus_stop_requested = true;
|
||||
return;
|
||||
}
|
||||
if (action == "modbus_start" || action == "modbus_status") {
|
||||
cJSON_Delete(root);
|
||||
writeModbusManagementResponse(uart_port, true, action.c_str(), nullptr);
|
||||
return;
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
writeModbusManagementResponse(uart_port, false, action.c_str(), "unknown action");
|
||||
}
|
||||
|
||||
void writeModbusManagementResponse(int uart_port, const bool ok, const char* action,
|
||||
const char* error) const {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(root, "ok", ok);
|
||||
cJSON_AddNumberToObject(root, "gw", channel.gateway_id);
|
||||
cJSON_AddStringToObject(root, "action", action == nullptr ? "unknown" : action);
|
||||
if (const auto config = activeModbusConfig()) {
|
||||
cJSON_AddStringToObject(root, "transport", config->transport.c_str());
|
||||
cJSON_AddNumberToObject(root, "unitID", config->unit_id);
|
||||
if (GatewayModbusTransportIsSerial(config->transport)) {
|
||||
cJSON_AddNumberToObject(root, "uartPort", config->serial.uart_port);
|
||||
cJSON_AddNumberToObject(root, "baudrate", config->serial.baudrate);
|
||||
} else {
|
||||
cJSON_AddNumberToObject(root, "port", config->port);
|
||||
}
|
||||
}
|
||||
if (error != nullptr) {
|
||||
cJSON_AddStringToObject(root, "error", error);
|
||||
}
|
||||
const std::string body = "@DALIGW " + PrintJson(root) + "\n";
|
||||
cJSON_Delete(root);
|
||||
uart_write_bytes(static_cast<uart_port_t>(uart_port), body.data(), body.size());
|
||||
}
|
||||
|
||||
};
|
||||
@@ -2696,8 +3176,9 @@ esp_err_t GatewayBridgeService::start() {
|
||||
|
||||
if (config_.modbus_enabled && config_.modbus_startup_enabled) {
|
||||
std::set<uint16_t> used_ports;
|
||||
std::set<int> used_uarts;
|
||||
for (const auto& runtime : runtimes_) {
|
||||
const esp_err_t err = runtime->startModbus(&used_ports);
|
||||
const esp_err_t err = runtime->startModbus(&used_ports, &used_uarts);
|
||||
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND && err != ESP_ERR_NOT_SUPPORTED) {
|
||||
ESP_LOGW(kTag, "gateway=%u Modbus startup skipped: %s", runtime->channel.gateway_id,
|
||||
esp_err_to_name(err));
|
||||
@@ -2981,7 +3462,14 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
|
||||
if (action == "modbus_start") {
|
||||
const esp_err_t err = runtime->startModbus();
|
||||
if (err != ESP_OK) {
|
||||
return ErrorResponse(err, "failed to start Modbus TCP bridge");
|
||||
return ErrorResponse(err, "failed to start Modbus bridge");
|
||||
}
|
||||
return handleGet("modbus", gateway_id.value());
|
||||
}
|
||||
if (action == "modbus_stop") {
|
||||
const esp_err_t err = runtime->stopModbus();
|
||||
if (err != ESP_OK) {
|
||||
return ErrorResponse(err, "failed to stop Modbus bridge");
|
||||
}
|
||||
return handleGet("modbus", gateway_id.value());
|
||||
}
|
||||
|
||||
@@ -18,14 +18,43 @@ constexpr uint16_t kGatewayModbusMaxReadBits = 2000;
|
||||
constexpr uint16_t kGatewayModbusMaxReadRegisters = 125;
|
||||
constexpr uint16_t kGatewayModbusMaxWriteBits = 1968;
|
||||
constexpr uint16_t kGatewayModbusMaxWriteRegisters = 123;
|
||||
constexpr uint32_t kGatewayModbusDefaultSerialBaudrate = 9600;
|
||||
constexpr uint32_t kGatewayModbusDefaultSerialResponseTimeoutMs = 20;
|
||||
constexpr uint32_t kGatewayModbusDefaultSerialInterFrameGapUs = 4000;
|
||||
|
||||
struct GatewayModbusRs485Config {
|
||||
bool enabled{false};
|
||||
int de_pin{-1};
|
||||
};
|
||||
|
||||
struct GatewayModbusSerialConfig {
|
||||
int uart_port{1};
|
||||
int tx_pin{-1};
|
||||
int rx_pin{-1};
|
||||
uint32_t baudrate{kGatewayModbusDefaultSerialBaudrate};
|
||||
int data_bits{8};
|
||||
std::string parity{"none"};
|
||||
int stop_bits{1};
|
||||
size_t rx_buffer_size{512};
|
||||
size_t tx_buffer_size{512};
|
||||
uint32_t response_timeout_ms{kGatewayModbusDefaultSerialResponseTimeoutMs};
|
||||
uint32_t inter_frame_gap_us{kGatewayModbusDefaultSerialInterFrameGapUs};
|
||||
GatewayModbusRs485Config rs485;
|
||||
};
|
||||
|
||||
struct GatewayModbusConfig {
|
||||
std::string transport{"tcp-server"};
|
||||
std::string host;
|
||||
uint16_t port{kGatewayModbusDefaultTcpPort};
|
||||
uint8_t unit_id{1};
|
||||
GatewayModbusSerialConfig serial;
|
||||
};
|
||||
|
||||
bool GatewayModbusTransportIsTcp(const std::string& transport);
|
||||
bool GatewayModbusTransportIsRtu(const std::string& transport);
|
||||
bool GatewayModbusTransportIsAscii(const std::string& transport);
|
||||
bool GatewayModbusTransportIsSerial(const std::string& transport);
|
||||
|
||||
enum class GatewayModbusSpace : uint8_t {
|
||||
kCoil = 1,
|
||||
kDiscreteInput = 2,
|
||||
|
||||
@@ -396,8 +396,44 @@ GatewayModbusPointBinding toBinding(const GatewayModbusPoint& point) {
|
||||
point.diagnostic_device_type};
|
||||
}
|
||||
|
||||
int clampedInt(const DaliValue::Object& json, const std::string& key, int fallback,
|
||||
int min_value, int max_value) {
|
||||
const int value = getObjectInt(json, key).value_or(fallback);
|
||||
return std::clamp(value, min_value, max_value);
|
||||
}
|
||||
|
||||
uint32_t clampedU32(const DaliValue::Object& json, const std::string& key, uint32_t fallback,
|
||||
uint32_t min_value, uint32_t max_value) {
|
||||
const int value = getObjectInt(json, key).value_or(static_cast<int>(fallback));
|
||||
return static_cast<uint32_t>(std::clamp(value, static_cast<int>(min_value),
|
||||
static_cast<int>(max_value)));
|
||||
}
|
||||
|
||||
size_t clampedSize(const DaliValue::Object& json, const std::string& key, size_t fallback,
|
||||
size_t min_value, size_t max_value) {
|
||||
const int value = getObjectInt(json, key).value_or(static_cast<int>(fallback));
|
||||
return static_cast<size_t>(std::clamp(value, static_cast<int>(min_value),
|
||||
static_cast<int>(max_value)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool GatewayModbusTransportIsTcp(const std::string& transport) {
|
||||
return transport.empty() || transport == "tcp" || transport == "tcp-server";
|
||||
}
|
||||
|
||||
bool GatewayModbusTransportIsRtu(const std::string& transport) {
|
||||
return transport == "rtu" || transport == "rtu-server" || transport == "modbus-rtu";
|
||||
}
|
||||
|
||||
bool GatewayModbusTransportIsAscii(const std::string& transport) {
|
||||
return transport == "ascii" || transport == "ascii-server" || transport == "modbus-ascii";
|
||||
}
|
||||
|
||||
bool GatewayModbusTransportIsSerial(const std::string& transport) {
|
||||
return GatewayModbusTransportIsRtu(transport) || GatewayModbusTransportIsAscii(transport);
|
||||
}
|
||||
|
||||
std::optional<GatewayModbusConfig> GatewayModbusConfigFromValue(const DaliValue* value) {
|
||||
if (value == nullptr || value->asObject() == nullptr) {
|
||||
return std::nullopt;
|
||||
@@ -410,6 +446,35 @@ std::optional<GatewayModbusConfig> GatewayModbusConfigFromValue(const DaliValue*
|
||||
getObjectInt(json, "port").value_or(kGatewayModbusDefaultTcpPort));
|
||||
config.unit_id = static_cast<uint8_t>(getObjectInt(json, "unitID").value_or(
|
||||
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.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,
|
||||
1200, 921600);
|
||||
config.serial.data_bits = clampedInt(*serial, "dataBits", config.serial.data_bits, 7, 8);
|
||||
config.serial.parity = getObjectString(*serial, "parity").value_or(config.serial.parity);
|
||||
config.serial.stop_bits = clampedInt(*serial, "stopBits", config.serial.stop_bits, 1, 2);
|
||||
config.serial.rx_buffer_size = clampedSize(*serial, "rxBufferBytes",
|
||||
config.serial.rx_buffer_size, 128, 4096);
|
||||
config.serial.tx_buffer_size = clampedSize(*serial, "txBufferBytes",
|
||||
config.serial.tx_buffer_size, 0, 4096);
|
||||
config.serial.response_timeout_ms = clampedU32(*serial, "responseTimeoutMs",
|
||||
config.serial.response_timeout_ms, 1, 1000);
|
||||
config.serial.inter_frame_gap_us = clampedU32(*serial, "interFrameGapUs",
|
||||
config.serial.inter_frame_gap_us, 1000,
|
||||
100000);
|
||||
if (const auto* rs485_value = getObjectValue(*serial, "rs485")) {
|
||||
if (const auto* rs485 = rs485_value->asObject()) {
|
||||
config.serial.rs485.enabled = getObjectBool(*rs485, "enabled")
|
||||
.value_or(config.serial.rs485.enabled);
|
||||
config.serial.rs485.de_pin = clampedInt(*rs485, "dePin",
|
||||
config.serial.rs485.de_pin, -1, 48);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -419,6 +484,23 @@ DaliValue GatewayModbusConfigToValue(const GatewayModbusConfig& config) {
|
||||
out["host"] = config.host;
|
||||
out["port"] = static_cast<int>(config.port);
|
||||
out["unitID"] = static_cast<int>(config.unit_id);
|
||||
DaliValue::Object serial;
|
||||
serial["uartPort"] = config.serial.uart_port;
|
||||
serial["txPin"] = config.serial.tx_pin;
|
||||
serial["rxPin"] = config.serial.rx_pin;
|
||||
serial["baudrate"] = static_cast<int>(config.serial.baudrate);
|
||||
serial["dataBits"] = config.serial.data_bits;
|
||||
serial["parity"] = config.serial.parity;
|
||||
serial["stopBits"] = config.serial.stop_bits;
|
||||
serial["rxBufferBytes"] = static_cast<int>(config.serial.rx_buffer_size);
|
||||
serial["txBufferBytes"] = static_cast<int>(config.serial.tx_buffer_size);
|
||||
serial["responseTimeoutMs"] = static_cast<int>(config.serial.response_timeout_ms);
|
||||
serial["interFrameGapUs"] = static_cast<int>(config.serial.inter_frame_gap_us);
|
||||
DaliValue::Object rs485;
|
||||
rs485["enabled"] = config.serial.rs485.enabled;
|
||||
rs485["dePin"] = config.serial.rs485.de_pin;
|
||||
serial["rs485"] = std::move(rs485);
|
||||
out["serial"] = std::move(serial);
|
||||
return DaliValue(std::move(out));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user