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:
Tony
2026-05-04 14:34:05 +08:00
parent 640e78f688
commit 34d2d9caa0
10 changed files with 1364 additions and 204 deletions
+1
View File
@@ -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};
};
+679 -191
View File
@@ -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,175 +2884,265 @@ 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;
}
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;
}
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;
}
if (value.value()) {
response[2 + (index / 8)] |= static_cast<uint8_t>(1U << (index % 8));
}
}
if (!ok) {
SendModbusException(client_sock, header, pdu[0], 0x02);
continue;
}
SendModbusFrame(client_sock, header, response);
continue;
}
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;
}
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;
}
WriteBe16(&response[2 + index * 2], value.value());
}
if (!ok) {
SendModbusException(client_sock, header, pdu[0], 0x02);
continue;
}
SendModbusFrame(client_sock, header, response);
continue;
}
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);
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);
continue;
}
SendModbusFrame(client_sock, header, pdu);
continue;
}
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;
}
SendModbusFrame(client_sock, header, pdu);
continue;
}
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);
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);
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;
}
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;
}
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;
}
}
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;
}
SendModbusException(client_sock, header, pdu[0], 0x01);
SendModbusFrame(client_sock, header, processModbusPdu(config, header[6], pdu));
}
}
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 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;
}
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;
}
}
modbus_uart_port = config.serial.uart_port;
return ESP_OK;
}
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;
}
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);
}
uart_driver_delete(static_cast<uart_port_t>(config.serial.uart_port));
}
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;
}
if (frame.empty() || frame.front() == '@') {
continue;
}
handleModbusRtuFrame(config, frame);
frame.clear();
}
}
void handleModbusRtuFrame(const GatewayModbusConfig& config, const std::vector<uint8_t>& frame) {
if (frame.size() < 4) {
return;
}
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());
}
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;
}
for (int i = 0; i < read_len; ++i) {
const char ch = static_cast<char>(read_buffer[i]);
if (line.empty()) {
if (ch != ':' && ch != '@') {
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();
}
}
}
}
void handleModbusAsciiFrame(const GatewayModbusConfig& config, std::string_view line) {
const auto decoded = DecodeModbusAsciiLine(line);
if (!decoded.has_value() || decoded->size() < 4) {
return;
}
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;
}
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());
}
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());
}
};
GatewayBridgeService::GatewayBridgeService(DaliDomainService& dali_domain,
@@ -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));
}