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,
+7
View File
@@ -0,0 +1,7 @@
idf_component_register(
SRCS "src/gateway_modbus.cpp"
INCLUDE_DIRS "include"
REQUIRES dali_cpp
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -0,0 +1,147 @@
#pragma once
#include "bridge.hpp"
#include "bridge_model.hpp"
#include "model_value.hpp"
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace gateway {
constexpr uint16_t kGatewayModbusDefaultTcpPort = 1502;
constexpr size_t kGatewayModbusMaxPduBytes = 252;
constexpr uint16_t kGatewayModbusMaxReadBits = 2000;
constexpr uint16_t kGatewayModbusMaxReadRegisters = 125;
constexpr uint16_t kGatewayModbusMaxWriteBits = 1968;
constexpr uint16_t kGatewayModbusMaxWriteRegisters = 123;
struct GatewayModbusConfig {
std::string transport{"tcp-server"};
std::string host;
uint16_t port{kGatewayModbusDefaultTcpPort};
uint8_t unit_id{1};
};
enum class GatewayModbusSpace : uint8_t {
kCoil = 1,
kDiscreteInput = 2,
kHoldingRegister = 3,
kInputRegister = 4,
};
enum class GatewayModbusAccess : uint8_t {
kReadOnly = 0,
kWriteOnly = 1,
kReadWrite = 2,
};
enum class GatewayModbusGeneratedKind : uint8_t {
kNone = 0,
kShortOn,
kShortOff,
kShortRecallMax,
kShortRecallMin,
kShortDiscovered,
kShortOnline,
kShortSupportsDt1,
kShortSupportsDt4,
kShortSupportsDt5,
kShortSupportsDt6,
kShortSupportsDt8,
kShortGroupMaskKnown,
kShortActualLevelKnown,
kShortSceneKnown,
kShortSettingsKnown,
kShortControlGearPresent,
kShortLampFailure,
kShortLampPowerOn,
kShortLimitError,
kShortFadingCompleted,
kShortResetState,
kShortMissingShortAddress,
kShortPowerSupplyFault,
kShortBrightness,
kShortColorTemperature,
kShortGroupMask,
kShortPowerOnLevel,
kShortSystemFailureLevel,
kShortMinLevel,
kShortMaxLevel,
kShortFadeTime,
kShortFadeRate,
kShortInventoryState,
kShortPrimaryType,
kShortTypeMask,
kShortActualLevel,
kShortSceneId,
kShortRawStatus,
};
struct GatewayModbusPoint {
GatewayModbusSpace space{GatewayModbusSpace::kHoldingRegister};
GatewayModbusAccess access{GatewayModbusAccess::kReadWrite};
uint16_t address{0};
std::string id;
std::string name;
bool generated{false};
GatewayModbusGeneratedKind generated_kind{GatewayModbusGeneratedKind::kNone};
int short_address{-1};
std::string model_id;
BridgeOperation operation{BridgeOperation::unknown};
std::optional<int> bit_index;
};
struct GatewayModbusPointBinding {
std::string model_id;
GatewayModbusSpace space{GatewayModbusSpace::kHoldingRegister};
uint16_t address{0};
std::string id;
std::string name;
bool generated{false};
GatewayModbusGeneratedKind generated_kind{GatewayModbusGeneratedKind::kNone};
int short_address{-1};
GatewayModbusAccess access{GatewayModbusAccess::kReadWrite};
};
std::optional<GatewayModbusConfig> GatewayModbusConfigFromValue(const DaliValue* value);
DaliValue GatewayModbusConfigToValue(const GatewayModbusConfig& config);
const char* GatewayModbusSpaceToString(GatewayModbusSpace space);
const char* GatewayModbusAccessToString(GatewayModbusAccess access);
const char* GatewayModbusGeneratedKindToString(GatewayModbusGeneratedKind kind);
int GatewayModbusHumanAddressFromWire(GatewayModbusSpace space, uint16_t zero_based_address);
std::optional<GatewayModbusSpace> GatewayModbusReadSpaceForFunction(uint8_t function_code);
std::optional<GatewayModbusSpace> GatewayModbusWriteSpaceForFunction(uint8_t function_code);
class GatewayModbusBridge {
public:
explicit GatewayModbusBridge(DaliBridgeEngine& engine);
void setConfig(const GatewayModbusConfig& config);
const GatewayModbusConfig& config() const;
void rebuildMap();
std::optional<GatewayModbusPoint> findPoint(GatewayModbusSpace space,
uint16_t address) const;
std::vector<GatewayModbusPointBinding> describePoints() const;
std::vector<GatewayModbusPointBinding> describeHoldingRegisters() const;
DaliBridgeResult readModelPoint(const GatewayModbusPoint& point) const;
DaliBridgeResult writeRegisterPoint(const GatewayModbusPoint& point, uint16_t value) const;
DaliBridgeResult writeCoilPoint(const GatewayModbusPoint& point, bool value) const;
private:
DaliBridgeResult executeModelPoint(const GatewayModbusPoint& point,
std::optional<int> value) const;
DaliBridgeEngine& engine_;
GatewayModbusConfig config_;
std::vector<GatewayModbusPoint> points_;
};
} // namespace gateway
@@ -0,0 +1,516 @@
#include "gateway_modbus.hpp"
#include <algorithm>
#include <array>
#include <cstdio>
#include <map>
#include <utility>
namespace gateway {
namespace {
constexpr uint16_t kCoilBase = 1;
constexpr uint16_t kDiscreteInputBase = 10001;
constexpr uint16_t kInputRegisterBase = 30001;
constexpr uint16_t kHoldingRegisterBase = 40001;
constexpr uint16_t kShortAddressCount = 64;
constexpr uint16_t kShortStride = 32;
struct PointKey {
GatewayModbusSpace space{GatewayModbusSpace::kHoldingRegister};
uint16_t address{0};
bool operator<(const PointKey& other) const {
if (space != other.space) {
return static_cast<uint8_t>(space) < static_cast<uint8_t>(other.space);
}
return address < other.address;
}
};
struct GeneratedPointSpec {
uint16_t offset;
GatewayModbusSpace space;
GatewayModbusAccess access;
GatewayModbusGeneratedKind kind;
const char* suffix;
const char* name;
};
constexpr std::array<GeneratedPointSpec, 4> kGeneratedCoils{{
{0, GatewayModbusSpace::kCoil, GatewayModbusAccess::kWriteOnly,
GatewayModbusGeneratedKind::kShortOn, "on", "recall max"},
{1, GatewayModbusSpace::kCoil, GatewayModbusAccess::kWriteOnly,
GatewayModbusGeneratedKind::kShortOff, "off", "off"},
{2, GatewayModbusSpace::kCoil, GatewayModbusAccess::kWriteOnly,
GatewayModbusGeneratedKind::kShortRecallMax, "recall_max", "recall max"},
{3, GatewayModbusSpace::kCoil, GatewayModbusAccess::kWriteOnly,
GatewayModbusGeneratedKind::kShortRecallMin, "recall_min", "recall min"},
}};
constexpr std::array<GeneratedPointSpec, 18> kGeneratedDiscreteInputs{{
{0, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortDiscovered, "discovered", "discovered"},
{1, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortOnline, "online", "online"},
{2, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSupportsDt1, "supports_dt1", "supports DT1"},
{3, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSupportsDt4, "supports_dt4", "supports DT4"},
{4, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSupportsDt5, "supports_dt5", "supports DT5"},
{5, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSupportsDt6, "supports_dt6", "supports DT6"},
{6, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSupportsDt8, "supports_dt8", "supports DT8"},
{7, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortGroupMaskKnown, "group_mask_known", "group mask known"},
{8, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortActualLevelKnown, "actual_level_known", "actual level known"},
{9, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSceneKnown, "scene_known", "scene known"},
{10, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSettingsKnown, "settings_known", "settings known"},
{16, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortControlGearPresent, "control_gear_present",
"control gear present"},
{17, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortLampFailure, "lamp_failure", "lamp failure"},
{18, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortLampPowerOn, "lamp_power_on", "lamp power on"},
{19, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortLimitError, "limit_error", "limit error"},
{20, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortFadingCompleted, "fading_completed", "fading completed"},
{21, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortResetState, "reset_state", "reset state"},
{22, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortMissingShortAddress, "missing_short_address",
"missing short address"},
}};
constexpr std::array<GeneratedPointSpec, 8> kGeneratedHoldingRegisters{{
{0, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortBrightness, "brightness", "brightness"},
{1, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortColorTemperature, "color_temperature",
"color temperature"},
{2, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortGroupMask, "group_mask", "group mask"},
{3, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortPowerOnLevel, "power_on_level", "power-on level"},
{4, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortSystemFailureLevel, "system_failure_level",
"system-failure level"},
{5, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortMinLevel, "min_level", "minimum level"},
{6, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortMaxLevel, "max_level", "maximum level"},
{7, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortFadeTime, "fade_time", "fade time"},
}};
constexpr std::array<GeneratedPointSpec, 13> kGeneratedInputRegisters{{
{0, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortInventoryState, "inventory_state", "inventory state"},
{1, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortPrimaryType, "primary_type", "primary type"},
{2, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortTypeMask, "type_mask", "device type mask"},
{3, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortActualLevel, "actual_level", "actual level"},
{4, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSceneId, "scene_id", "scene id"},
{5, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortRawStatus, "raw_status", "raw status"},
{6, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortGroupMask, "group_mask", "group mask"},
{7, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortPowerOnLevel, "power_on_level", "power-on level"},
{8, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSystemFailureLevel, "system_failure_level",
"system-failure level"},
{9, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortMinLevel, "min_level", "minimum level"},
{10, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortMaxLevel, "max_level", "maximum level"},
{11, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortFadeTime, "fade_time", "fade time"},
{12, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortFadeRate, "fade_rate", "fade rate"},
}};
uint16_t baseForSpace(GatewayModbusSpace space) {
switch (space) {
case GatewayModbusSpace::kCoil:
return kCoilBase;
case GatewayModbusSpace::kDiscreteInput:
return kDiscreteInputBase;
case GatewayModbusSpace::kInputRegister:
return kInputRegisterBase;
case GatewayModbusSpace::kHoldingRegister:
return kHoldingRegisterBase;
}
return kHoldingRegisterBase;
}
std::optional<GatewayModbusSpace> spaceForObjectType(BridgeObjectType type) {
switch (type) {
case BridgeObjectType::coil:
return GatewayModbusSpace::kCoil;
case BridgeObjectType::discreteInput:
return GatewayModbusSpace::kDiscreteInput;
case BridgeObjectType::inputRegister:
return GatewayModbusSpace::kInputRegister;
case BridgeObjectType::holdingRegister:
return GatewayModbusSpace::kHoldingRegister;
default:
return std::nullopt;
}
}
GatewayModbusAccess accessForSpace(GatewayModbusSpace space) {
switch (space) {
case GatewayModbusSpace::kCoil:
case GatewayModbusSpace::kHoldingRegister:
return GatewayModbusAccess::kReadWrite;
case GatewayModbusSpace::kDiscreteInput:
case GatewayModbusSpace::kInputRegister:
return GatewayModbusAccess::kReadOnly;
}
return GatewayModbusAccess::kReadOnly;
}
std::string generatedId(uint8_t short_address, const char* suffix) {
char buffer[64];
std::snprintf(buffer, sizeof(buffer), "dali_%02u_%s", static_cast<unsigned>(short_address),
suffix == nullptr ? "point" : suffix);
return buffer;
}
std::string generatedName(uint8_t short_address, const char* name) {
char buffer[96];
std::snprintf(buffer, sizeof(buffer), "DALI %u %s", static_cast<unsigned>(short_address),
name == nullptr ? "point" : name);
return buffer;
}
void addGeneratedPoint(std::map<PointKey, GatewayModbusPoint>* points, uint8_t short_address,
const GeneratedPointSpec& spec) {
if (points == nullptr) {
return;
}
const uint16_t address = static_cast<uint16_t>(baseForSpace(spec.space) +
short_address * kShortStride + spec.offset);
GatewayModbusPoint point;
point.space = spec.space;
point.access = spec.access;
point.address = address;
point.id = generatedId(short_address, spec.suffix);
point.name = generatedName(short_address, spec.name);
point.generated = true;
point.generated_kind = spec.kind;
point.short_address = short_address;
(*points)[PointKey{spec.space, address}] = std::move(point);
}
GatewayModbusPointBinding toBinding(const GatewayModbusPoint& point) {
return GatewayModbusPointBinding{point.model_id,
point.space,
point.address,
point.id,
point.name,
point.generated,
point.generated_kind,
point.short_address,
point.access};
}
} // namespace
std::optional<GatewayModbusConfig> GatewayModbusConfigFromValue(const DaliValue* value) {
if (value == nullptr || value->asObject() == nullptr) {
return std::nullopt;
}
const auto& json = *value->asObject();
GatewayModbusConfig config;
config.transport = getObjectString(json, "transport").value_or("tcp-server");
config.host = getObjectString(json, "host").value_or("");
config.port = static_cast<uint16_t>(
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))));
return config;
}
DaliValue GatewayModbusConfigToValue(const GatewayModbusConfig& config) {
DaliValue::Object out;
out["transport"] = config.transport;
out["host"] = config.host;
out["port"] = static_cast<int>(config.port);
out["unitID"] = static_cast<int>(config.unit_id);
return DaliValue(std::move(out));
}
const char* GatewayModbusSpaceToString(GatewayModbusSpace space) {
switch (space) {
case GatewayModbusSpace::kCoil:
return "coil";
case GatewayModbusSpace::kDiscreteInput:
return "discrete_input";
case GatewayModbusSpace::kHoldingRegister:
return "holding_register";
case GatewayModbusSpace::kInputRegister:
return "input_register";
}
return "unknown";
}
const char* GatewayModbusAccessToString(GatewayModbusAccess access) {
switch (access) {
case GatewayModbusAccess::kReadOnly:
return "read_only";
case GatewayModbusAccess::kWriteOnly:
return "write_only";
case GatewayModbusAccess::kReadWrite:
return "read_write";
}
return "unknown";
}
const char* GatewayModbusGeneratedKindToString(GatewayModbusGeneratedKind kind) {
switch (kind) {
case GatewayModbusGeneratedKind::kShortOn:
return "short_on";
case GatewayModbusGeneratedKind::kShortOff:
return "short_off";
case GatewayModbusGeneratedKind::kShortRecallMax:
return "short_recall_max";
case GatewayModbusGeneratedKind::kShortRecallMin:
return "short_recall_min";
case GatewayModbusGeneratedKind::kShortDiscovered:
return "short_discovered";
case GatewayModbusGeneratedKind::kShortOnline:
return "short_online";
case GatewayModbusGeneratedKind::kShortSupportsDt1:
return "short_supports_dt1";
case GatewayModbusGeneratedKind::kShortSupportsDt4:
return "short_supports_dt4";
case GatewayModbusGeneratedKind::kShortSupportsDt5:
return "short_supports_dt5";
case GatewayModbusGeneratedKind::kShortSupportsDt6:
return "short_supports_dt6";
case GatewayModbusGeneratedKind::kShortSupportsDt8:
return "short_supports_dt8";
case GatewayModbusGeneratedKind::kShortGroupMaskKnown:
return "short_group_mask_known";
case GatewayModbusGeneratedKind::kShortActualLevelKnown:
return "short_actual_level_known";
case GatewayModbusGeneratedKind::kShortSceneKnown:
return "short_scene_known";
case GatewayModbusGeneratedKind::kShortSettingsKnown:
return "short_settings_known";
case GatewayModbusGeneratedKind::kShortControlGearPresent:
return "short_control_gear_present";
case GatewayModbusGeneratedKind::kShortLampFailure:
return "short_lamp_failure";
case GatewayModbusGeneratedKind::kShortLampPowerOn:
return "short_lamp_power_on";
case GatewayModbusGeneratedKind::kShortLimitError:
return "short_limit_error";
case GatewayModbusGeneratedKind::kShortFadingCompleted:
return "short_fading_completed";
case GatewayModbusGeneratedKind::kShortResetState:
return "short_reset_state";
case GatewayModbusGeneratedKind::kShortMissingShortAddress:
return "short_missing_short_address";
case GatewayModbusGeneratedKind::kShortPowerSupplyFault:
return "short_power_supply_fault";
case GatewayModbusGeneratedKind::kShortBrightness:
return "short_brightness";
case GatewayModbusGeneratedKind::kShortColorTemperature:
return "short_color_temperature";
case GatewayModbusGeneratedKind::kShortGroupMask:
return "short_group_mask";
case GatewayModbusGeneratedKind::kShortPowerOnLevel:
return "short_power_on_level";
case GatewayModbusGeneratedKind::kShortSystemFailureLevel:
return "short_system_failure_level";
case GatewayModbusGeneratedKind::kShortMinLevel:
return "short_min_level";
case GatewayModbusGeneratedKind::kShortMaxLevel:
return "short_max_level";
case GatewayModbusGeneratedKind::kShortFadeTime:
return "short_fade_time";
case GatewayModbusGeneratedKind::kShortFadeRate:
return "short_fade_rate";
case GatewayModbusGeneratedKind::kShortInventoryState:
return "short_inventory_state";
case GatewayModbusGeneratedKind::kShortPrimaryType:
return "short_primary_type";
case GatewayModbusGeneratedKind::kShortTypeMask:
return "short_type_mask";
case GatewayModbusGeneratedKind::kShortActualLevel:
return "short_actual_level";
case GatewayModbusGeneratedKind::kShortSceneId:
return "short_scene_id";
case GatewayModbusGeneratedKind::kShortRawStatus:
return "short_raw_status";
case GatewayModbusGeneratedKind::kNone:
default:
return "none";
}
}
int GatewayModbusHumanAddressFromWire(GatewayModbusSpace space, uint16_t zero_based_address) {
return baseForSpace(space) + static_cast<int>(zero_based_address);
}
std::optional<GatewayModbusSpace> GatewayModbusReadSpaceForFunction(uint8_t function_code) {
switch (function_code) {
case 0x01:
return GatewayModbusSpace::kCoil;
case 0x02:
return GatewayModbusSpace::kDiscreteInput;
case 0x03:
return GatewayModbusSpace::kHoldingRegister;
case 0x04:
return GatewayModbusSpace::kInputRegister;
default:
return std::nullopt;
}
}
std::optional<GatewayModbusSpace> GatewayModbusWriteSpaceForFunction(uint8_t function_code) {
switch (function_code) {
case 0x05:
case 0x0F:
return GatewayModbusSpace::kCoil;
case 0x06:
case 0x10:
return GatewayModbusSpace::kHoldingRegister;
default:
return std::nullopt;
}
}
GatewayModbusBridge::GatewayModbusBridge(DaliBridgeEngine& engine) : engine_(engine) {
rebuildMap();
}
void GatewayModbusBridge::setConfig(const GatewayModbusConfig& config) { config_ = config; }
const GatewayModbusConfig& GatewayModbusBridge::config() const { return config_; }
void GatewayModbusBridge::rebuildMap() {
std::map<PointKey, GatewayModbusPoint> next;
for (uint8_t short_address = 0; short_address < kShortAddressCount; ++short_address) {
for (const auto& spec : kGeneratedCoils) {
addGeneratedPoint(&next, short_address, spec);
}
for (const auto& spec : kGeneratedDiscreteInputs) {
addGeneratedPoint(&next, short_address, spec);
}
for (const auto& spec : kGeneratedHoldingRegisters) {
addGeneratedPoint(&next, short_address, spec);
}
for (const auto& spec : kGeneratedInputRegisters) {
addGeneratedPoint(&next, short_address, spec);
}
}
for (const auto& model : engine_.listModels()) {
if (model.protocol != BridgeProtocolKind::modbus || !model.external.registerAddress.has_value()) {
continue;
}
const auto space = spaceForObjectType(model.external.objectType);
if (!space.has_value()) {
continue;
}
GatewayModbusPoint point;
point.space = space.value();
point.access = accessForSpace(space.value());
point.address = static_cast<uint16_t>(model.external.registerAddress.value());
point.id = model.id;
point.name = model.displayName();
point.generated = false;
point.generated_kind = GatewayModbusGeneratedKind::kNone;
point.model_id = model.id;
point.operation = model.operation;
point.bit_index = model.external.bitIndex;
if (model.dali.kind == BridgeDaliTargetKind::shortAddress && model.dali.shortAddress.has_value()) {
point.short_address = model.dali.shortAddress.value();
}
next[PointKey{point.space, point.address}] = std::move(point);
}
points_.clear();
points_.reserve(next.size());
for (auto& entry : next) {
points_.push_back(std::move(entry.second));
}
}
std::optional<GatewayModbusPoint> GatewayModbusBridge::findPoint(GatewayModbusSpace space,
uint16_t address) const {
const auto found = std::find_if(points_.begin(), points_.end(), [space, address](const auto& point) {
return point.space == space && point.address == address;
});
if (found == points_.end()) {
return std::nullopt;
}
return *found;
}
std::vector<GatewayModbusPointBinding> GatewayModbusBridge::describePoints() const {
std::vector<GatewayModbusPointBinding> bindings;
bindings.reserve(points_.size());
for (const auto& point : points_) {
bindings.push_back(toBinding(point));
}
return bindings;
}
std::vector<GatewayModbusPointBinding> GatewayModbusBridge::describeHoldingRegisters() const {
std::vector<GatewayModbusPointBinding> bindings;
for (const auto& point : points_) {
if (point.space == GatewayModbusSpace::kHoldingRegister) {
bindings.push_back(toBinding(point));
}
}
return bindings;
}
DaliBridgeResult GatewayModbusBridge::readModelPoint(const GatewayModbusPoint& point) const {
return executeModelPoint(point, std::nullopt);
}
DaliBridgeResult GatewayModbusBridge::writeRegisterPoint(const GatewayModbusPoint& point,
uint16_t value) const {
return executeModelPoint(point, static_cast<int>(value));
}
DaliBridgeResult GatewayModbusBridge::writeCoilPoint(const GatewayModbusPoint& point,
bool value) const {
return executeModelPoint(point, value ? 1 : 0);
}
DaliBridgeResult GatewayModbusBridge::executeModelPoint(const GatewayModbusPoint& point,
std::optional<int> value) const {
DaliBridgeRequest request;
request.sequence = "modbus-" + std::to_string(point.address);
request.modelID = point.model_id;
if (value.has_value()) {
request.value = value.value();
}
if (point.model_id.empty()) {
DaliBridgeResult result;
result.sequence = request.sequence;
result.error = "generated Modbus point requires gateway handler";
return result;
}
return engine_.execute(request);
}
} // namespace gateway