Compare commits

...

2 Commits

Author SHA1 Message Date
Tony 694217eb2c 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>
2026-05-04 01:19:05 +08:00
Tony 8aa5a451a4 feat(gateway_bacnet): add support for new BACnet object types and enhance state management functions
Co-authored-by: Copilot <copilot@github.com>
2026-05-04 01:18:44 +08:00
13 changed files with 1440 additions and 64 deletions
+18 -1
View File
@@ -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.
- `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.
- `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_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.
@@ -18,4 +22,17 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
## 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.
+3 -1
View File
@@ -391,7 +391,9 @@ config GATEWAY_MODBUS_BRIDGE_SUPPORTED
depends on GATEWAY_BRIDGE_SUPPORTED && GATEWAY_WIFI_SUPPORTED
default y
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
bool "Start Modbus TCP bridge at startup"
+2 -1
View File
@@ -491,7 +491,8 @@ extern "C" void app_main(void) {
static_cast<uint32_t>(CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE);
bridge_config.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) {
+4
View File
@@ -49,11 +49,15 @@ set(BACNET_BASIC_SRCS
"${BACNET_SRC_ROOT}/bacnet/basic/binding/address.c"
"${BACNET_SRC_ROOT}/bacnet/basic/npdu/h_npdu.c"
"${BACNET_SRC_ROOT}/bacnet/basic/npdu/s_router.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/ai.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/ao.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/av.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/bi.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/bo.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/bv.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/device.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/ms-input.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/mso.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/msv.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/h_apdu.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/h_cov.c"
@@ -9,11 +9,15 @@ extern "C" {
typedef enum gateway_bacnet_object_kind {
GW_BACNET_OBJECT_UNKNOWN = 0,
GW_BACNET_OBJECT_ANALOG_INPUT,
GW_BACNET_OBJECT_ANALOG_VALUE,
GW_BACNET_OBJECT_ANALOG_OUTPUT,
GW_BACNET_OBJECT_BINARY_INPUT,
GW_BACNET_OBJECT_BINARY_VALUE,
GW_BACNET_OBJECT_BINARY_OUTPUT,
GW_BACNET_OBJECT_MULTI_STATE_INPUT,
GW_BACNET_OBJECT_MULTI_STATE_VALUE,
GW_BACNET_OBJECT_MULTI_STATE_OUTPUT,
} gateway_bacnet_object_kind_t;
typedef enum gateway_bacnet_write_value_kind {
@@ -42,16 +42,32 @@ GatewayBacnetServer* g_server = nullptr;
gateway_bacnet_object_kind_t ToBacnetKind(BridgeObjectType type) {
switch (type) {
case BridgeObjectType::analogInput:
return GW_BACNET_OBJECT_ANALOG_INPUT;
case BridgeObjectType::analogValue:
return GW_BACNET_OBJECT_ANALOG_VALUE;
case BridgeObjectType::analogOutput:
return GW_BACNET_OBJECT_ANALOG_OUTPUT;
case BridgeObjectType::binaryInput:
return GW_BACNET_OBJECT_BINARY_INPUT;
case BridgeObjectType::binaryValue:
return GW_BACNET_OBJECT_BINARY_VALUE;
case BridgeObjectType::binaryOutput:
return GW_BACNET_OBJECT_BINARY_OUTPUT;
case BridgeObjectType::multiStateInput:
return GW_BACNET_OBJECT_MULTI_STATE_INPUT;
case BridgeObjectType::multiStateValue:
return GW_BACNET_OBJECT_MULTI_STATE_VALUE;
case BridgeObjectType::multiStateOutput:
return GW_BACNET_OBJECT_MULTI_STATE_OUTPUT;
case BridgeObjectType::holdingRegister:
return GW_BACNET_OBJECT_ANALOG_VALUE;
case BridgeObjectType::inputRegister:
return GW_BACNET_OBJECT_ANALOG_INPUT;
case BridgeObjectType::coil:
return GW_BACNET_OBJECT_BINARY_OUTPUT;
case BridgeObjectType::discreteInput:
return GW_BACNET_OBJECT_BINARY_INPUT;
default:
return GW_BACNET_OBJECT_UNKNOWN;
}
@@ -59,16 +75,24 @@ gateway_bacnet_object_kind_t ToBacnetKind(BridgeObjectType type) {
BridgeObjectType FromBacnetKind(gateway_bacnet_object_kind_t kind) {
switch (kind) {
case GW_BACNET_OBJECT_ANALOG_INPUT:
return BridgeObjectType::analogInput;
case GW_BACNET_OBJECT_ANALOG_VALUE:
return BridgeObjectType::analogValue;
case GW_BACNET_OBJECT_ANALOG_OUTPUT:
return BridgeObjectType::analogOutput;
case GW_BACNET_OBJECT_BINARY_INPUT:
return BridgeObjectType::binaryInput;
case GW_BACNET_OBJECT_BINARY_VALUE:
return BridgeObjectType::binaryValue;
case GW_BACNET_OBJECT_BINARY_OUTPUT:
return BridgeObjectType::binaryOutput;
case GW_BACNET_OBJECT_MULTI_STATE_INPUT:
return BridgeObjectType::multiStateInput;
case GW_BACNET_OBJECT_MULTI_STATE_VALUE:
return BridgeObjectType::multiStateValue;
case GW_BACNET_OBJECT_MULTI_STATE_OUTPUT:
return BridgeObjectType::multiStateOutput;
default:
return BridgeObjectType::unknown;
}
@@ -5,11 +5,15 @@
#include "bacnet/apdu.h"
#include "bacnet/basic/binding/address.h"
#include "bacnet/basic/object/ai.h"
#include "bacnet/basic/object/ao.h"
#include "bacnet/basic/object/av.h"
#include "bacnet/basic/object/bi.h"
#include "bacnet/basic/object/bo.h"
#include "bacnet/basic/object/bv.h"
#include "bacnet/basic/object/device.h"
#include "bacnet/basic/object/ms-input.h"
#include "bacnet/basic/object/mso.h"
#include "bacnet/basic/object/msv.h"
#include "bacnet/basic/service/h_apdu.h"
#include "bacnet/basic/services.h"
@@ -89,6 +93,46 @@ static bool clear_multistate_value_objects(void)
return true;
}
static bool clear_analog_input_objects(void)
{
unsigned count = Analog_Input_Count();
while (count > 0) {
count--;
Analog_Input_Delete(Analog_Input_Index_To_Instance(count));
}
return true;
}
static bool clear_binary_input_objects(void)
{
unsigned count = Binary_Input_Count();
while (count > 0) {
count--;
Binary_Input_Delete(Binary_Input_Index_To_Instance(count));
}
return true;
}
static bool clear_multistate_input_objects(void)
{
unsigned count = Multistate_Input_Count();
while (count > 0) {
count--;
Multistate_Input_Delete(Multistate_Input_Index_To_Instance(count));
}
return true;
}
static bool clear_multistate_output_objects(void)
{
unsigned count = Multistate_Output_Count();
while (count > 0) {
count--;
Multistate_Output_Delete(Multistate_Output_Index_To_Instance(count));
}
return true;
}
static void set_analog_value_state(
uint32_t object_instance, bool out_of_service, BACNET_RELIABILITY reliability)
{
@@ -124,6 +168,34 @@ static void set_multistate_value_state(
Multistate_Value_Reliability_Set(object_instance, reliability);
}
static void set_analog_input_state(
uint32_t object_instance, bool out_of_service, BACNET_RELIABILITY reliability)
{
Analog_Input_Out_Of_Service_Set(object_instance, out_of_service);
Analog_Input_Reliability_Set(object_instance, reliability);
}
static void set_binary_input_state(
uint32_t object_instance, bool out_of_service, BACNET_RELIABILITY reliability)
{
Binary_Input_Out_Of_Service_Set(object_instance, out_of_service);
Binary_Input_Reliability_Set(object_instance, reliability);
}
static void set_multistate_input_state(
uint32_t object_instance, bool out_of_service, BACNET_RELIABILITY reliability)
{
Multistate_Input_Out_Of_Service_Set(object_instance, out_of_service);
Multistate_Input_Reliability_Set(object_instance, reliability);
}
static void set_multistate_output_state(
uint32_t object_instance, bool out_of_service, BACNET_RELIABILITY reliability)
{
Multistate_Output_Out_Of_Service_Set(object_instance, out_of_service);
Multistate_Output_Reliability_Set(object_instance, reliability);
}
static void notify_write_real(
gateway_bacnet_object_kind_t object_kind, uint32_t object_instance, double value)
{
@@ -198,6 +270,25 @@ static void multistate_value_write(uint32_t object_instance, uint32_t old_value,
notify_write_unsigned(GW_BACNET_OBJECT_MULTI_STATE_VALUE, object_instance, value);
}
static void binary_input_write(
uint32_t object_instance, BACNET_BINARY_PV old_value, BACNET_BINARY_PV value)
{
(void)old_value;
notify_write_boolean(GW_BACNET_OBJECT_BINARY_INPUT, object_instance, value == BINARY_ACTIVE);
}
static void multistate_input_write(uint32_t object_instance, uint32_t old_value, uint32_t value)
{
(void)old_value;
notify_write_unsigned(GW_BACNET_OBJECT_MULTI_STATE_INPUT, object_instance, value);
}
static void multistate_output_write(uint32_t object_instance, uint32_t old_value, uint32_t value)
{
(void)old_value;
notify_write_unsigned(GW_BACNET_OBJECT_MULTI_STATE_OUTPUT, object_instance, value);
}
static object_functions_t Object_Table[] = {
{ OBJECT_DEVICE, NULL, Device_Count, Device_Index_To_Instance,
Device_Valid_Object_Instance_Number, Device_Object_Name, Device_Read_Property_Local,
@@ -234,6 +325,32 @@ static object_functions_t Object_Table[] = {
Multistate_Value_Encode_Value_List, Multistate_Value_Change_Of_Value,
Multistate_Value_Change_Of_Value_Clear, NULL, NULL, NULL, Multistate_Value_Create,
Multistate_Value_Delete, NULL, Multistate_Value_Writable_Property_List },
{ OBJECT_ANALOG_INPUT, Analog_Input_Init, Analog_Input_Count,
Analog_Input_Index_To_Instance, Analog_Input_Valid_Instance, Analog_Input_Object_Name,
Analog_Input_Read_Property, Analog_Input_Write_Property, Analog_Input_Property_Lists,
NULL, NULL, Analog_Input_Encode_Value_List, Analog_Input_Change_Of_Value,
Analog_Input_Change_Of_Value_Clear, NULL, NULL, NULL, Analog_Input_Create,
Analog_Input_Delete, NULL, Analog_Input_Writable_Property_List },
{ OBJECT_BINARY_INPUT, Binary_Input_Init, Binary_Input_Count,
Binary_Input_Index_To_Instance, Binary_Input_Valid_Instance, Binary_Input_Object_Name,
Binary_Input_Read_Property, Binary_Input_Write_Property, Binary_Input_Property_Lists,
NULL, NULL, Binary_Input_Encode_Value_List, Binary_Input_Change_Of_Value,
Binary_Input_Change_Of_Value_Clear, NULL, NULL, NULL, Binary_Input_Create,
Binary_Input_Delete, NULL, Binary_Input_Writable_Property_List },
{ OBJECT_MULTI_STATE_INPUT, Multistate_Input_Init, Multistate_Input_Count,
Multistate_Input_Index_To_Instance, Multistate_Input_Valid_Instance,
Multistate_Input_Object_Name, Multistate_Input_Read_Property,
Multistate_Input_Write_Property, Multistate_Input_Property_Lists, NULL, NULL,
Multistate_Input_Encode_Value_List, Multistate_Input_Change_Of_Value,
Multistate_Input_Change_Of_Value_Clear, NULL, NULL, NULL, Multistate_Input_Create,
Multistate_Input_Delete, NULL, Multistate_Input_Writable_Property_List },
{ OBJECT_MULTI_STATE_OUTPUT, Multistate_Output_Init, Multistate_Output_Count,
Multistate_Output_Index_To_Instance, Multistate_Output_Valid_Instance,
Multistate_Output_Object_Name, Multistate_Output_Read_Property,
Multistate_Output_Write_Property, Multistate_Output_Property_Lists, NULL, NULL,
Multistate_Output_Encode_Value_List, Multistate_Output_Change_Of_Value,
Multistate_Output_Change_Of_Value_Clear, NULL, NULL, NULL, Multistate_Output_Create,
Multistate_Output_Delete, NULL, Multistate_Output_Writable_Property_List },
{ MAX_BACNET_OBJECT_TYPE, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL },
};
@@ -269,6 +386,9 @@ bool gateway_bacnet_stack_start(
Binary_Value_Write_Present_Value_Callback_Set(binary_value_write);
Binary_Output_Write_Present_Value_Callback_Set(binary_output_write);
Multistate_Value_Write_Present_Value_Callback_Set(multistate_value_write);
Binary_Input_Write_Present_Value_Callback_Set(binary_input_write);
Multistate_Input_Write_Present_Value_Callback_Set(multistate_input_write);
Multistate_Output_Write_Present_Value_Callback_Set(multistate_output_write);
apdu_set_unrecognized_service_handler_handler(handler_unrecognized_service);
apdu_set_unconfirmed_handler(SERVICE_UNCONFIRMED_WHO_IS, handler_who_is);
@@ -358,6 +478,45 @@ bool gateway_bacnet_stack_upsert_object(
Multistate_Value_Present_Value_Set(object_instance, 1);
set_multistate_value_state(object_instance, out_of_service, object_reliability);
return true;
case GW_BACNET_OBJECT_ANALOG_INPUT:
if (!Analog_Input_Valid_Instance(object_instance)) {
Analog_Input_Create(object_instance);
}
Analog_Input_Name_Set(object_instance, object_name);
Analog_Input_Description_Set(object_instance, description);
Analog_Input_Units_Set(object_instance, UNITS_PERCENT);
Analog_Input_Present_Value_Set(object_instance, 0.0f);
set_analog_input_state(object_instance, out_of_service, object_reliability);
return true;
case GW_BACNET_OBJECT_BINARY_INPUT:
if (!Binary_Input_Valid_Instance(object_instance)) {
Binary_Input_Create(object_instance);
}
Binary_Input_Name_Set(object_instance, object_name);
Binary_Input_Description_Set(object_instance, description);
Binary_Input_Present_Value_Set(object_instance, BINARY_INACTIVE);
set_binary_input_state(object_instance, out_of_service, object_reliability);
return true;
case GW_BACNET_OBJECT_MULTI_STATE_INPUT:
if (!Multistate_Input_Valid_Instance(object_instance)) {
Multistate_Input_Create(object_instance);
}
Multistate_Input_Name_Set(object_instance, object_name);
Multistate_Input_Description_Set(object_instance, description);
Multistate_Input_State_Text_List_Set(object_instance, Multistate_Value_States);
Multistate_Input_Present_Value_Set(object_instance, 1);
set_multistate_input_state(object_instance, out_of_service, object_reliability);
return true;
case GW_BACNET_OBJECT_MULTI_STATE_OUTPUT:
if (!Multistate_Output_Valid_Instance(object_instance)) {
Multistate_Output_Create(object_instance);
}
Multistate_Output_Name_Set(object_instance, object_name);
Multistate_Output_Description_Set(object_instance, description);
Multistate_Output_State_Text_List_Set(object_instance, Multistate_Value_States);
Multistate_Output_Present_Value_Set(object_instance, 1, BACNET_MAX_PRIORITY);
set_multistate_output_state(object_instance, out_of_service, object_reliability);
return true;
default:
return false;
}
@@ -367,9 +526,13 @@ bool gateway_bacnet_stack_clear_objects(void)
{
return clear_analog_value_objects() &&
clear_analog_output_objects() &&
clear_analog_input_objects() &&
clear_binary_value_objects() &&
clear_binary_output_objects() &&
clear_multistate_value_objects();
clear_binary_input_objects() &&
clear_multistate_value_objects() &&
clear_multistate_input_objects() &&
clear_multistate_output_objects();
}
void gateway_bacnet_stack_send_i_am(void)
+2
View File
@@ -3,6 +3,8 @@ set(GATEWAY_BRIDGE_REQUIRES
dali_cpp
espressif__cjson
freertos
gateway_cache
gateway_modbus
log
lwip
nvs_flash
@@ -12,6 +12,7 @@
namespace gateway {
class DaliDomainService;
class GatewayCache;
struct GatewayBridgeServiceConfig {
bool bridge_enabled{true};
@@ -35,6 +36,7 @@ struct GatewayBridgeHttpResponse {
class GatewayBridgeService {
public:
GatewayBridgeService(DaliDomainService& dali_domain,
GatewayCache& cache,
GatewayBridgeServiceConfig config = {});
~GatewayBridgeService();
@@ -52,6 +54,7 @@ class GatewayBridgeService {
const ChannelRuntime* findRuntime(uint8_t gateway_id) const;
DaliDomainService& dali_domain_;
GatewayCache& cache_;
GatewayBridgeServiceConfig config_;
std::vector<std::unique_ptr<ChannelRuntime>> runtimes_;
};
+546 -60
View File
@@ -8,10 +8,12 @@
#include "bridge_model.hpp"
#include "bridge_provisioning.hpp"
#include "dali_comm.hpp"
#include "dali_define.hpp"
#include "dali_domain.hpp"
#include "gateway_cache.hpp"
#include "gateway_cloud.hpp"
#include "gateway_modbus.hpp"
#include "gateway_provisioning.hpp"
#include "modbus_bridge.hpp"
#include "cJSON.h"
#include "esp_log.h"
@@ -34,13 +36,18 @@ namespace gateway {
namespace {
constexpr const char* kTag = "gateway_bridge";
constexpr int kDefaultModbusPort = 1502;
constexpr size_t kModbusMaxPduBytes = 252;
constexpr const char* kBridgeConfigKey = "bridge_cfg";
constexpr const char* kDiscoveryInventoryKey = "bridge_disc";
constexpr int kMaxDaliShortAddress = 63;
constexpr uint16_t kModbusUnknownRegister = 0xFFFF;
constexpr uint32_t kBacnetReliabilityNoFaultDetected = 0;
constexpr uint32_t kBacnetReliabilityCommunicationFailure = 12;
struct GatewayBridgeStoredConfig {
BridgeRuntimeConfig bridge;
std::optional<GatewayModbusConfig> modbus;
};
struct BridgeDiscoveryEntry {
int short_address{0};
bool online{true};
@@ -181,6 +188,39 @@ bool ValidShortAddress(int address) {
return address >= 0 && address <= kMaxDaliShortAddress;
}
uint8_t RawArcAddressFromDec(int dec_address) {
if (dec_address >= 0 && dec_address < 64) {
return static_cast<uint8_t>(dec_address * 2);
}
if (dec_address >= 64 && dec_address < 80) {
return static_cast<uint8_t>(0x80 + (dec_address - 64) * 2);
}
return 0xfe;
}
uint8_t RawCommandAddressFromDec(int dec_address) {
if (dec_address >= 0 && dec_address < 64) {
return static_cast<uint8_t>(dec_address * 2 + 1);
}
if (dec_address >= 64 && dec_address < 80) {
return static_cast<uint8_t>(0x80 + (dec_address - 64) * 2 + 1);
}
return 0xff;
}
uint16_t DeviceTypeMask(const DaliDomainSnapshot& snapshot) {
uint16_t mask = 0;
const auto types = snapshot.int_arrays.find("types");
if (types != snapshot.int_arrays.end()) {
for (const int type : types->second) {
if (type >= 0 && type < 16) {
mask |= static_cast<uint16_t>(1U << type);
}
}
}
return mask;
}
bool IsRawBridgeOperation(BridgeOperation operation) {
switch (operation) {
case BridgeOperation::send:
@@ -711,14 +751,33 @@ cJSON* ToCjson(const DaliValue& value) {
return cJSON_CreateNull();
}
std::string BridgeRuntimeConfigToJson(const BridgeRuntimeConfig& config) {
cJSON* root = ToCjson(DaliValue(config.toJson()));
DaliValue::Object GatewayBridgeStoredConfigToValue(
const BridgeRuntimeConfig& bridge_config,
const std::optional<GatewayModbusConfig>& modbus_config) {
DaliValue::Object out = bridge_config.toJson();
if (modbus_config.has_value()) {
out["modbus"] = GatewayModbusConfigToValue(modbus_config.value());
}
return out;
}
std::string GatewayBridgeStoredConfigToJson(
const BridgeRuntimeConfig& bridge_config,
const std::optional<GatewayModbusConfig>& modbus_config) {
cJSON* root = ToCjson(DaliValue(GatewayBridgeStoredConfigToValue(bridge_config, modbus_config)));
const std::string body = PrintJson(root);
cJSON_Delete(root);
return body;
}
std::optional<BridgeRuntimeConfig> BridgeRuntimeConfigFromJson(std::string_view json) {
GatewayBridgeStoredConfig GatewayBridgeStoredConfigFromValue(const DaliValue::Object& object) {
GatewayBridgeStoredConfig config;
config.bridge = BridgeRuntimeConfig::fromJson(object);
config.modbus = GatewayModbusConfigFromValue(getObjectValue(object, "modbus"));
return config;
}
std::optional<GatewayBridgeStoredConfig> GatewayBridgeStoredConfigFromJson(std::string_view json) {
cJSON* root = cJSON_ParseWithLength(json.data(), json.size());
if (root == nullptr) {
return std::nullopt;
@@ -729,7 +788,7 @@ std::optional<BridgeRuntimeConfig> BridgeRuntimeConfigFromJson(std::string_view
if (object == nullptr) {
return std::nullopt;
}
return BridgeRuntimeConfig::fromJson(*object);
return GatewayBridgeStoredConfigFromValue(*object);
}
GatewayCloudConfig GatewayCloudConfigFromJson(cJSON* root) {
@@ -852,16 +911,12 @@ bool SendModbusException(int sock, const uint8_t* mbap, uint8_t function_code,
return SendModbusFrame(sock, mbap, pdu);
}
int HoldingRegisterFromWireAddress(uint16_t zero_based_address) {
return 40001 + static_cast<int>(zero_based_address);
}
} // namespace
struct GatewayBridgeService::ChannelRuntime {
explicit ChannelRuntime(DaliDomainService& domain, DaliChannelInfo channel,
explicit ChannelRuntime(DaliDomainService& domain, GatewayCache& cache, DaliChannelInfo channel,
GatewayBridgeServiceConfig service_config)
: domain(domain), channel(std::move(channel)), service_config(service_config),
: domain(domain), cache(cache), channel(std::move(channel)), service_config(service_config),
lock(xSemaphoreCreateRecursiveMutex()) {}
~ChannelRuntime() {
@@ -875,17 +930,19 @@ struct GatewayBridgeService::ChannelRuntime {
}
DaliDomainService& domain;
GatewayCache& cache;
DaliChannelInfo channel;
GatewayBridgeServiceConfig service_config;
SemaphoreHandle_t lock{nullptr};
std::unique_ptr<DaliComm> comm;
std::unique_ptr<DaliBridgeEngine> engine;
std::unique_ptr<DaliModbusBridge> modbus;
std::unique_ptr<GatewayModbusBridge> modbus;
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
std::unique_ptr<DaliBacnetBridge> bacnet;
#endif
std::unique_ptr<DaliCloudBridge> cloud;
BridgeRuntimeConfig bridge_config;
std::optional<GatewayModbusConfig> modbus_config;
BridgeDiscoveryInventory discovery_inventory;
std::optional<GatewayCloudConfig> cloud_config;
bool bridge_config_loaded{false};
@@ -920,7 +977,13 @@ struct GatewayBridgeService::ChannelRuntime {
[](uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); });
BridgeProvisioningStore bridge_store(bridgeNamespace());
bridge_config_loaded = bridge_store.load(&bridge_config) == ESP_OK;
DaliValue::Object bridge_object;
if (bridge_store.loadObject(kBridgeConfigKey, &bridge_object) == ESP_OK) {
const auto stored_config = GatewayBridgeStoredConfigFromValue(bridge_object);
bridge_config = stored_config.bridge;
modbus_config = stored_config.modbus;
bridge_config_loaded = true;
}
DaliValue::Object discovery_object;
if (bridge_store.loadObject(kDiscoveryInventoryKey, &discovery_object) == ESP_OK) {
discovery_inventory = DiscoveryInventoryFromValue(discovery_object);
@@ -948,9 +1011,9 @@ struct GatewayBridgeService::ChannelRuntime {
engine->upsertModel(model);
}
modbus = std::make_unique<DaliModbusBridge>(*engine);
if (bridge_config.modbus.has_value()) {
modbus->setConfig(bridge_config.modbus.value());
modbus = std::make_unique<GatewayModbusBridge>(*engine);
if (modbus_config.has_value()) {
modbus->setConfig(modbus_config.value());
}
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
@@ -1072,17 +1135,20 @@ struct GatewayBridgeService::ChannelRuntime {
}
esp_err_t saveBridgeConfig(std::string_view json) {
auto parsed = BridgeRuntimeConfigFromJson(json);
auto parsed = GatewayBridgeStoredConfigFromJson(json);
if (!parsed.has_value()) {
return ESP_ERR_INVALID_ARG;
}
BridgeProvisioningStore store(bridgeNamespace());
const esp_err_t err = store.save(parsed.value());
const esp_err_t err = store.saveObject(
kBridgeConfigKey,
GatewayBridgeStoredConfigToValue(parsed->bridge, parsed->modbus));
if (err != ESP_OK) {
return err;
}
LockGuard guard(lock);
bridge_config = parsed.value();
bridge_config = parsed->bridge;
modbus_config = parsed->modbus;
bridge_config_loaded = true;
applyBridgeConfigLocked();
return ESP_OK;
@@ -1096,6 +1162,7 @@ struct GatewayBridgeService::ChannelRuntime {
}
LockGuard guard(lock);
bridge_config = BridgeRuntimeConfig{};
modbus_config.reset();
bridge_config_loaded = false;
applyBridgeConfigLocked();
return ESP_OK;
@@ -1329,10 +1396,10 @@ struct GatewayBridgeService::ChannelRuntime {
if (modbus_json != nullptr) {
cJSON_AddBoolToObject(modbus_json, "enabled", service_config.modbus_enabled);
cJSON_AddBoolToObject(modbus_json, "started", modbus_started);
if (bridge_config.modbus.has_value()) {
cJSON_AddStringToObject(modbus_json, "transport", bridge_config.modbus->transport.c_str());
cJSON_AddNumberToObject(modbus_json, "port", bridge_config.modbus->port);
cJSON_AddNumberToObject(modbus_json, "unitID", bridge_config.modbus->unitID);
if (modbus_config.has_value()) {
cJSON_AddStringToObject(modbus_json, "transport", modbus_config->transport.c_str());
cJSON_AddNumberToObject(modbus_json, "port", modbus_config->port);
cJSON_AddNumberToObject(modbus_json, "unitID", modbus_config->unit_id);
}
cJSON_AddItemToObject(root, "modbus", modbus_json);
}
@@ -1378,7 +1445,8 @@ struct GatewayBridgeService::ChannelRuntime {
}
GatewayBridgeHttpResponse configJson() const {
return GatewayBridgeHttpResponse{ESP_OK, BridgeRuntimeConfigToJson(bridge_config)};
return GatewayBridgeHttpResponse{ESP_OK,
GatewayBridgeStoredConfigToJson(bridge_config, modbus_config)};
}
GatewayBridgeHttpResponse inventoryJson() const {
@@ -1534,13 +1602,27 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
cJSON* bindings = cJSON_CreateArray();
if (bindings != nullptr && modbus != nullptr) {
for (const auto& binding : modbus->describeHoldingRegisters()) {
for (const auto& binding : modbus->describePoints()) {
cJSON* item = cJSON_CreateObject();
if (item == nullptr) {
continue;
}
cJSON_AddStringToObject(item, "model", binding.modelID.c_str());
cJSON_AddNumberToObject(item, "registerAddress", binding.registerAddress);
if (!binding.model_id.empty()) {
cJSON_AddStringToObject(item, "model", binding.model_id.c_str());
}
cJSON_AddStringToObject(item, "space", GatewayModbusSpaceToString(binding.space));
cJSON_AddNumberToObject(item, "address", binding.address);
cJSON_AddStringToObject(item, "id", binding.id.c_str());
cJSON_AddStringToObject(item, "name", binding.name.c_str());
cJSON_AddStringToObject(item, "access", GatewayModbusAccessToString(binding.access));
cJSON_AddBoolToObject(item, "generated", binding.generated);
if (binding.generated) {
cJSON_AddStringToObject(item, "generatedKind",
GatewayModbusGeneratedKindToString(binding.generated_kind));
}
if (binding.short_address >= 0) {
cJSON_AddNumberToObject(item, "shortAddress", binding.short_address);
}
cJSON_AddItemToArray(bindings, item);
}
}
@@ -1633,6 +1715,305 @@ struct GatewayBridgeService::ChannelRuntime {
return JsonOk(root);
}
std::optional<bool> readGeneratedBoolPointLocked(const GatewayModbusPoint& point) {
if (!point.generated || !ValidShortAddress(point.short_address)) {
return std::nullopt;
}
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
const auto state = cache.daliAddressState(channel.gateway_id,
static_cast<uint8_t>(point.short_address));
switch (point.generated_kind) {
case GatewayModbusGeneratedKind::kShortDiscovered:
return discovery != nullptr;
case GatewayModbusGeneratedKind::kShortOnline:
return discovery != nullptr && discovery->online;
case GatewayModbusGeneratedKind::kShortSupportsDt1:
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 1);
case GatewayModbusGeneratedKind::kShortSupportsDt4:
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 4);
case GatewayModbusGeneratedKind::kShortSupportsDt5:
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 5);
case GatewayModbusGeneratedKind::kShortSupportsDt6:
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 6);
case GatewayModbusGeneratedKind::kShortSupportsDt8:
return discovery != nullptr && SnapshotHasDeviceType(discovery->discovery, 8);
case GatewayModbusGeneratedKind::kShortGroupMaskKnown:
return state.group_mask_known;
case GatewayModbusGeneratedKind::kShortActualLevelKnown:
return state.status.actual_level.has_value();
case GatewayModbusGeneratedKind::kShortSceneKnown:
return state.status.scene_id.has_value();
case GatewayModbusGeneratedKind::kShortSettingsKnown:
return state.settings.anyKnown();
case GatewayModbusGeneratedKind::kShortControlGearPresent:
case GatewayModbusGeneratedKind::kShortLampFailure:
case GatewayModbusGeneratedKind::kShortLampPowerOn:
case GatewayModbusGeneratedKind::kShortLimitError:
case GatewayModbusGeneratedKind::kShortFadingCompleted:
case GatewayModbusGeneratedKind::kShortResetState:
case GatewayModbusGeneratedKind::kShortMissingShortAddress:
case GatewayModbusGeneratedKind::kShortPowerSupplyFault:
return false;
default:
return std::nullopt;
}
}
std::optional<uint16_t> readGeneratedRegisterPointLocked(const GatewayModbusPoint& point) {
if (!point.generated || !ValidShortAddress(point.short_address)) {
return std::nullopt;
}
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
const auto state = cache.daliAddressState(channel.gateway_id,
static_cast<uint8_t>(point.short_address));
switch (point.generated_kind) {
case GatewayModbusGeneratedKind::kShortInventoryState:
if (discovery == nullptr) {
return 0;
}
return discovery->online ? 2 : 1;
case GatewayModbusGeneratedKind::kShortPrimaryType: {
if (discovery == nullptr) {
return kModbusUnknownRegister;
}
const auto primary = discovery->discovery.ints.find("primaryType");
return primary == discovery->discovery.ints.end()
? kModbusUnknownRegister
: static_cast<uint16_t>(primary->second);
}
case GatewayModbusGeneratedKind::kShortTypeMask:
return discovery == nullptr ? kModbusUnknownRegister : DeviceTypeMask(discovery->discovery);
case GatewayModbusGeneratedKind::kShortBrightness:
case GatewayModbusGeneratedKind::kShortActualLevel:
return state.status.actual_level.has_value()
? static_cast<uint16_t>(state.status.actual_level.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortSceneId:
return state.status.scene_id.has_value()
? static_cast<uint16_t>(state.status.scene_id.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortRawStatus:
return kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortGroupMask:
return state.group_mask_known ? state.group_mask : kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortPowerOnLevel:
return state.settings.power_on_level.has_value()
? static_cast<uint16_t>(state.settings.power_on_level.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortSystemFailureLevel:
return state.settings.system_failure_level.has_value()
? static_cast<uint16_t>(state.settings.system_failure_level.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortMinLevel:
return state.settings.min_level.has_value()
? static_cast<uint16_t>(state.settings.min_level.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortMaxLevel:
return state.settings.max_level.has_value()
? static_cast<uint16_t>(state.settings.max_level.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortFadeTime:
return state.settings.fade_time.has_value()
? static_cast<uint16_t>(state.settings.fade_time.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortFadeRate:
return state.settings.fade_rate.has_value()
? static_cast<uint16_t>(state.settings.fade_rate.value())
: kModbusUnknownRegister;
case GatewayModbusGeneratedKind::kShortColorTemperature:
return kModbusUnknownRegister;
default:
return std::nullopt;
}
}
bool writeGeneratedCoilPointLocked(const GatewayModbusPoint& point, bool value) {
if (!point.generated || !ValidShortAddress(point.short_address)) {
return false;
}
if (!value) {
return true;
}
const uint8_t raw_command_address = RawCommandAddressFromDec(point.short_address);
bool sent = false;
uint8_t mirrored_command = 0;
switch (point.generated_kind) {
case GatewayModbusGeneratedKind::kShortOn:
case GatewayModbusGeneratedKind::kShortRecallMax:
sent = domain.on(channel.gateway_id, point.short_address);
mirrored_command = DALI_CMD_RECALL_MAX;
break;
case GatewayModbusGeneratedKind::kShortOff:
sent = domain.off(channel.gateway_id, point.short_address);
mirrored_command = DALI_CMD_OFF;
break;
case GatewayModbusGeneratedKind::kShortRecallMin:
sent = domain.sendRaw(channel.gateway_id, raw_command_address, DALI_CMD_RECALL_MIN);
mirrored_command = DALI_CMD_RECALL_MIN;
break;
default:
return false;
}
if (sent) {
cache.mirrorDaliCommand(channel.gateway_id, raw_command_address, mirrored_command);
}
return sent;
}
bool writeGeneratedRegisterPointLocked(const GatewayModbusPoint& point, uint16_t value) {
if (!point.generated || !ValidShortAddress(point.short_address)) {
return false;
}
switch (point.generated_kind) {
case GatewayModbusGeneratedKind::kShortBrightness:
if (value > 254) {
return false;
}
if (domain.setBright(channel.gateway_id, point.short_address, value)) {
cache.mirrorDaliCommand(channel.gateway_id, RawArcAddressFromDec(point.short_address),
static_cast<uint8_t>(value));
return true;
}
return false;
case GatewayModbusGeneratedKind::kShortColorTemperature:
return domain.setColTemp(channel.gateway_id, point.short_address, value);
case GatewayModbusGeneratedKind::kShortGroupMask:
if (domain.applyGroupMask(channel.gateway_id, point.short_address, value)) {
cache.setDaliGroupMask(channel.gateway_id, static_cast<uint8_t>(point.short_address),
value);
return true;
}
return false;
case GatewayModbusGeneratedKind::kShortPowerOnLevel:
case GatewayModbusGeneratedKind::kShortSystemFailureLevel:
case GatewayModbusGeneratedKind::kShortMinLevel:
case GatewayModbusGeneratedKind::kShortMaxLevel:
case GatewayModbusGeneratedKind::kShortFadeTime:
case GatewayModbusGeneratedKind::kShortFadeRate: {
if (value > 255) {
return false;
}
auto current = cache.daliAddressState(channel.gateway_id,
static_cast<uint8_t>(point.short_address)).settings;
switch (point.generated_kind) {
case GatewayModbusGeneratedKind::kShortPowerOnLevel:
current.power_on_level = static_cast<uint8_t>(value);
break;
case GatewayModbusGeneratedKind::kShortSystemFailureLevel:
current.system_failure_level = static_cast<uint8_t>(value);
break;
case GatewayModbusGeneratedKind::kShortMinLevel:
current.min_level = static_cast<uint8_t>(value);
break;
case GatewayModbusGeneratedKind::kShortMaxLevel:
current.max_level = static_cast<uint8_t>(value);
break;
case GatewayModbusGeneratedKind::kShortFadeTime:
current.fade_time = static_cast<uint8_t>(value);
break;
case GatewayModbusGeneratedKind::kShortFadeRate:
current.fade_rate = static_cast<uint8_t>(value);
break;
default:
break;
}
DaliAddressSettingsSnapshot domain_settings;
domain_settings.power_on_level = current.power_on_level;
domain_settings.system_failure_level = current.system_failure_level;
domain_settings.min_level = current.min_level;
domain_settings.max_level = current.max_level;
domain_settings.fade_time = current.fade_time;
domain_settings.fade_rate = current.fade_rate;
if (domain.applyAddressSettings(channel.gateway_id, point.short_address, domain_settings)) {
cache.setDaliSettings(channel.gateway_id, static_cast<uint8_t>(point.short_address),
current);
return true;
}
return false;
}
default:
return false;
}
}
std::optional<bool> readModbusBoolPoint(GatewayModbusSpace space, uint16_t address) {
LockGuard guard(lock);
if (modbus == nullptr) {
return std::nullopt;
}
const auto point = modbus->findPoint(space, address);
if (!point.has_value()) {
return std::nullopt;
}
if (point->generated) {
return readGeneratedBoolPointLocked(point.value()).value_or(false);
}
const DaliBridgeResult result = modbus->readModelPoint(point.value());
if (!result.ok || !result.data.has_value()) {
return std::nullopt;
}
if (point->bit_index.has_value() && point->bit_index.value() >= 0 &&
point->bit_index.value() < 16) {
return (result.data.value() & (1 << point->bit_index.value())) != 0;
}
return result.data.value() != 0;
}
std::optional<uint16_t> readModbusRegisterPoint(GatewayModbusSpace space, uint16_t address) {
LockGuard guard(lock);
if (modbus == nullptr) {
return std::nullopt;
}
const auto point = modbus->findPoint(space, address);
if (!point.has_value()) {
return std::nullopt;
}
if (point->generated) {
return readGeneratedRegisterPointLocked(point.value()).value_or(kModbusUnknownRegister);
}
const DaliBridgeResult result = modbus->readModelPoint(point.value());
if (!result.ok || !result.data.has_value()) {
return std::nullopt;
}
return static_cast<uint16_t>(result.data.value() & 0xFFFF);
}
bool writeModbusCoilPoint(uint16_t address, bool value) {
LockGuard guard(lock);
if (modbus == nullptr) {
return false;
}
const auto point = modbus->findPoint(GatewayModbusSpace::kCoil, address);
if (!point.has_value()) {
return false;
}
if (point->generated) {
return writeGeneratedCoilPointLocked(point.value(), value);
}
const DaliBridgeResult result = modbus->writeCoilPoint(point.value(), value);
return result.ok;
}
bool writeModbusRegisterPoint(uint16_t address, uint16_t value) {
LockGuard guard(lock);
if (modbus == nullptr) {
return false;
}
const auto point = modbus->findPoint(GatewayModbusSpace::kHoldingRegister, address);
if (!point.has_value()) {
return false;
}
if (point->generated) {
return writeGeneratedRegisterPointLocked(point.value(), value);
}
const DaliBridgeResult result = modbus->writeRegisterPoint(point.value(), value);
return result.ok;
}
esp_err_t startModbus(std::set<uint16_t>* used_ports = nullptr) {
LockGuard guard(lock);
if (!service_config.modbus_enabled) {
@@ -1641,11 +2022,11 @@ struct GatewayBridgeService::ChannelRuntime {
if (modbus_started || modbus_task_handle != nullptr) {
return ESP_OK;
}
if (!bridge_config.modbus.has_value()) {
if (!modbus_config.has_value()) {
return ESP_ERR_NOT_FOUND;
}
const uint16_t port = bridge_config.modbus->port == 0 ? kDefaultModbusPort
: bridge_config.modbus->port;
const uint16_t port = modbus_config->port == 0 ? kGatewayModbusDefaultTcpPort
: modbus_config->port;
if (used_ports != nullptr) {
if (used_ports->find(port) != used_ports->end()) {
ESP_LOGW(kTag, "gateway=%u skips duplicate Modbus TCP port %u", channel.gateway_id, port);
@@ -1666,9 +2047,9 @@ struct GatewayBridgeService::ChannelRuntime {
}
void modbusTaskLoop() {
const uint16_t port = bridge_config.modbus.has_value() && bridge_config.modbus->port != 0
? bridge_config.modbus->port
: kDefaultModbusPort;
const uint16_t port = modbus_config.has_value() && modbus_config->port != 0
? modbus_config->port
: kGatewayModbusDefaultTcpPort;
const int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (listen_sock < 0) {
ESP_LOGE(kTag, "gateway=%u failed to create Modbus socket", channel.gateway_id);
@@ -1715,7 +2096,7 @@ struct GatewayBridgeService::ChannelRuntime {
while (RecvAll(client_sock, header, sizeof(header))) {
const uint16_t protocol_id = ReadBe16(&header[2]);
const uint16_t length = ReadBe16(&header[4]);
if (protocol_id != 0 || length < 2 || length > kModbusMaxPduBytes) {
if (protocol_id != 0 || length < 2 || length > kGatewayModbusMaxPduBytes) {
break;
}
@@ -1724,18 +2105,85 @@ struct GatewayBridgeService::ChannelRuntime {
break;
}
if (bridge_config.modbus.has_value() && bridge_config.modbus->unitID != 0 &&
header[6] != bridge_config.modbus->unitID) {
if (modbus_config.has_value() && modbus_config->unit_id != 0 &&
header[6] != modbus_config->unit_id) {
SendModbusException(client_sock, header, pdu[0], 0x0B);
continue;
}
if (pdu[0] == 0x06 && pdu.size() == 5) {
const uint16_t wire_register = ReadBe16(&pdu[1]);
const uint16_t value = ReadBe16(&pdu[3]);
const int holding_register = HoldingRegisterFromWireAddress(wire_register);
const auto result = handleHoldingRegisterWrite(holding_register, value);
if (!result.ok) {
if ((pdu[0] == 0x01 || pdu[0] == 0x02) && pdu.size() == 5) {
const auto space = GatewayModbusReadSpaceForFunction(pdu[0]);
const uint16_t start_address = ReadBe16(&pdu[1]);
const uint16_t quantity = ReadBe16(&pdu[3]);
if (!space.has_value() || quantity == 0 || quantity > kGatewayModbusMaxReadBits) {
SendModbusException(client_sock, header, pdu[0], 0x03);
continue;
}
const uint8_t byte_count = static_cast<uint8_t>((quantity + 7U) / 8U);
std::vector<uint8_t> response(2 + byte_count, 0);
response[0] = pdu[0];
response[1] = byte_count;
bool ok = true;
for (uint16_t index = 0; index < quantity; ++index) {
const auto human_address = static_cast<uint16_t>(
GatewayModbusHumanAddressFromWire(space.value(), start_address + index));
const auto value = readModbusBoolPoint(space.value(), human_address);
if (!value.has_value()) {
ok = false;
break;
}
if (value.value()) {
response[2 + (index / 8)] |= static_cast<uint8_t>(1U << (index % 8));
}
}
if (!ok) {
SendModbusException(client_sock, header, pdu[0], 0x02);
continue;
}
SendModbusFrame(client_sock, header, response);
continue;
}
if ((pdu[0] == 0x03 || pdu[0] == 0x04) && pdu.size() == 5) {
const auto space = GatewayModbusReadSpaceForFunction(pdu[0]);
const uint16_t start_address = ReadBe16(&pdu[1]);
const uint16_t quantity = ReadBe16(&pdu[3]);
if (!space.has_value() || quantity == 0 || quantity > kGatewayModbusMaxReadRegisters) {
SendModbusException(client_sock, header, pdu[0], 0x03);
continue;
}
std::vector<uint8_t> response(2 + quantity * 2);
response[0] = pdu[0];
response[1] = static_cast<uint8_t>(quantity * 2);
bool ok = true;
for (uint16_t index = 0; index < quantity; ++index) {
const auto human_address = static_cast<uint16_t>(
GatewayModbusHumanAddressFromWire(space.value(), start_address + index));
const auto value = readModbusRegisterPoint(space.value(), human_address);
if (!value.has_value()) {
ok = false;
break;
}
WriteBe16(&response[2 + index * 2], value.value());
}
if (!ok) {
SendModbusException(client_sock, header, pdu[0], 0x02);
continue;
}
SendModbusFrame(client_sock, header, response);
continue;
}
if (pdu[0] == 0x05 && pdu.size() == 5) {
const uint16_t wire_address = ReadBe16(&pdu[1]);
const uint16_t raw_value = ReadBe16(&pdu[3]);
if (raw_value != 0x0000 && raw_value != 0xFF00) {
SendModbusException(client_sock, header, pdu[0], 0x03);
continue;
}
const auto coil = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
GatewayModbusSpace::kCoil, wire_address));
if (!writeModbusCoilPoint(coil, raw_value == 0xFF00)) {
SendModbusException(client_sock, header, pdu[0], 0x04);
continue;
}
@@ -1743,11 +2191,58 @@ struct GatewayBridgeService::ChannelRuntime {
continue;
}
if (pdu[0] == 0x06 && pdu.size() == 5) {
const uint16_t wire_register = ReadBe16(&pdu[1]);
const uint16_t value = ReadBe16(&pdu[3]);
const auto holding_register = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
GatewayModbusSpace::kHoldingRegister, wire_register));
if (!writeModbusRegisterPoint(holding_register, value)) {
SendModbusException(client_sock, header, pdu[0], 0x04);
continue;
}
SendModbusFrame(client_sock, header, pdu);
continue;
}
if (pdu[0] == 0x0F && pdu.size() >= 6) {
const uint16_t start_address = ReadBe16(&pdu[1]);
const uint16_t quantity = ReadBe16(&pdu[3]);
const uint8_t byte_count = pdu[5];
if (quantity == 0 || quantity > kGatewayModbusMaxWriteBits ||
pdu.size() != static_cast<size_t>(6 + byte_count) ||
byte_count != static_cast<uint8_t>((quantity + 7U) / 8U)) {
SendModbusException(client_sock, header, pdu[0], 0x03);
continue;
}
bool ok = true;
for (uint16_t index = 0; index < quantity; ++index) {
const bool value = (pdu[6 + (index / 8)] & (1U << (index % 8))) != 0;
const auto coil = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
GatewayModbusSpace::kCoil, start_address + index));
if (!writeModbusCoilPoint(coil, value)) {
ok = false;
break;
}
}
if (!ok) {
SendModbusException(client_sock, header, pdu[0], 0x04);
continue;
}
std::vector<uint8_t> response(5);
response[0] = pdu[0];
WriteBe16(&response[1], start_address);
WriteBe16(&response[3], quantity);
SendModbusFrame(client_sock, header, response);
continue;
}
if (pdu[0] == 0x10 && pdu.size() >= 6) {
const uint16_t start_register = ReadBe16(&pdu[1]);
const uint16_t quantity = ReadBe16(&pdu[3]);
const uint8_t byte_count = pdu[5];
if (pdu.size() != static_cast<size_t>(6 + byte_count) || byte_count != quantity * 2) {
if (quantity == 0 || quantity > kGatewayModbusMaxWriteRegisters ||
pdu.size() != static_cast<size_t>(6 + byte_count) ||
byte_count != quantity * 2) {
SendModbusException(client_sock, header, pdu[0], 0x03);
continue;
}
@@ -1755,9 +2250,9 @@ struct GatewayBridgeService::ChannelRuntime {
for (uint16_t index = 0; index < quantity; ++index) {
const size_t offset = 6 + (index * 2);
const uint16_t value = ReadBe16(&pdu[offset]);
const int holding_register = HoldingRegisterFromWireAddress(start_register + index);
const auto result = handleHoldingRegisterWrite(holding_register, value);
if (!result.ok) {
const auto holding_register = static_cast<uint16_t>(GatewayModbusHumanAddressFromWire(
GatewayModbusSpace::kHoldingRegister, start_register + index));
if (!writeModbusRegisterPoint(holding_register, value)) {
ok = false;
break;
}
@@ -1778,21 +2273,12 @@ struct GatewayBridgeService::ChannelRuntime {
}
}
DaliBridgeResult handleHoldingRegisterWrite(int holding_register, int value) {
LockGuard guard(lock);
if (modbus == nullptr) {
DaliBridgeResult result;
result.sequence = "modbus-" + std::to_string(holding_register);
result.error = "modbus bridge not ready";
return result;
}
return modbus->handleHoldingRegisterWrite(holding_register, value);
}
};
GatewayBridgeService::GatewayBridgeService(DaliDomainService& dali_domain,
GatewayCache& cache,
GatewayBridgeServiceConfig config)
: dali_domain_(dali_domain), config_(config) {}
: dali_domain_(dali_domain), cache_(cache), config_(config) {}
GatewayBridgeService::~GatewayBridgeService() = default;
@@ -1808,7 +2294,7 @@ esp_err_t GatewayBridgeService::start() {
const auto channels = dali_domain_.channelInfo();
runtimes_.reserve(channels.size());
for (const auto& channel : channels) {
auto runtime = std::make_unique<ChannelRuntime>(dali_domain_, channel, config_);
auto runtime = std::make_unique<ChannelRuntime>(dali_domain_, cache_, channel, config_);
const esp_err_t err = runtime->start();
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to start bridge runtime gateway=%u: %s", channel.gateway_id,
+7
View File
@@ -0,0 +1,7 @@
idf_component_register(
SRCS "src/gateway_modbus.cpp"
INCLUDE_DIRS "include"
REQUIRES dali_cpp
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -0,0 +1,147 @@
#pragma once
#include "bridge.hpp"
#include "bridge_model.hpp"
#include "model_value.hpp"
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace gateway {
constexpr uint16_t kGatewayModbusDefaultTcpPort = 1502;
constexpr size_t kGatewayModbusMaxPduBytes = 252;
constexpr uint16_t kGatewayModbusMaxReadBits = 2000;
constexpr uint16_t kGatewayModbusMaxReadRegisters = 125;
constexpr uint16_t kGatewayModbusMaxWriteBits = 1968;
constexpr uint16_t kGatewayModbusMaxWriteRegisters = 123;
struct GatewayModbusConfig {
std::string transport{"tcp-server"};
std::string host;
uint16_t port{kGatewayModbusDefaultTcpPort};
uint8_t unit_id{1};
};
enum class GatewayModbusSpace : uint8_t {
kCoil = 1,
kDiscreteInput = 2,
kHoldingRegister = 3,
kInputRegister = 4,
};
enum class GatewayModbusAccess : uint8_t {
kReadOnly = 0,
kWriteOnly = 1,
kReadWrite = 2,
};
enum class GatewayModbusGeneratedKind : uint8_t {
kNone = 0,
kShortOn,
kShortOff,
kShortRecallMax,
kShortRecallMin,
kShortDiscovered,
kShortOnline,
kShortSupportsDt1,
kShortSupportsDt4,
kShortSupportsDt5,
kShortSupportsDt6,
kShortSupportsDt8,
kShortGroupMaskKnown,
kShortActualLevelKnown,
kShortSceneKnown,
kShortSettingsKnown,
kShortControlGearPresent,
kShortLampFailure,
kShortLampPowerOn,
kShortLimitError,
kShortFadingCompleted,
kShortResetState,
kShortMissingShortAddress,
kShortPowerSupplyFault,
kShortBrightness,
kShortColorTemperature,
kShortGroupMask,
kShortPowerOnLevel,
kShortSystemFailureLevel,
kShortMinLevel,
kShortMaxLevel,
kShortFadeTime,
kShortFadeRate,
kShortInventoryState,
kShortPrimaryType,
kShortTypeMask,
kShortActualLevel,
kShortSceneId,
kShortRawStatus,
};
struct GatewayModbusPoint {
GatewayModbusSpace space{GatewayModbusSpace::kHoldingRegister};
GatewayModbusAccess access{GatewayModbusAccess::kReadWrite};
uint16_t address{0};
std::string id;
std::string name;
bool generated{false};
GatewayModbusGeneratedKind generated_kind{GatewayModbusGeneratedKind::kNone};
int short_address{-1};
std::string model_id;
BridgeOperation operation{BridgeOperation::unknown};
std::optional<int> bit_index;
};
struct GatewayModbusPointBinding {
std::string model_id;
GatewayModbusSpace space{GatewayModbusSpace::kHoldingRegister};
uint16_t address{0};
std::string id;
std::string name;
bool generated{false};
GatewayModbusGeneratedKind generated_kind{GatewayModbusGeneratedKind::kNone};
int short_address{-1};
GatewayModbusAccess access{GatewayModbusAccess::kReadWrite};
};
std::optional<GatewayModbusConfig> GatewayModbusConfigFromValue(const DaliValue* value);
DaliValue GatewayModbusConfigToValue(const GatewayModbusConfig& config);
const char* GatewayModbusSpaceToString(GatewayModbusSpace space);
const char* GatewayModbusAccessToString(GatewayModbusAccess access);
const char* GatewayModbusGeneratedKindToString(GatewayModbusGeneratedKind kind);
int GatewayModbusHumanAddressFromWire(GatewayModbusSpace space, uint16_t zero_based_address);
std::optional<GatewayModbusSpace> GatewayModbusReadSpaceForFunction(uint8_t function_code);
std::optional<GatewayModbusSpace> GatewayModbusWriteSpaceForFunction(uint8_t function_code);
class GatewayModbusBridge {
public:
explicit GatewayModbusBridge(DaliBridgeEngine& engine);
void setConfig(const GatewayModbusConfig& config);
const GatewayModbusConfig& config() const;
void rebuildMap();
std::optional<GatewayModbusPoint> findPoint(GatewayModbusSpace space,
uint16_t address) const;
std::vector<GatewayModbusPointBinding> describePoints() const;
std::vector<GatewayModbusPointBinding> describeHoldingRegisters() const;
DaliBridgeResult readModelPoint(const GatewayModbusPoint& point) const;
DaliBridgeResult writeRegisterPoint(const GatewayModbusPoint& point, uint16_t value) const;
DaliBridgeResult writeCoilPoint(const GatewayModbusPoint& point, bool value) const;
private:
DaliBridgeResult executeModelPoint(const GatewayModbusPoint& point,
std::optional<int> value) const;
DaliBridgeEngine& engine_;
GatewayModbusConfig config_;
std::vector<GatewayModbusPoint> points_;
};
} // namespace gateway
@@ -0,0 +1,516 @@
#include "gateway_modbus.hpp"
#include <algorithm>
#include <array>
#include <cstdio>
#include <map>
#include <utility>
namespace gateway {
namespace {
constexpr uint16_t kCoilBase = 1;
constexpr uint16_t kDiscreteInputBase = 10001;
constexpr uint16_t kInputRegisterBase = 30001;
constexpr uint16_t kHoldingRegisterBase = 40001;
constexpr uint16_t kShortAddressCount = 64;
constexpr uint16_t kShortStride = 32;
struct PointKey {
GatewayModbusSpace space{GatewayModbusSpace::kHoldingRegister};
uint16_t address{0};
bool operator<(const PointKey& other) const {
if (space != other.space) {
return static_cast<uint8_t>(space) < static_cast<uint8_t>(other.space);
}
return address < other.address;
}
};
struct GeneratedPointSpec {
uint16_t offset;
GatewayModbusSpace space;
GatewayModbusAccess access;
GatewayModbusGeneratedKind kind;
const char* suffix;
const char* name;
};
constexpr std::array<GeneratedPointSpec, 4> kGeneratedCoils{{
{0, GatewayModbusSpace::kCoil, GatewayModbusAccess::kWriteOnly,
GatewayModbusGeneratedKind::kShortOn, "on", "recall max"},
{1, GatewayModbusSpace::kCoil, GatewayModbusAccess::kWriteOnly,
GatewayModbusGeneratedKind::kShortOff, "off", "off"},
{2, GatewayModbusSpace::kCoil, GatewayModbusAccess::kWriteOnly,
GatewayModbusGeneratedKind::kShortRecallMax, "recall_max", "recall max"},
{3, GatewayModbusSpace::kCoil, GatewayModbusAccess::kWriteOnly,
GatewayModbusGeneratedKind::kShortRecallMin, "recall_min", "recall min"},
}};
constexpr std::array<GeneratedPointSpec, 18> kGeneratedDiscreteInputs{{
{0, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortDiscovered, "discovered", "discovered"},
{1, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortOnline, "online", "online"},
{2, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSupportsDt1, "supports_dt1", "supports DT1"},
{3, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSupportsDt4, "supports_dt4", "supports DT4"},
{4, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSupportsDt5, "supports_dt5", "supports DT5"},
{5, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSupportsDt6, "supports_dt6", "supports DT6"},
{6, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSupportsDt8, "supports_dt8", "supports DT8"},
{7, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortGroupMaskKnown, "group_mask_known", "group mask known"},
{8, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortActualLevelKnown, "actual_level_known", "actual level known"},
{9, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSceneKnown, "scene_known", "scene known"},
{10, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSettingsKnown, "settings_known", "settings known"},
{16, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortControlGearPresent, "control_gear_present",
"control gear present"},
{17, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortLampFailure, "lamp_failure", "lamp failure"},
{18, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortLampPowerOn, "lamp_power_on", "lamp power on"},
{19, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortLimitError, "limit_error", "limit error"},
{20, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortFadingCompleted, "fading_completed", "fading completed"},
{21, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortResetState, "reset_state", "reset state"},
{22, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortMissingShortAddress, "missing_short_address",
"missing short address"},
}};
constexpr std::array<GeneratedPointSpec, 8> kGeneratedHoldingRegisters{{
{0, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortBrightness, "brightness", "brightness"},
{1, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortColorTemperature, "color_temperature",
"color temperature"},
{2, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortGroupMask, "group_mask", "group mask"},
{3, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortPowerOnLevel, "power_on_level", "power-on level"},
{4, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortSystemFailureLevel, "system_failure_level",
"system-failure level"},
{5, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortMinLevel, "min_level", "minimum level"},
{6, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortMaxLevel, "max_level", "maximum level"},
{7, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
GatewayModbusGeneratedKind::kShortFadeTime, "fade_time", "fade time"},
}};
constexpr std::array<GeneratedPointSpec, 13> kGeneratedInputRegisters{{
{0, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortInventoryState, "inventory_state", "inventory state"},
{1, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortPrimaryType, "primary_type", "primary type"},
{2, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortTypeMask, "type_mask", "device type mask"},
{3, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortActualLevel, "actual_level", "actual level"},
{4, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSceneId, "scene_id", "scene id"},
{5, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortRawStatus, "raw_status", "raw status"},
{6, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortGroupMask, "group_mask", "group mask"},
{7, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortPowerOnLevel, "power_on_level", "power-on level"},
{8, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortSystemFailureLevel, "system_failure_level",
"system-failure level"},
{9, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortMinLevel, "min_level", "minimum level"},
{10, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortMaxLevel, "max_level", "maximum level"},
{11, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortFadeTime, "fade_time", "fade time"},
{12, GatewayModbusSpace::kInputRegister, GatewayModbusAccess::kReadOnly,
GatewayModbusGeneratedKind::kShortFadeRate, "fade_rate", "fade rate"},
}};
uint16_t baseForSpace(GatewayModbusSpace space) {
switch (space) {
case GatewayModbusSpace::kCoil:
return kCoilBase;
case GatewayModbusSpace::kDiscreteInput:
return kDiscreteInputBase;
case GatewayModbusSpace::kInputRegister:
return kInputRegisterBase;
case GatewayModbusSpace::kHoldingRegister:
return kHoldingRegisterBase;
}
return kHoldingRegisterBase;
}
std::optional<GatewayModbusSpace> spaceForObjectType(BridgeObjectType type) {
switch (type) {
case BridgeObjectType::coil:
return GatewayModbusSpace::kCoil;
case BridgeObjectType::discreteInput:
return GatewayModbusSpace::kDiscreteInput;
case BridgeObjectType::inputRegister:
return GatewayModbusSpace::kInputRegister;
case BridgeObjectType::holdingRegister:
return GatewayModbusSpace::kHoldingRegister;
default:
return std::nullopt;
}
}
GatewayModbusAccess accessForSpace(GatewayModbusSpace space) {
switch (space) {
case GatewayModbusSpace::kCoil:
case GatewayModbusSpace::kHoldingRegister:
return GatewayModbusAccess::kReadWrite;
case GatewayModbusSpace::kDiscreteInput:
case GatewayModbusSpace::kInputRegister:
return GatewayModbusAccess::kReadOnly;
}
return GatewayModbusAccess::kReadOnly;
}
std::string generatedId(uint8_t short_address, const char* suffix) {
char buffer[64];
std::snprintf(buffer, sizeof(buffer), "dali_%02u_%s", static_cast<unsigned>(short_address),
suffix == nullptr ? "point" : suffix);
return buffer;
}
std::string generatedName(uint8_t short_address, const char* name) {
char buffer[96];
std::snprintf(buffer, sizeof(buffer), "DALI %u %s", static_cast<unsigned>(short_address),
name == nullptr ? "point" : name);
return buffer;
}
void addGeneratedPoint(std::map<PointKey, GatewayModbusPoint>* points, uint8_t short_address,
const GeneratedPointSpec& spec) {
if (points == nullptr) {
return;
}
const uint16_t address = static_cast<uint16_t>(baseForSpace(spec.space) +
short_address * kShortStride + spec.offset);
GatewayModbusPoint point;
point.space = spec.space;
point.access = spec.access;
point.address = address;
point.id = generatedId(short_address, spec.suffix);
point.name = generatedName(short_address, spec.name);
point.generated = true;
point.generated_kind = spec.kind;
point.short_address = short_address;
(*points)[PointKey{spec.space, address}] = std::move(point);
}
GatewayModbusPointBinding toBinding(const GatewayModbusPoint& point) {
return GatewayModbusPointBinding{point.model_id,
point.space,
point.address,
point.id,
point.name,
point.generated,
point.generated_kind,
point.short_address,
point.access};
}
} // namespace
std::optional<GatewayModbusConfig> GatewayModbusConfigFromValue(const DaliValue* value) {
if (value == nullptr || value->asObject() == nullptr) {
return std::nullopt;
}
const auto& json = *value->asObject();
GatewayModbusConfig config;
config.transport = getObjectString(json, "transport").value_or("tcp-server");
config.host = getObjectString(json, "host").value_or("");
config.port = static_cast<uint16_t>(
getObjectInt(json, "port").value_or(kGatewayModbusDefaultTcpPort));
config.unit_id = static_cast<uint8_t>(getObjectInt(json, "unitID").value_or(
getObjectInt(json, "unitId").value_or(getObjectInt(json, "unit_id").value_or(1))));
return config;
}
DaliValue GatewayModbusConfigToValue(const GatewayModbusConfig& config) {
DaliValue::Object out;
out["transport"] = config.transport;
out["host"] = config.host;
out["port"] = static_cast<int>(config.port);
out["unitID"] = static_cast<int>(config.unit_id);
return DaliValue(std::move(out));
}
const char* GatewayModbusSpaceToString(GatewayModbusSpace space) {
switch (space) {
case GatewayModbusSpace::kCoil:
return "coil";
case GatewayModbusSpace::kDiscreteInput:
return "discrete_input";
case GatewayModbusSpace::kHoldingRegister:
return "holding_register";
case GatewayModbusSpace::kInputRegister:
return "input_register";
}
return "unknown";
}
const char* GatewayModbusAccessToString(GatewayModbusAccess access) {
switch (access) {
case GatewayModbusAccess::kReadOnly:
return "read_only";
case GatewayModbusAccess::kWriteOnly:
return "write_only";
case GatewayModbusAccess::kReadWrite:
return "read_write";
}
return "unknown";
}
const char* GatewayModbusGeneratedKindToString(GatewayModbusGeneratedKind kind) {
switch (kind) {
case GatewayModbusGeneratedKind::kShortOn:
return "short_on";
case GatewayModbusGeneratedKind::kShortOff:
return "short_off";
case GatewayModbusGeneratedKind::kShortRecallMax:
return "short_recall_max";
case GatewayModbusGeneratedKind::kShortRecallMin:
return "short_recall_min";
case GatewayModbusGeneratedKind::kShortDiscovered:
return "short_discovered";
case GatewayModbusGeneratedKind::kShortOnline:
return "short_online";
case GatewayModbusGeneratedKind::kShortSupportsDt1:
return "short_supports_dt1";
case GatewayModbusGeneratedKind::kShortSupportsDt4:
return "short_supports_dt4";
case GatewayModbusGeneratedKind::kShortSupportsDt5:
return "short_supports_dt5";
case GatewayModbusGeneratedKind::kShortSupportsDt6:
return "short_supports_dt6";
case GatewayModbusGeneratedKind::kShortSupportsDt8:
return "short_supports_dt8";
case GatewayModbusGeneratedKind::kShortGroupMaskKnown:
return "short_group_mask_known";
case GatewayModbusGeneratedKind::kShortActualLevelKnown:
return "short_actual_level_known";
case GatewayModbusGeneratedKind::kShortSceneKnown:
return "short_scene_known";
case GatewayModbusGeneratedKind::kShortSettingsKnown:
return "short_settings_known";
case GatewayModbusGeneratedKind::kShortControlGearPresent:
return "short_control_gear_present";
case GatewayModbusGeneratedKind::kShortLampFailure:
return "short_lamp_failure";
case GatewayModbusGeneratedKind::kShortLampPowerOn:
return "short_lamp_power_on";
case GatewayModbusGeneratedKind::kShortLimitError:
return "short_limit_error";
case GatewayModbusGeneratedKind::kShortFadingCompleted:
return "short_fading_completed";
case GatewayModbusGeneratedKind::kShortResetState:
return "short_reset_state";
case GatewayModbusGeneratedKind::kShortMissingShortAddress:
return "short_missing_short_address";
case GatewayModbusGeneratedKind::kShortPowerSupplyFault:
return "short_power_supply_fault";
case GatewayModbusGeneratedKind::kShortBrightness:
return "short_brightness";
case GatewayModbusGeneratedKind::kShortColorTemperature:
return "short_color_temperature";
case GatewayModbusGeneratedKind::kShortGroupMask:
return "short_group_mask";
case GatewayModbusGeneratedKind::kShortPowerOnLevel:
return "short_power_on_level";
case GatewayModbusGeneratedKind::kShortSystemFailureLevel:
return "short_system_failure_level";
case GatewayModbusGeneratedKind::kShortMinLevel:
return "short_min_level";
case GatewayModbusGeneratedKind::kShortMaxLevel:
return "short_max_level";
case GatewayModbusGeneratedKind::kShortFadeTime:
return "short_fade_time";
case GatewayModbusGeneratedKind::kShortFadeRate:
return "short_fade_rate";
case GatewayModbusGeneratedKind::kShortInventoryState:
return "short_inventory_state";
case GatewayModbusGeneratedKind::kShortPrimaryType:
return "short_primary_type";
case GatewayModbusGeneratedKind::kShortTypeMask:
return "short_type_mask";
case GatewayModbusGeneratedKind::kShortActualLevel:
return "short_actual_level";
case GatewayModbusGeneratedKind::kShortSceneId:
return "short_scene_id";
case GatewayModbusGeneratedKind::kShortRawStatus:
return "short_raw_status";
case GatewayModbusGeneratedKind::kNone:
default:
return "none";
}
}
int GatewayModbusHumanAddressFromWire(GatewayModbusSpace space, uint16_t zero_based_address) {
return baseForSpace(space) + static_cast<int>(zero_based_address);
}
std::optional<GatewayModbusSpace> GatewayModbusReadSpaceForFunction(uint8_t function_code) {
switch (function_code) {
case 0x01:
return GatewayModbusSpace::kCoil;
case 0x02:
return GatewayModbusSpace::kDiscreteInput;
case 0x03:
return GatewayModbusSpace::kHoldingRegister;
case 0x04:
return GatewayModbusSpace::kInputRegister;
default:
return std::nullopt;
}
}
std::optional<GatewayModbusSpace> GatewayModbusWriteSpaceForFunction(uint8_t function_code) {
switch (function_code) {
case 0x05:
case 0x0F:
return GatewayModbusSpace::kCoil;
case 0x06:
case 0x10:
return GatewayModbusSpace::kHoldingRegister;
default:
return std::nullopt;
}
}
GatewayModbusBridge::GatewayModbusBridge(DaliBridgeEngine& engine) : engine_(engine) {
rebuildMap();
}
void GatewayModbusBridge::setConfig(const GatewayModbusConfig& config) { config_ = config; }
const GatewayModbusConfig& GatewayModbusBridge::config() const { return config_; }
void GatewayModbusBridge::rebuildMap() {
std::map<PointKey, GatewayModbusPoint> next;
for (uint8_t short_address = 0; short_address < kShortAddressCount; ++short_address) {
for (const auto& spec : kGeneratedCoils) {
addGeneratedPoint(&next, short_address, spec);
}
for (const auto& spec : kGeneratedDiscreteInputs) {
addGeneratedPoint(&next, short_address, spec);
}
for (const auto& spec : kGeneratedHoldingRegisters) {
addGeneratedPoint(&next, short_address, spec);
}
for (const auto& spec : kGeneratedInputRegisters) {
addGeneratedPoint(&next, short_address, spec);
}
}
for (const auto& model : engine_.listModels()) {
if (model.protocol != BridgeProtocolKind::modbus || !model.external.registerAddress.has_value()) {
continue;
}
const auto space = spaceForObjectType(model.external.objectType);
if (!space.has_value()) {
continue;
}
GatewayModbusPoint point;
point.space = space.value();
point.access = accessForSpace(space.value());
point.address = static_cast<uint16_t>(model.external.registerAddress.value());
point.id = model.id;
point.name = model.displayName();
point.generated = false;
point.generated_kind = GatewayModbusGeneratedKind::kNone;
point.model_id = model.id;
point.operation = model.operation;
point.bit_index = model.external.bitIndex;
if (model.dali.kind == BridgeDaliTargetKind::shortAddress && model.dali.shortAddress.has_value()) {
point.short_address = model.dali.shortAddress.value();
}
next[PointKey{point.space, point.address}] = std::move(point);
}
points_.clear();
points_.reserve(next.size());
for (auto& entry : next) {
points_.push_back(std::move(entry.second));
}
}
std::optional<GatewayModbusPoint> GatewayModbusBridge::findPoint(GatewayModbusSpace space,
uint16_t address) const {
const auto found = std::find_if(points_.begin(), points_.end(), [space, address](const auto& point) {
return point.space == space && point.address == address;
});
if (found == points_.end()) {
return std::nullopt;
}
return *found;
}
std::vector<GatewayModbusPointBinding> GatewayModbusBridge::describePoints() const {
std::vector<GatewayModbusPointBinding> bindings;
bindings.reserve(points_.size());
for (const auto& point : points_) {
bindings.push_back(toBinding(point));
}
return bindings;
}
std::vector<GatewayModbusPointBinding> GatewayModbusBridge::describeHoldingRegisters() const {
std::vector<GatewayModbusPointBinding> bindings;
for (const auto& point : points_) {
if (point.space == GatewayModbusSpace::kHoldingRegister) {
bindings.push_back(toBinding(point));
}
}
return bindings;
}
DaliBridgeResult GatewayModbusBridge::readModelPoint(const GatewayModbusPoint& point) const {
return executeModelPoint(point, std::nullopt);
}
DaliBridgeResult GatewayModbusBridge::writeRegisterPoint(const GatewayModbusPoint& point,
uint16_t value) const {
return executeModelPoint(point, static_cast<int>(value));
}
DaliBridgeResult GatewayModbusBridge::writeCoilPoint(const GatewayModbusPoint& point,
bool value) const {
return executeModelPoint(point, value ? 1 : 0);
}
DaliBridgeResult GatewayModbusBridge::executeModelPoint(const GatewayModbusPoint& point,
std::optional<int> value) const {
DaliBridgeRequest request;
request.sequence = "modbus-" + std::to_string(point.address);
request.modelID = point.model_id;
if (value.has_value()) {
request.value = value.value();
}
if (point.model_id.empty()) {
DaliBridgeResult result;
result.sequence = request.sequence;
result.error = "generated Modbus point requires gateway handler";
return result;
}
return engine_.execute(request);
}
} // namespace gateway