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:
@@ -10,6 +10,10 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
|
|||||||
- `gateway_core/`: boot profile and top-level role bootstrap.
|
- `gateway_core/`: boot profile and top-level role bootstrap.
|
||||||
- `dali/`: vendored ESP-IDF DALI HAL/backend reused from LuatOS, including native raw receive fan-out.
|
- `dali/`: vendored ESP-IDF DALI HAL/backend reused from LuatOS, including native raw receive fan-out.
|
||||||
- `dali_domain/`: native DALI domain facade over `dali_cpp` and raw frame sinks.
|
- `dali_domain/`: native DALI domain facade over `dali_cpp` and raw frame sinks.
|
||||||
|
- `gateway_cache/`: DALI scene/group/settings/runtime cache used by controller reconciliation and protocol bridges.
|
||||||
|
- `gateway_bridge/`: per-channel bridge provisioning, command execution, protocol startup, and HTTP bridge actions.
|
||||||
|
- `gateway_modbus/`: gateway-owned Modbus TCP config, generated DALI point tables, and provisioned Modbus model override dispatch.
|
||||||
|
- `gateway_bacnet/`: BACnet/IP server adapter backed by bacnet-stack.
|
||||||
- `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`, including raw DALI notifications.
|
- `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`, including raw DALI notifications.
|
||||||
- `gateway_controller/`: Lua-compatible gateway command dispatcher, internal scene/group state, and notification fan-out.
|
- `gateway_controller/`: Lua-compatible gateway command dispatcher, internal scene/group state, and notification fan-out.
|
||||||
- `gateway_network/`: HTTP `/info`, `/dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP port `2020` command/notify routing, Wi-Fi STA lifecycle, ESP-Touch smartconfig, setup AP mode, ESP-NOW setup ingress, and BOOT-button Wi-Fi reset for the native gateway.
|
- `gateway_network/`: HTTP `/info`, `/dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP port `2020` command/notify routing, Wi-Fi STA lifecycle, ESP-Touch smartconfig, setup AP mode, ESP-NOW setup ingress, and BOOT-button Wi-Fi reset for the native gateway.
|
||||||
@@ -18,4 +22,17 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
|
|||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port with raw receive fan-out, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app also includes a `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, and a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, Wi-Fi STA lifecycle, ESP-Touch smartconfig credential provisioning, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, and BOOT-button Wi-Fi credential clearing. Startup behavior is configured in `main/Kconfig.projbuild`: BLE is enabled by default, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, and the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected. Runtime settings and internal scene/group data are cached in RAM after load, skip unchanged flash writes, and batch Wi-Fi credential commits to reduce flash stalls on ESP32-S3 boards where flash and PSRAM share the SPI bus. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. The checked-in `sdkconfig` is aligned with the app's custom 16 MB partition table so the Wi-Fi/BLE/network-enabled image fits the OTA app slots.
|
The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port with raw receive fan-out, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app also includes a `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, and a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, Wi-Fi STA lifecycle, ESP-Touch smartconfig credential provisioning, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, and BOOT-button Wi-Fi credential clearing. Startup behavior is configured in `main/Kconfig.projbuild`: BLE is enabled by default, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, and the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected. Runtime settings and internal scene/group data are cached in RAM after load, skip unchanged flash writes, and batch Wi-Fi credential commits to reduce flash stalls on ESP32-S3 boards where flash and PSRAM share the SPI bus. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. The checked-in `sdkconfig` is aligned with the app's custom 16 MB partition table so the Wi-Fi/BLE/network-enabled image fits the OTA app slots.
|
||||||
|
|
||||||
|
## Modbus TCP
|
||||||
|
|
||||||
|
Modbus TCP is owned by `gateway/components/gateway_modbus` and started through the per-channel bridge service. The gateway keeps the existing bridge config JSON shape with a top-level `modbus` object containing `transport`, `host`, `port`, and `unitID`, but parsing and runtime behavior now live in the gateway project rather than in `dali_cpp`.
|
||||||
|
|
||||||
|
The first generated map slice creates stable points for every DALI short address `0-63` whether the device is online, offline, or never seen. Per short address, the generated map reserves a 32-point stride in each Modbus space:
|
||||||
|
|
||||||
|
- Coils: command triggers such as on, off, recall max, and recall min.
|
||||||
|
- Discrete inputs: inventory, online, supported device-type, cache-known, and base status bit positions.
|
||||||
|
- Holding registers: writable brightness, color temperature, group mask, power-on level, system-failure level, min/max level, and fade time.
|
||||||
|
- Input registers: read-only inventory state, primary type, type mask, cached actual level, scene id, raw status placeholder, group mask, and cached settings.
|
||||||
|
|
||||||
|
Unknown numeric values read as `0xFFFF`; booleans read as false unless inventory or cache state proves otherwise. Provisioned Modbus models still work as overrides at their configured Modbus point, and normal generated reads prefer gateway cache state to avoid DALI bus polling.
|
||||||
@@ -391,7 +391,9 @@ config GATEWAY_MODBUS_BRIDGE_SUPPORTED
|
|||||||
depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED
|
depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED
|
||||||
default y
|
default y
|
||||||
help
|
help
|
||||||
Enables the per-channel Modbus TCP adapter backed by DaliModbusBridge. Runtime startup still requires persisted bridge config with Modbus settings.
|
Enables the gateway-owned per-channel Modbus TCP server, generated DALI point map,
|
||||||
|
and provisioned model overrides. Runtime startup still requires persisted bridge
|
||||||
|
config with Modbus settings.
|
||||||
|
|
||||||
config GATEWAY_START_MODBUS_BRIDGE_ENABLED
|
config GATEWAY_START_MODBUS_BRIDGE_ENABLED
|
||||||
bool "Start Modbus TCP bridge at startup"
|
bool "Start Modbus TCP bridge at startup"
|
||||||
|
|||||||
@@ -491,7 +491,8 @@ extern "C" void app_main(void) {
|
|||||||
static_cast<uint32_t>(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE);
|
static_cast<uint32_t>(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE);
|
||||||
bridge_config.bacnet_task_priority =
|
bridge_config.bacnet_task_priority =
|
||||||
static_cast<UBaseType_t>(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY);
|
static_cast<UBaseType_t>(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY);
|
||||||
s_bridge = std::make_unique<gateway::GatewayBridgeService>(*s_dali_domain, bridge_config);
|
s_bridge = std::make_unique<gateway::GatewayBridgeService>(*s_dali_domain, *s_cache,
|
||||||
|
bridge_config);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile.enable_wifi || profile.enable_eth) {
|
if (profile.enable_wifi || profile.enable_eth) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ set(GATEWAY_BRIDGE_REQUIRES
|
|||||||
dali_cpp
|
dali_cpp
|
||||||
espressif__cjson
|
espressif__cjson
|
||||||
freertos
|
freertos
|
||||||
|
gateway_cache
|
||||||
|
gateway_modbus
|
||||||
log
|
log
|
||||||
lwip
|
lwip
|
||||||
nvs_flash
|
nvs_flash
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
namespace gateway {
|
namespace gateway {
|
||||||
|
|
||||||
class DaliDomainService;
|
class DaliDomainService;
|
||||||
|
class GatewayCache;
|
||||||
|
|
||||||
struct GatewayBridgeServiceConfig {
|
struct GatewayBridgeServiceConfig {
|
||||||
bool bridge_enabled{true};
|
bool bridge_enabled{true};
|
||||||
@@ -35,6 +36,7 @@ struct GatewayBridgeHttpResponse {
|
|||||||
class GatewayBridgeService {
|
class GatewayBridgeService {
|
||||||
public:
|
public:
|
||||||
GatewayBridgeService(DaliDomainService& dali_domain,
|
GatewayBridgeService(DaliDomainService& dali_domain,
|
||||||
|
GatewayCache& cache,
|
||||||
GatewayBridgeServiceConfig config = {});
|
GatewayBridgeServiceConfig config = {});
|
||||||
~GatewayBridgeService();
|
~GatewayBridgeService();
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ class GatewayBridgeService {
|
|||||||
const ChannelRuntime* findRuntime(uint8_t gateway_id) const;
|
const ChannelRuntime* findRuntime(uint8_t gateway_id) const;
|
||||||
|
|
||||||
DaliDomainService& dali_domain_;
|
DaliDomainService& dali_domain_;
|
||||||
|
GatewayCache& cache_;
|
||||||
GatewayBridgeServiceConfig config_;
|
GatewayBridgeServiceConfig config_;
|
||||||
std::vector<std::unique_ptr<ChannelRuntime>> runtimes_;
|
std::vector<std::unique_ptr<ChannelRuntime>> runtimes_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,10 +8,12 @@
|
|||||||
#include "bridge_model.hpp"
|
#include "bridge_model.hpp"
|
||||||
#include "bridge_provisioning.hpp"
|
#include "bridge_provisioning.hpp"
|
||||||
#include "dali_comm.hpp"
|
#include "dali_comm.hpp"
|
||||||
|
#include "dali_define.hpp"
|
||||||
#include "dali_domain.hpp"
|
#include "dali_domain.hpp"
|
||||||
|
#include "gateway_cache.hpp"
|
||||||
#include "gateway_cloud.hpp"
|
#include "gateway_cloud.hpp"
|
||||||
|
#include "gateway_modbus.hpp"
|
||||||
#include "gateway_provisioning.hpp"
|
#include "gateway_provisioning.hpp"
|
||||||
#include "modbus_bridge.hpp"
|
|
||||||
|
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
@@ -34,13 +36,18 @@ namespace gateway {
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr const char* kTag = "gateway_bridge";
|
constexpr const char* kTag = "gateway_bridge";
|
||||||
constexpr int kDefaultModbusPort = 1502;
|
constexpr const char* kBridgeConfigKey = "bridge_cfg";
|
||||||
constexpr size_t kModbusMaxPduBytes = 252;
|
|
||||||
constexpr const char* kDiscoveryInventoryKey = "bridge_disc";
|
constexpr const char* kDiscoveryInventoryKey = "bridge_disc";
|
||||||
constexpr int kMaxDaliShortAddress = 63;
|
constexpr int kMaxDaliShortAddress = 63;
|
||||||
|
constexpr uint16_t kModbusUnknownRegister = 0xFFFF;
|
||||||
constexpr uint32_t kBacnetReliabilityNoFaultDetected = 0;
|
constexpr uint32_t kBacnetReliabilityNoFaultDetected = 0;
|
||||||
constexpr uint32_t kBacnetReliabilityCommunicationFailure = 12;
|
constexpr uint32_t kBacnetReliabilityCommunicationFailure = 12;
|
||||||
|
|
||||||
|
struct GatewayBridgeStoredConfig {
|
||||||
|
BridgeRuntimeConfig bridge;
|
||||||
|
std::optional<GatewayModbusConfig> modbus;
|
||||||
|
};
|
||||||
|
|
||||||
struct BridgeDiscoveryEntry {
|
struct BridgeDiscoveryEntry {
|
||||||
int short_address{0};
|
int short_address{0};
|
||||||
bool online{true};
|
bool online{true};
|
||||||
@@ -181,6 +188,39 @@ bool ValidShortAddress(int address) {
|
|||||||
return address >= 0 && address <= kMaxDaliShortAddress;
|
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) {
|
bool IsRawBridgeOperation(BridgeOperation operation) {
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case BridgeOperation::send:
|
case BridgeOperation::send:
|
||||||
@@ -711,14 +751,33 @@ cJSON* ToCjson(const DaliValue& value) {
|
|||||||
return cJSON_CreateNull();
|
return cJSON_CreateNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string BridgeRuntimeConfigToJson(const BridgeRuntimeConfig& config) {
|
DaliValue::Object GatewayBridgeStoredConfigToValue(
|
||||||
cJSON* root = ToCjson(DaliValue(config.toJson()));
|
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);
|
const std::string body = PrintJson(root);
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return body;
|
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());
|
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
|
||||||
if (root == nullptr) {
|
if (root == nullptr) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -729,7 +788,7 @@ std::optional<BridgeRuntimeConfig> BridgeRuntimeConfigFromJson(std::string_view
|
|||||||
if (object == nullptr) {
|
if (object == nullptr) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return BridgeRuntimeConfig::fromJson(*object);
|
return GatewayBridgeStoredConfigFromValue(*object);
|
||||||
}
|
}
|
||||||
|
|
||||||
GatewayCloudConfig GatewayCloudConfigFromJson(cJSON* root) {
|
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);
|
return SendModbusFrame(sock, mbap, pdu);
|
||||||
}
|
}
|
||||||
|
|
||||||
int HoldingRegisterFromWireAddress(uint16_t zero_based_address) {
|
|
||||||
return 40001 + static_cast<int>(zero_based_address);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
struct GatewayBridgeService::ChannelRuntime {
|
struct GatewayBridgeService::ChannelRuntime {
|
||||||
explicit ChannelRuntime(DaliDomainService& domain, DaliChannelInfo channel,
|
explicit ChannelRuntime(DaliDomainService& domain, GatewayCache& cache, DaliChannelInfo channel,
|
||||||
GatewayBridgeServiceConfig service_config)
|
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()) {}
|
lock(xSemaphoreCreateRecursiveMutex()) {}
|
||||||
|
|
||||||
~ChannelRuntime() {
|
~ChannelRuntime() {
|
||||||
@@ -875,17 +930,19 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DaliDomainService& domain;
|
DaliDomainService& domain;
|
||||||
|
GatewayCache& cache;
|
||||||
DaliChannelInfo channel;
|
DaliChannelInfo channel;
|
||||||
GatewayBridgeServiceConfig service_config;
|
GatewayBridgeServiceConfig service_config;
|
||||||
SemaphoreHandle_t lock{nullptr};
|
SemaphoreHandle_t lock{nullptr};
|
||||||
std::unique_ptr<DaliComm> comm;
|
std::unique_ptr<DaliComm> comm;
|
||||||
std::unique_ptr<DaliBridgeEngine> engine;
|
std::unique_ptr<DaliBridgeEngine> engine;
|
||||||
std::unique_ptr<DaliModbusBridge> modbus;
|
std::unique_ptr<GatewayModbusBridge> modbus;
|
||||||
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
||||||
std::unique_ptr<DaliBacnetBridge> bacnet;
|
std::unique_ptr<DaliBacnetBridge> bacnet;
|
||||||
#endif
|
#endif
|
||||||
std::unique_ptr<DaliCloudBridge> cloud;
|
std::unique_ptr<DaliCloudBridge> cloud;
|
||||||
BridgeRuntimeConfig bridge_config;
|
BridgeRuntimeConfig bridge_config;
|
||||||
|
std::optional<GatewayModbusConfig> modbus_config;
|
||||||
BridgeDiscoveryInventory discovery_inventory;
|
BridgeDiscoveryInventory discovery_inventory;
|
||||||
std::optional<GatewayCloudConfig> cloud_config;
|
std::optional<GatewayCloudConfig> cloud_config;
|
||||||
bool bridge_config_loaded{false};
|
bool bridge_config_loaded{false};
|
||||||
@@ -920,7 +977,13 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
[](uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); });
|
[](uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); });
|
||||||
|
|
||||||
BridgeProvisioningStore bridge_store(bridgeNamespace());
|
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;
|
DaliValue::Object discovery_object;
|
||||||
if (bridge_store.loadObject(kDiscoveryInventoryKey, &discovery_object) == ESP_OK) {
|
if (bridge_store.loadObject(kDiscoveryInventoryKey, &discovery_object) == ESP_OK) {
|
||||||
discovery_inventory = DiscoveryInventoryFromValue(discovery_object);
|
discovery_inventory = DiscoveryInventoryFromValue(discovery_object);
|
||||||
@@ -948,9 +1011,9 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
engine->upsertModel(model);
|
engine->upsertModel(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
modbus = std::make_unique<DaliModbusBridge>(*engine);
|
modbus = std::make_unique<GatewayModbusBridge>(*engine);
|
||||||
if (bridge_config.modbus.has_value()) {
|
if (modbus_config.has_value()) {
|
||||||
modbus->setConfig(bridge_config.modbus.value());
|
modbus->setConfig(modbus_config.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
||||||
@@ -1072,17 +1135,20 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t saveBridgeConfig(std::string_view json) {
|
esp_err_t saveBridgeConfig(std::string_view json) {
|
||||||
auto parsed = BridgeRuntimeConfigFromJson(json);
|
auto parsed = GatewayBridgeStoredConfigFromJson(json);
|
||||||
if (!parsed.has_value()) {
|
if (!parsed.has_value()) {
|
||||||
return ESP_ERR_INVALID_ARG;
|
return ESP_ERR_INVALID_ARG;
|
||||||
}
|
}
|
||||||
BridgeProvisioningStore store(bridgeNamespace());
|
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) {
|
if (err != ESP_OK) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
LockGuard guard(lock);
|
LockGuard guard(lock);
|
||||||
bridge_config = parsed.value();
|
bridge_config = parsed->bridge;
|
||||||
|
modbus_config = parsed->modbus;
|
||||||
bridge_config_loaded = true;
|
bridge_config_loaded = true;
|
||||||
applyBridgeConfigLocked();
|
applyBridgeConfigLocked();
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
@@ -1096,6 +1162,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
}
|
}
|
||||||
LockGuard guard(lock);
|
LockGuard guard(lock);
|
||||||
bridge_config = BridgeRuntimeConfig{};
|
bridge_config = BridgeRuntimeConfig{};
|
||||||
|
modbus_config.reset();
|
||||||
bridge_config_loaded = false;
|
bridge_config_loaded = false;
|
||||||
applyBridgeConfigLocked();
|
applyBridgeConfigLocked();
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
@@ -1329,10 +1396,10 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
if (modbus_json != nullptr) {
|
if (modbus_json != nullptr) {
|
||||||
cJSON_AddBoolToObject(modbus_json, "enabled", service_config.modbus_enabled);
|
cJSON_AddBoolToObject(modbus_json, "enabled", service_config.modbus_enabled);
|
||||||
cJSON_AddBoolToObject(modbus_json, "started", modbus_started);
|
cJSON_AddBoolToObject(modbus_json, "started", modbus_started);
|
||||||
if (bridge_config.modbus.has_value()) {
|
if (modbus_config.has_value()) {
|
||||||
cJSON_AddStringToObject(modbus_json, "transport", bridge_config.modbus->transport.c_str());
|
cJSON_AddStringToObject(modbus_json, "transport", modbus_config->transport.c_str());
|
||||||
cJSON_AddNumberToObject(modbus_json, "port", bridge_config.modbus->port);
|
cJSON_AddNumberToObject(modbus_json, "port", modbus_config->port);
|
||||||
cJSON_AddNumberToObject(modbus_json, "unitID", bridge_config.modbus->unitID);
|
cJSON_AddNumberToObject(modbus_json, "unitID", modbus_config->unit_id);
|
||||||
}
|
}
|
||||||
cJSON_AddItemToObject(root, "modbus", modbus_json);
|
cJSON_AddItemToObject(root, "modbus", modbus_json);
|
||||||
}
|
}
|
||||||
@@ -1378,7 +1445,8 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GatewayBridgeHttpResponse configJson() const {
|
GatewayBridgeHttpResponse configJson() const {
|
||||||
return GatewayBridgeHttpResponse{ESP_OK, BridgeRuntimeConfigToJson(bridge_config)};
|
return GatewayBridgeHttpResponse{ESP_OK,
|
||||||
|
GatewayBridgeStoredConfigToJson(bridge_config, modbus_config)};
|
||||||
}
|
}
|
||||||
|
|
||||||
GatewayBridgeHttpResponse inventoryJson() const {
|
GatewayBridgeHttpResponse inventoryJson() const {
|
||||||
@@ -1534,13 +1602,27 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
||||||
cJSON* bindings = cJSON_CreateArray();
|
cJSON* bindings = cJSON_CreateArray();
|
||||||
if (bindings != nullptr && modbus != nullptr) {
|
if (bindings != nullptr && modbus != nullptr) {
|
||||||
for (const auto& binding : modbus->describeHoldingRegisters()) {
|
for (const auto& binding : modbus->describePoints()) {
|
||||||
cJSON* item = cJSON_CreateObject();
|
cJSON* item = cJSON_CreateObject();
|
||||||
if (item == nullptr) {
|
if (item == nullptr) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
cJSON_AddStringToObject(item, "model", binding.modelID.c_str());
|
if (!binding.model_id.empty()) {
|
||||||
cJSON_AddNumberToObject(item, "registerAddress", binding.registerAddress);
|
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);
|
cJSON_AddItemToArray(bindings, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1633,6 +1715,305 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
return JsonOk(root);
|
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) {
|
esp_err_t startModbus(std::set<uint16_t>* used_ports = nullptr) {
|
||||||
LockGuard guard(lock);
|
LockGuard guard(lock);
|
||||||
if (!service_config.modbus_enabled) {
|
if (!service_config.modbus_enabled) {
|
||||||
@@ -1641,11 +2022,11 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
if (modbus_started || modbus_task_handle != nullptr) {
|
if (modbus_started || modbus_task_handle != nullptr) {
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
if (!bridge_config.modbus.has_value()) {
|
if (!modbus_config.has_value()) {
|
||||||
return ESP_ERR_NOT_FOUND;
|
return ESP_ERR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
const uint16_t port = bridge_config.modbus->port == 0 ? kDefaultModbusPort
|
const uint16_t port = modbus_config->port == 0 ? kGatewayModbusDefaultTcpPort
|
||||||
: bridge_config.modbus->port;
|
: modbus_config->port;
|
||||||
if (used_ports != nullptr) {
|
if (used_ports != nullptr) {
|
||||||
if (used_ports->find(port) != used_ports->end()) {
|
if (used_ports->find(port) != used_ports->end()) {
|
||||||
ESP_LOGW(kTag, "gateway=%u skips duplicate Modbus TCP port %u", channel.gateway_id, port);
|
ESP_LOGW(kTag, "gateway=%u skips duplicate Modbus TCP port %u", channel.gateway_id, port);
|
||||||
@@ -1666,9 +2047,9 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void modbusTaskLoop() {
|
void modbusTaskLoop() {
|
||||||
const uint16_t port = bridge_config.modbus.has_value() && bridge_config.modbus->port != 0
|
const uint16_t port = modbus_config.has_value() && modbus_config->port != 0
|
||||||
? bridge_config.modbus->port
|
? modbus_config->port
|
||||||
: kDefaultModbusPort;
|
: kGatewayModbusDefaultTcpPort;
|
||||||
const int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
const int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
||||||
if (listen_sock < 0) {
|
if (listen_sock < 0) {
|
||||||
ESP_LOGE(kTag, "gateway=%u failed to create Modbus socket", channel.gateway_id);
|
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))) {
|
while (RecvAll(client_sock, header, sizeof(header))) {
|
||||||
const uint16_t protocol_id = ReadBe16(&header[2]);
|
const uint16_t protocol_id = ReadBe16(&header[2]);
|
||||||
const uint16_t length = ReadBe16(&header[4]);
|
const uint16_t length = ReadBe16(&header[4]);
|
||||||
if (protocol_id != 0 || length < 2 || length > kModbusMaxPduBytes) {
|
if (protocol_id != 0 || length < 2 || length > kGatewayModbusMaxPduBytes) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1724,18 +2105,85 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bridge_config.modbus.has_value() && bridge_config.modbus->unitID != 0 &&
|
if (modbus_config.has_value() && modbus_config->unit_id != 0 &&
|
||||||
header[6] != bridge_config.modbus->unitID) {
|
header[6] != modbus_config->unit_id) {
|
||||||
SendModbusException(client_sock, header, pdu[0], 0x0B);
|
SendModbusException(client_sock, header, pdu[0], 0x0B);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pdu[0] == 0x06 && pdu.size() == 5) {
|
if ((pdu[0] == 0x01 || pdu[0] == 0x02) && pdu.size() == 5) {
|
||||||
const uint16_t wire_register = ReadBe16(&pdu[1]);
|
const auto space = GatewayModbusReadSpaceForFunction(pdu[0]);
|
||||||
const uint16_t value = ReadBe16(&pdu[3]);
|
const uint16_t start_address = ReadBe16(&pdu[1]);
|
||||||
const int holding_register = HoldingRegisterFromWireAddress(wire_register);
|
const uint16_t quantity = ReadBe16(&pdu[3]);
|
||||||
const auto result = handleHoldingRegisterWrite(holding_register, value);
|
if (!space.has_value() || quantity == 0 || quantity > kGatewayModbusMaxReadBits) {
|
||||||
if (!result.ok) {
|
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);
|
SendModbusException(client_sock, header, pdu[0], 0x04);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1743,11 +2191,58 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
continue;
|
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) {
|
if (pdu[0] == 0x10 && pdu.size() >= 6) {
|
||||||
const uint16_t start_register = ReadBe16(&pdu[1]);
|
const uint16_t start_register = ReadBe16(&pdu[1]);
|
||||||
const uint16_t quantity = ReadBe16(&pdu[3]);
|
const uint16_t quantity = ReadBe16(&pdu[3]);
|
||||||
const uint8_t byte_count = pdu[5];
|
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);
|
SendModbusException(client_sock, header, pdu[0], 0x03);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1755,9 +2250,9 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
for (uint16_t index = 0; index < quantity; ++index) {
|
for (uint16_t index = 0; index < quantity; ++index) {
|
||||||
const size_t offset = 6 + (index * 2);
|
const size_t offset = 6 + (index * 2);
|
||||||
const uint16_t value = ReadBe16(&pdu[offset]);
|
const uint16_t value = ReadBe16(&pdu[offset]);
|
||||||
const int holding_register = HoldingRegisterFromWireAddress(start_register + index);
|
const auto holding_register = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
|
||||||
const auto result = handleHoldingRegisterWrite(holding_register, value);
|
GatewayModbusSpace::kHoldingRegister, start_register + index));
|
||||||
if (!result.ok) {
|
if (!writeModbusRegisterPoint(holding_register, value)) {
|
||||||
ok = false;
|
ok = false;
|
||||||
break;
|
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,
|
GatewayBridgeService::GatewayBridgeService(DaliDomainService& dali_domain,
|
||||||
|
GatewayCache& cache,
|
||||||
GatewayBridgeServiceConfig config)
|
GatewayBridgeServiceConfig config)
|
||||||
: dali_domain_(dali_domain), config_(config) {}
|
: dali_domain_(dali_domain), cache_(cache), config_(config) {}
|
||||||
|
|
||||||
GatewayBridgeService::~GatewayBridgeService() = default;
|
GatewayBridgeService::~GatewayBridgeService() = default;
|
||||||
|
|
||||||
@@ -1808,7 +2294,7 @@ esp_err_t GatewayBridgeService::start() {
|
|||||||
const auto channels = dali_domain_.channelInfo();
|
const auto channels = dali_domain_.channelInfo();
|
||||||
runtimes_.reserve(channels.size());
|
runtimes_.reserve(channels.size());
|
||||||
for (const auto& channel : channels) {
|
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();
|
const esp_err_t err = runtime->start();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(kTag, "failed to start bridge runtime gateway=%u: %s", channel.gateway_id,
|
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