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,
|
||||
|
||||
Reference in New Issue
Block a user