Add Gateway Modbus component with configuration and bridge implementation

- Created CMakeLists.txt for the Gateway Modbus component.
- Added header file `gateway_modbus.hpp` defining configuration structures, enums, and point structures.
- Implemented the `gateway_modbus.cpp` source file containing the logic for managing Modbus points, including reading and writing operations.
- Introduced utility functions for converting configurations to and from DaliValue, and for handling Modbus space and access types.
- Established a bridge class to manage Modbus points and their interactions with the DaliBridgeEngine.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Tony
2026-05-04 01:19:05 +08:00
parent 8aa5a451a4
commit 694217eb2c
9 changed files with 1244 additions and 63 deletions
+2
View File
@@ -3,6 +3,8 @@ set(GATEWAY_BRIDGE_REQUIRES
dali_cpp
espressif__cjson
freertos
gateway_cache
gateway_modbus
log
lwip
nvs_flash
@@ -12,6 +12,7 @@
namespace gateway {
class DaliDomainService;
class GatewayCache;
struct GatewayBridgeServiceConfig {
bool bridge_enabled{true};
@@ -35,6 +36,7 @@ struct GatewayBridgeHttpResponse {
class GatewayBridgeService {
public:
GatewayBridgeService(DaliDomainService& dali_domain,
GatewayCache& cache,
GatewayBridgeServiceConfig config = {});
~GatewayBridgeService();
@@ -52,6 +54,7 @@ class GatewayBridgeService {
const ChannelRuntime* findRuntime(uint8_t gateway_id) const;
DaliDomainService& dali_domain_;
GatewayCache& cache_;
GatewayBridgeServiceConfig config_;
std::vector<std::unique_ptr<ChannelRuntime>> runtimes_;
};
+546 -60
View File
@@ -8,10 +8,12 @@
#include "bridge_model.hpp"
#include "bridge_provisioning.hpp"
#include "dali_comm.hpp"
#include "dali_define.hpp"
#include "dali_domain.hpp"
#include "gateway_cache.hpp"
#include "gateway_cloud.hpp"
#include "gateway_modbus.hpp"
#include "gateway_provisioning.hpp"
#include "modbus_bridge.hpp"
#include "cJSON.h"
#include "esp_log.h"
@@ -34,13 +36,18 @@ namespace gateway {
namespace {
constexpr const char* kTag = "gateway_bridge";
constexpr int kDefaultModbusPort = 1502;
constexpr size_t kModbusMaxPduBytes = 252;
constexpr const char* kBridgeConfigKey = "bridge_cfg";
constexpr const char* kDiscoveryInventoryKey = "bridge_disc";
constexpr int kMaxDaliShortAddress = 63;
constexpr uint16_t kModbusUnknownRegister = 0xFFFF;
constexpr uint32_t kBacnetReliabilityNoFaultDetected = 0;
constexpr uint32_t kBacnetReliabilityCommunicationFailure = 12;
struct GatewayBridgeStoredConfig {
BridgeRuntimeConfig bridge;
std::optional<GatewayModbusConfig> modbus;
};
struct BridgeDiscoveryEntry {
int short_address{0};
bool online{true};
@@ -181,6 +188,39 @@ bool ValidShortAddress(int address) {
return address >= 0 && address <= kMaxDaliShortAddress;
}
uint8_t RawArcAddressFromDec(int dec_address) {
if (dec_address >= 0 && dec_address < 64) {
return static_cast<uint8_t>(dec_address * 2);
}
if (dec_address >= 64 && dec_address < 80) {
return static_cast<uint8_t>(0x80 + (dec_address - 64) * 2);
}
return 0xfe;
}
uint8_t RawCommandAddressFromDec(int dec_address) {
if (dec_address >= 0 && dec_address < 64) {
return static_cast<uint8_t>(dec_address * 2 + 1);
}
if (dec_address >= 64 && dec_address < 80) {
return static_cast<uint8_t>(0x80 + (dec_address - 64) * 2 + 1);
}
return 0xff;
}
uint16_t DeviceTypeMask(const DaliDomainSnapshot& snapshot) {
uint16_t mask = 0;
const auto types = snapshot.int_arrays.find("types");
if (types != snapshot.int_arrays.end()) {
for (const int type : types->second) {
if (type >= 0 && type < 16) {
mask |= static_cast<uint16_t>(1U << type);
}
}
}
return mask;
}
bool IsRawBridgeOperation(BridgeOperation operation) {
switch (operation) {
case BridgeOperation::send:
@@ -711,14 +751,33 @@ cJSON* ToCjson(const DaliValue& value) {
return cJSON_CreateNull();
}
std::string BridgeRuntimeConfigToJson(const BridgeRuntimeConfig& config) {
cJSON* root = ToCjson(DaliValue(config.toJson()));
DaliValue::Object GatewayBridgeStoredConfigToValue(
const BridgeRuntimeConfig& bridge_config,
const std::optional<GatewayModbusConfig>& modbus_config) {
DaliValue::Object out = bridge_config.toJson();
if (modbus_config.has_value()) {
out["modbus"] = GatewayModbusConfigToValue(modbus_config.value());
}
return out;
}
std::string GatewayBridgeStoredConfigToJson(
const BridgeRuntimeConfig& bridge_config,
const std::optional<GatewayModbusConfig>& modbus_config) {
cJSON* root = ToCjson(DaliValue(GatewayBridgeStoredConfigToValue(bridge_config, modbus_config)));
const std::string body = PrintJson(root);
cJSON_Delete(root);
return body;
}
std::optional<BridgeRuntimeConfig> BridgeRuntimeConfigFromJson(std::string_view json) {
GatewayBridgeStoredConfig GatewayBridgeStoredConfigFromValue(const DaliValue::Object& object) {
GatewayBridgeStoredConfig config;
config.bridge = BridgeRuntimeConfig::fromJson(object);
config.modbus = GatewayModbusConfigFromValue(getObjectValue(object, "modbus"));
return config;
}
std::optional<GatewayBridgeStoredConfig> GatewayBridgeStoredConfigFromJson(std::string_view json) {
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
if (root == nullptr) {
return std::nullopt;
@@ -729,7 +788,7 @@ std::optional<BridgeRuntimeConfig> BridgeRuntimeConfigFromJson(std::string_view
if (object == nullptr) {
return std::nullopt;
}
return BridgeRuntimeConfig::fromJson(*object);
return GatewayBridgeStoredConfigFromValue(*object);
}
GatewayCloudConfig GatewayCloudConfigFromJson(cJSON* root) {
@@ -852,16 +911,12 @@ bool SendModbusException(int sock, const uint8_t* mbap, uint8_t function_code,
return SendModbusFrame(sock, mbap, pdu);
}
int HoldingRegisterFromWireAddress(uint16_t zero_based_address) {
return 40001 + static_cast<int>(zero_based_address);
}
} // namespace
struct GatewayBridgeService::ChannelRuntime {
explicit ChannelRuntime(DaliDomainService& domain, DaliChannelInfo channel,
explicit ChannelRuntime(DaliDomainService& domain, GatewayCache& cache, DaliChannelInfo channel,
GatewayBridgeServiceConfig service_config)
: domain(domain), channel(std::move(channel)), service_config(service_config),
: domain(domain), cache(cache), channel(std::move(channel)), service_config(service_config),
lock(xSemaphoreCreateRecursiveMutex()) {}
~ChannelRuntime() {
@@ -875,17 +930,19 @@ struct GatewayBridgeService::ChannelRuntime {
}
DaliDomainService& domain;
GatewayCache& cache;
DaliChannelInfo channel;
GatewayBridgeServiceConfig service_config;
SemaphoreHandle_t lock{nullptr};
std::unique_ptr<DaliComm> comm;
std::unique_ptr<DaliBridgeEngine> engine;
std::unique_ptr<DaliModbusBridge> modbus;
std::unique_ptr<GatewayModbusBridge> modbus;
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
std::unique_ptr<DaliBacnetBridge> bacnet;
#endif
std::unique_ptr<DaliCloudBridge> cloud;
BridgeRuntimeConfig bridge_config;
std::optional<GatewayModbusConfig> modbus_config;
BridgeDiscoveryInventory discovery_inventory;
std::optional<GatewayCloudConfig> cloud_config;
bool bridge_config_loaded{false};
@@ -920,7 +977,13 @@ struct GatewayBridgeService::ChannelRuntime {
[](uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); });
BridgeProvisioningStore bridge_store(bridgeNamespace());
bridge_config_loaded = bridge_store.load(&bridge_config) == ESP_OK;
DaliValue::Object bridge_object;
if (bridge_store.loadObject(kBridgeConfigKey, &bridge_object) == ESP_OK) {
const auto stored_config = GatewayBridgeStoredConfigFromValue(bridge_object);
bridge_config = stored_config.bridge;
modbus_config = stored_config.modbus;
bridge_config_loaded = true;
}
DaliValue::Object discovery_object;
if (bridge_store.loadObject(kDiscoveryInventoryKey, &discovery_object) == ESP_OK) {
discovery_inventory = DiscoveryInventoryFromValue(discovery_object);
@@ -948,9 +1011,9 @@ struct GatewayBridgeService::ChannelRuntime {
engine->upsertModel(model);
}
modbus = std::make_unique<DaliModbusBridge>(*engine);
if (bridge_config.modbus.has_value()) {
modbus->setConfig(bridge_config.modbus.value());
modbus = std::make_unique<GatewayModbusBridge>(*engine);
if (modbus_config.has_value()) {
modbus->setConfig(modbus_config.value());
}
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
@@ -1072,17 +1135,20 @@ struct GatewayBridgeService::ChannelRuntime {
}
esp_err_t saveBridgeConfig(std::string_view json) {
auto parsed = BridgeRuntimeConfigFromJson(json);
auto parsed = GatewayBridgeStoredConfigFromJson(json);
if (!parsed.has_value()) {
return ESP_ERR_INVALID_ARG;
}
BridgeProvisioningStore store(bridgeNamespace());
const esp_err_t err = store.save(parsed.value());
const esp_err_t err = store.saveObject(
kBridgeConfigKey,
GatewayBridgeStoredConfigToValue(parsed->bridge, parsed->modbus));
if (err != ESP_OK) {
return err;
}
LockGuard guard(lock);
bridge_config = parsed.value();
bridge_config = parsed->bridge;
modbus_config = parsed->modbus;
bridge_config_loaded = true;
applyBridgeConfigLocked();
return ESP_OK;
@@ -1096,6 +1162,7 @@ struct GatewayBridgeService::ChannelRuntime {
}
LockGuard guard(lock);
bridge_config = BridgeRuntimeConfig{};
modbus_config.reset();
bridge_config_loaded = false;
applyBridgeConfigLocked();
return ESP_OK;
@@ -1329,10 +1396,10 @@ struct GatewayBridgeService::ChannelRuntime {
if (modbus_json != nullptr) {
cJSON_AddBoolToObject(modbus_json, "enabled", service_config.modbus_enabled);
cJSON_AddBoolToObject(modbus_json, "started", modbus_started);
if (bridge_config.modbus.has_value()) {
cJSON_AddStringToObject(modbus_json, "transport", bridge_config.modbus->transport.c_str());
cJSON_AddNumberToObject(modbus_json, "port", bridge_config.modbus->port);
cJSON_AddNumberToObject(modbus_json, "unitID", bridge_config.modbus->unitID);
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);
}
cJSON_AddItemToObject(root, "modbus", modbus_json);
}
@@ -1378,7 +1445,8 @@ struct GatewayBridgeService::ChannelRuntime {
}
GatewayBridgeHttpResponse configJson() const {
return GatewayBridgeHttpResponse{ESP_OK, BridgeRuntimeConfigToJson(bridge_config)};
return GatewayBridgeHttpResponse{ESP_OK,
GatewayBridgeStoredConfigToJson(bridge_config, modbus_config)};
}
GatewayBridgeHttpResponse inventoryJson() const {
@@ -1534,13 +1602,27 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
cJSON* bindings = cJSON_CreateArray();
if (bindings != nullptr && modbus != nullptr) {
for (const auto& binding : modbus->describeHoldingRegisters()) {
for (const auto& binding : modbus->describePoints()) {
cJSON* item = cJSON_CreateObject();
if (item == nullptr) {
continue;
}
cJSON_AddStringToObject(item, "model", binding.modelID.c_str());
cJSON_AddNumberToObject(item, "registerAddress", binding.registerAddress);
if (!binding.model_id.empty()) {
cJSON_AddStringToObject(item, "model", binding.model_id.c_str());
}
cJSON_AddStringToObject(item, "space", GatewayModbusSpaceToString(binding.space));
cJSON_AddNumberToObject(item, "address", binding.address);
cJSON_AddStringToObject(item, "id", binding.id.c_str());
cJSON_AddStringToObject(item, "name", binding.name.c_str());
cJSON_AddStringToObject(item, "access", GatewayModbusAccessToString(binding.access));
cJSON_AddBoolToObject(item, "generated", binding.generated);
if (binding.generated) {
cJSON_AddStringToObject(item, "generatedKind",
GatewayModbusGeneratedKindToString(binding.generated_kind));
}
if (binding.short_address >= 0) {
cJSON_AddNumberToObject(item, "shortAddress", binding.short_address);
}
cJSON_AddItemToArray(bindings, item);
}
}
@@ -1633,6 +1715,305 @@ struct GatewayBridgeService::ChannelRuntime {
return JsonOk(root);
}
std::optional<bool> readGeneratedBoolPointLocked(const GatewayModbusPoint& point) {
if (!point.generated || !ValidShortAddress(point.short_address)) {
return std::nullopt;
}
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
const auto state = cache.daliAddressState(channel.gateway_id,
static_cast<uint8_t>(point.short_address));
switch (point.generated_kind) {
case GatewayModbusGeneratedKind::kShortDiscovered:
return discovery != nullptr;
case GatewayModbusGeneratedKind::kShortOnline:
return discovery != nullptr && discovery->online;
case GatewayModbusGeneratedKind::kShortSupportsDt1:
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 1);
case GatewayModbusGeneratedKind::kShortSupportsDt4:
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 4);
case GatewayModbusGeneratedKind::kShortSupportsDt5:
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 5);
case GatewayModbusGeneratedKind::kShortSupportsDt6:
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 6);
case GatewayModbusGeneratedKind::kShortSupportsDt8:
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 8);
case GatewayModbusGeneratedKind::kShortGroupMaskKnown:
return state.group_mask_known;
case GatewayModbusGeneratedKind::kShortActualLevelKnown:
return state.status.actual_level.has_value();
case GatewayModbusGeneratedKind::kShortSceneKnown:
return state.status.scene_id.has_value();
case GatewayModbusGeneratedKind::kShortSettingsKnown:
return state.settings.anyKnown();
case GatewayModbusGeneratedKind::kShortControlGearPresent:
case GatewayModbusGeneratedKind::kShortLampFailure:
case GatewayModbusGeneratedKind::kShortLampPowerOn:
case GatewayModbusGeneratedKind::kShortLimitError:
case GatewayModbusGeneratedKind::kShortFadingCompleted:
case GatewayModbusGeneratedKind::kShortResetState:
case GatewayModbusGeneratedKind::kShortMissingShortAddress:
case GatewayModbusGeneratedKind::kShortPowerSupplyFault:
return false;
default:
return std::nullopt;
}
}
std::optional<uint16_t> readGeneratedRegisterPointLocked(const GatewayModbusPoint& point) {
if (!point.generated || !ValidShortAddress(point.short_address)) {
return std::nullopt;
}
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
const auto state = cache.daliAddressState(channel.gateway_id,
static_cast<uint8_t>(point.short_address));
switch (point.generated_kind) {
case GatewayModbusGeneratedKind::kShortInventoryState:
if (discovery == nullptr) {
return 0;
}
return discovery->online ? 2 : 1;
case GatewayModbusGeneratedKind::kShortPrimaryType: {
if (discovery == nullptr) {
return kModbusUnknownRegister;
}
const auto primary = discovery->discovery.ints.find("primaryType");
return primary == discovery->discovery.ints.end()
? kModbusUnknownRegister
: static_cast<uint16_t>(primary->second);
}
case GatewayModbusGeneratedKind::kShortTypeMask:
return discovery == nullptr ? kModbusUnknownRegister : DeviceTypeMask(discovery->discovery);
case GatewayModbusGeneratedKind::kShortBrightness:
case GatewayModbusGeneratedKind::kShortActualLevel:
return state.status.actual_level.has_value()
? static_cast<uint16_t>(state.status.actual_level.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortSceneId:
return state.status.scene_id.has_value()
? static_cast<uint16_t>(state.status.scene_id.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortRawStatus:
return kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortGroupMask:
return state.group_mask_known ? state.group_mask : kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortPowerOnLevel:
return state.settings.power_on_level.has_value()
? static_cast<uint16_t>(state.settings.power_on_level.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortSystemFailureLevel:
return state.settings.system_failure_level.has_value()
? static_cast<uint16_t>(state.settings.system_failure_level.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortMinLevel:
return state.settings.min_level.has_value()
? static_cast<uint16_t>(state.settings.min_level.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortMaxLevel:
return state.settings.max_level.has_value()
? static_cast<uint16_t>(state.settings.max_level.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortFadeTime:
return state.settings.fade_time.has_value()
? static_cast<uint16_t>(state.settings.fade_time.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortFadeRate:
return state.settings.fade_rate.has_value()
? static_cast<uint16_t>(state.settings.fade_rate.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortColorTemperature:
return kModbusUnknownRegister;
default:
return std::nullopt;
}
}
bool writeGeneratedCoilPointLocked(const GatewayModbusPoint& point, bool value) {
if (!point.generated || !ValidShortAddress(point.short_address)) {
return false;
}
if (!value) {
return true;
}
const uint8_t raw_command_address = RawCommandAddressFromDec(point.short_address);
bool sent = false;
uint8_t mirrored_command = 0;
switch (point.generated_kind) {
case GatewayModbusGeneratedKind::kShortOn:
case GatewayModbusGeneratedKind::kShortRecallMax:
sent = domain.on(channel.gateway_id, point.short_address);
mirrored_command = DALI_CMD_RECALL_MAX;
break;
case GatewayModbusGeneratedKind::kShortOff:
sent = domain.off(channel.gateway_id, point.short_address);
mirrored_command = DALI_CMD_OFF;
break;
case GatewayModbusGeneratedKind::kShortRecallMin:
sent = domain.sendRaw(channel.gateway_id, raw_command_address, DALI_CMD_RECALL_MIN);
mirrored_command = DALI_CMD_RECALL_MIN;
break;
default:
return false;
}
if (sent) {
cache.mirrorDaliCommand(channel.gateway_id, raw_command_address, mirrored_command);
}
return sent;
}
bool writeGeneratedRegisterPointLocked(const GatewayModbusPoint& point, uint16_t value) {
if (!point.generated || !ValidShortAddress(point.short_address)) {
return false;
}
switch (point.generated_kind) {
case GatewayModbusGeneratedKind::kShortBrightness:
if (value > 254) {
return false;
}
if (domain.setBright(channel.gateway_id, point.short_address, value)) {
cache.mirrorDaliCommand(channel.gateway_id, RawArcAddressFromDec(point.short_address),
static_cast<uint8_t>(value));
return true;
}
return false;
case GatewayModbusGeneratedKind::kShortColorTemperature:
return domain.setColTemp(channel.gateway_id, point.short_address, value);
case GatewayModbusGeneratedKind::kShortGroupMask:
if (domain.applyGroupMask(channel.gateway_id, point.short_address, value)) {
cache.setDaliGroupMask(channel.gateway_id, static_cast<uint8_t>(point.short_address),
value);
return true;
}
return false;
case GatewayModbusGeneratedKind::kShortPowerOnLevel:
case GatewayModbusGeneratedKind::kShortSystemFailureLevel:
case GatewayModbusGeneratedKind::kShortMinLevel:
case GatewayModbusGeneratedKind::kShortMaxLevel:
case GatewayModbusGeneratedKind::kShortFadeTime:
case GatewayModbusGeneratedKind::kShortFadeRate: {
if (value > 255) {
return false;
}
auto current = cache.daliAddressState(channel.gateway_id,
static_cast<uint8_t>(point.short_address)).settings;
switch (point.generated_kind) {
case GatewayModbusGeneratedKind::kShortPowerOnLevel:
current.power_on_level = static_cast<uint8_t>(value);
break;
case GatewayModbusGeneratedKind::kShortSystemFailureLevel:
current.system_failure_level = static_cast<uint8_t>(value);
break;
case GatewayModbusGeneratedKind::kShortMinLevel:
current.min_level = static_cast<uint8_t>(value);
break;
case GatewayModbusGeneratedKind::kShortMaxLevel:
current.max_level = static_cast<uint8_t>(value);
break;
case GatewayModbusGeneratedKind::kShortFadeTime:
current.fade_time = static_cast<uint8_t>(value);
break;
case GatewayModbusGeneratedKind::kShortFadeRate:
current.fade_rate = static_cast<uint8_t>(value);
break;
default:
break;
}
DaliAddressSettingsSnapshot domain_settings;
domain_settings.power_on_level = current.power_on_level;
domain_settings.system_failure_level = current.system_failure_level;
domain_settings.min_level = current.min_level;
domain_settings.max_level = current.max_level;
domain_settings.fade_time = current.fade_time;
domain_settings.fade_rate = current.fade_rate;
if (domain.applyAddressSettings(channel.gateway_id, point.short_address, domain_settings)) {
cache.setDaliSettings(channel.gateway_id, static_cast<uint8_t>(point.short_address),
current);
return true;
}
return false;
}
default:
return false;
}
}
std::optional<bool> readModbusBoolPoint(GatewayModbusSpace space, uint16_t address) {
LockGuard guard(lock);
if (modbus == nullptr) {
return std::nullopt;
}
const auto point = modbus->findPoint(space, address);
if (!point.has_value()) {
return std::nullopt;
}
if (point->generated) {
return readGeneratedBoolPointLocked(point.value()).value_or(false);
}
const DaliBridgeResult result = modbus->readModelPoint(point.value());
if (!result.ok || !result.data.has_value()) {
return std::nullopt;
}
if (point->bit_index.has_value() && point->bit_index.value() >= 0 &&
point->bit_index.value() < 16) {
return (result.data.value() & (1 << point->bit_index.value())) != 0;
}
return result.data.value() != 0;
}
std::optional<uint16_t> readModbusRegisterPoint(GatewayModbusSpace space, uint16_t address) {
LockGuard guard(lock);
if (modbus == nullptr) {
return std::nullopt;
}
const auto point = modbus->findPoint(space, address);
if (!point.has_value()) {
return std::nullopt;
}
if (point->generated) {
return readGeneratedRegisterPointLocked(point.value()).value_or(kModbusUnknownRegister);
}
const DaliBridgeResult result = modbus->readModelPoint(point.value());
if (!result.ok || !result.data.has_value()) {
return std::nullopt;
}
return static_cast<uint16_t>(result.data.value() & 0xFFFF);
}
bool writeModbusCoilPoint(uint16_t address, bool value) {
LockGuard guard(lock);
if (modbus == nullptr) {
return false;
}
const auto point = modbus->findPoint(GatewayModbusSpace::kCoil, address);
if (!point.has_value()) {
return false;
}
if (point->generated) {
return writeGeneratedCoilPointLocked(point.value(), value);
}
const DaliBridgeResult result = modbus->writeCoilPoint(point.value(), value);
return result.ok;
}
bool writeModbusRegisterPoint(uint16_t address, uint16_t value) {
LockGuard guard(lock);
if (modbus == nullptr) {
return false;
}
const auto point = modbus->findPoint(GatewayModbusSpace::kHoldingRegister, address);
if (!point.has_value()) {
return false;
}
if (point->generated) {
return writeGeneratedRegisterPointLocked(point.value(), value);
}
const DaliBridgeResult result = modbus->writeRegisterPoint(point.value(), value);
return result.ok;
}
esp_err_t startModbus(std::set<uint16_t>* used_ports = nullptr) {
LockGuard guard(lock);
if (!service_config.modbus_enabled) {
@@ -1641,11 +2022,11 @@ struct GatewayBridgeService::ChannelRuntime {
if (modbus_started || modbus_task_handle != nullptr) {
return ESP_OK;
}
if (!bridge_config.modbus.has_value()) {
if (!modbus_config.has_value()) {
return ESP_ERR_NOT_FOUND;
}
const uint16_t port = bridge_config.modbus->port == 0 ? kDefaultModbusPort
: bridge_config.modbus->port;
const uint16_t port = modbus_config->port == 0 ? kGatewayModbusDefaultTcpPort
: modbus_config->port;
if (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);
@@ -1666,9 +2047,9 @@ struct GatewayBridgeService::ChannelRuntime {
}
void modbusTaskLoop() {
const uint16_t port = bridge_config.modbus.has_value() && bridge_config.modbus->port != 0
? bridge_config.modbus->port
: kDefaultModbusPort;
const uint16_t port = modbus_config.has_value() && modbus_config->port != 0
? modbus_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);
@@ -1715,7 +2096,7 @@ struct GatewayBridgeService::ChannelRuntime {
while (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 > kModbusMaxPduBytes) {
if (protocol_id != 0 || length < 2 || length > kGatewayModbusMaxPduBytes) {
break;
}
@@ -1724,18 +2105,85 @@ struct GatewayBridgeService::ChannelRuntime {
break;
}
if (bridge_config.modbus.has_value() && bridge_config.modbus->unitID != 0 &&
header[6] != bridge_config.modbus->unitID) {
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] == 0x06 && pdu.size() == 5) {
const uint16_t wire_register = ReadBe16(&pdu[1]);
const uint16_t value = ReadBe16(&pdu[3]);
const int holding_register = HoldingRegisterFromWireAddress(wire_register);
const auto result = handleHoldingRegisterWrite(holding_register, value);
if (!result.ok) {
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;
}
@@ -1743,11 +2191,58 @@ struct GatewayBridgeService::ChannelRuntime {
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 (pdu.size() != static_cast<size_t>(6 + byte_count) || byte_count != quantity * 2) {
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;
}
@@ -1755,9 +2250,9 @@ struct GatewayBridgeService::ChannelRuntime {
for (uint16_t index = 0; index < quantity; ++index) {
const size_t offset = 6 + (index * 2);
const uint16_t value = ReadBe16(&pdu[offset]);
const int holding_register = HoldingRegisterFromWireAddress(start_register + index);
const auto result = handleHoldingRegisterWrite(holding_register, value);
if (!result.ok) {
const auto holding_register = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
GatewayModbusSpace::kHoldingRegister, start_register + index));
if (!writeModbusRegisterPoint(holding_register, value)) {
ok = false;
break;
}
@@ -1778,21 +2273,12 @@ struct GatewayBridgeService::ChannelRuntime {
}
}
DaliBridgeResult handleHoldingRegisterWrite(int holding_register, int value) {
LockGuard guard(lock);
if (modbus == nullptr) {
DaliBridgeResult result;
result.sequence = "modbus-" + std::to_string(holding_register);
result.error = "modbus bridge not ready";
return result;
}
return modbus->handleHoldingRegisterWrite(holding_register, value);
}
};
GatewayBridgeService::GatewayBridgeService(DaliDomainService& dali_domain,
GatewayCache& cache,
GatewayBridgeServiceConfig config)
: dali_domain_(dali_domain), config_(config) {}
: dali_domain_(dali_domain), cache_(cache), config_(config) {}
GatewayBridgeService::~GatewayBridgeService() = default;
@@ -1808,7 +2294,7 @@ esp_err_t GatewayBridgeService::start() {
const auto channels = dali_domain_.channelInfo();
runtimes_.reserve(channels.size());
for (const auto& channel : channels) {
auto runtime = std::make_unique<ChannelRuntime>(dali_domain_, channel, config_);
auto runtime = std::make_unique<ChannelRuntime>(dali_domain_, cache_, channel, config_);
const esp_err_t err = runtime->start();
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to start bridge runtime gateway=%u: %s", channel.gateway_id,