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:
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user