Refactor DALI component: remove Modbus bridge support, update documentation, and enhance bridge model handling
This commit is contained in:
@@ -19,7 +19,6 @@ idf_component_register(
|
|||||||
"src/color.cpp"
|
"src/color.cpp"
|
||||||
"src/gateway_cloud.cpp"
|
"src/gateway_cloud.cpp"
|
||||||
"src/gateway_provisioning.cpp"
|
"src/gateway_provisioning.cpp"
|
||||||
"src/modbus_bridge.cpp"
|
|
||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
REQUIRES mqtt cjson nvs_flash
|
REQUIRES mqtt cjson nvs_flash
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,29 +50,28 @@ The component now includes a protocol-agnostic bridge layer for mapping external
|
|||||||
|
|
||||||
- `bridge_model.hpp` defines the strongly typed mapping model: protocol kind, external point, DALI target, default operation, and value transform.
|
- `bridge_model.hpp` defines the strongly typed mapping model: protocol kind, external point, DALI target, default operation, and value transform.
|
||||||
- `bridge.hpp` provides `DaliBridgeEngine`, which resolves models and dispatches requests into `DaliComm`, `DaliBase`, and `DaliDT8`.
|
- `bridge.hpp` provides `DaliBridgeEngine`, which resolves models and dispatches requests into `DaliComm`, `DaliBase`, and `DaliDT8`.
|
||||||
- `bridge_provisioning.hpp` provides `BridgeProvisioningStore` for persisting bridge models and protocol config in ESP-IDF NVS.
|
- `bridge_provisioning.hpp` provides `BridgeProvisioningStore` for persisting bridge models and shared protocol config in ESP-IDF NVS.
|
||||||
- `modbus_bridge.hpp` provides a Modbus skeleton adapter keyed by holding register bindings.
|
- Modbus runtime support is owned by the native gateway project in `gateway/components/gateway_modbus`.
|
||||||
- `bacnet_bridge.hpp` provides a BACnet skeleton adapter keyed by object type, instance, and property bindings.
|
- `bacnet_bridge.hpp` provides a BACnet skeleton adapter keyed by object type, instance, and property bindings.
|
||||||
|
|
||||||
### Example Model Mapping
|
### Example Model Mapping
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
BridgeModel modbusBrightness;
|
BridgeModel brightness;
|
||||||
modbusBrightness.id = "modbus-light-1";
|
brightness.id = "line-1-brightness";
|
||||||
modbusBrightness.name = "Line 1 brightness";
|
brightness.name = "Line 1 brightness";
|
||||||
modbusBrightness.protocol = BridgeProtocolKind::modbus;
|
brightness.dali.shortAddress = 1;
|
||||||
modbusBrightness.external.objectType = BridgeObjectType::holdingRegister;
|
brightness.operation = BridgeOperation::setBrightness;
|
||||||
modbusBrightness.external.registerAddress = 40001;
|
brightness.valueTransform.clampMin = 0;
|
||||||
modbusBrightness.dali.shortAddress = 1;
|
brightness.valueTransform.clampMax = 254;
|
||||||
modbusBrightness.operation = BridgeOperation::setBrightness;
|
|
||||||
modbusBrightness.valueTransform.clampMin = 0;
|
|
||||||
modbusBrightness.valueTransform.clampMax = 254;
|
|
||||||
|
|
||||||
DaliBridgeEngine engine(comm);
|
DaliBridgeEngine engine(comm);
|
||||||
engine.upsertModel(modbusBrightness);
|
engine.upsertModel(brightness);
|
||||||
|
|
||||||
DaliModbusBridge modbus(engine);
|
DaliBridgeRequest request;
|
||||||
modbus.handleHoldingRegisterWrite(40001, 180);
|
request.modelID = "line-1-brightness";
|
||||||
|
request.value = 180;
|
||||||
|
engine.execute(request);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Supported Bridge Operations
|
### Supported Bridge Operations
|
||||||
@@ -108,16 +107,11 @@ Query-style operations return `data` when available and may include decoded flag
|
|||||||
|
|
||||||
## Bridge Provisioning via NVS
|
## Bridge Provisioning via NVS
|
||||||
|
|
||||||
Use `BridgeProvisioningStore` to persist bridge models and protocol-specific config for Modbus and BACnet:
|
Use `BridgeProvisioningStore` to persist bridge models and shared protocol-specific config such as BACnet:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
BridgeRuntimeConfig runtime;
|
BridgeRuntimeConfig runtime;
|
||||||
runtime.models.push_back(modbusBrightness);
|
runtime.models.push_back(brightness);
|
||||||
|
|
||||||
ModbusBridgeConfig modbus;
|
|
||||||
modbus.transport = "tcp-server";
|
|
||||||
modbus.port = 1502;
|
|
||||||
runtime.modbus = modbus;
|
|
||||||
|
|
||||||
BridgeProvisioningStore store;
|
BridgeProvisioningStore store;
|
||||||
store.save(runtime);
|
store.save(runtime);
|
||||||
@@ -128,6 +122,8 @@ if (store.load(&loaded) == ESP_OK) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The gateway project stores Modbus TCP settings in the same persisted JSON shape, but parses and applies that section in `gateway/components/gateway_modbus` rather than in this standalone DALI component.
|
||||||
|
|
||||||
## Cloud Bridge (ESP32 Gateway)
|
## Cloud Bridge (ESP32 Gateway)
|
||||||
|
|
||||||
The component now includes `DaliCloudBridge` in `include/gateway_cloud.hpp` to connect ESP32 gateways to the backend MQTT broker.
|
The component now includes `DaliCloudBridge` in `include/gateway_cloud.hpp` to connect ESP32 gateways to the backend MQTT broker.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# ESP32-S3 Bridge Example
|
# ESP32-S3 Bridge Example
|
||||||
|
|
||||||
This ESP-IDF example wires `dali_cpp` into a standalone application and demonstrates how to register strongly typed Modbus and BACnet bridge models.
|
This ESP-IDF example wires `dali_cpp` into a standalone application and demonstrates how to register strongly typed bridge and BACnet models. Modbus runtime support now lives in the native gateway project.
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
@@ -21,13 +21,9 @@ idf.py build
|
|||||||
|
|
||||||
- `DaliBridgeEngine` resolving model bindings.
|
- `DaliBridgeEngine` resolving model bindings.
|
||||||
- `BridgeProvisioningStore` loading and saving model/config state in NVS.
|
- `BridgeProvisioningStore` loading and saving model/config state in NVS.
|
||||||
- `DaliModbusBridge` mapping Modbus TCP holding-register writes to DALI operations.
|
|
||||||
- `DaliBacnetBridge` mapping BACnet property writes to DALI brightness percentage updates.
|
- `DaliBacnetBridge` mapping BACnet property writes to DALI brightness percentage updates.
|
||||||
- A simple Modbus TCP listener supporting write-single-register (`0x06`) and write-multiple-registers (`0x10`).
|
|
||||||
- Placeholder DALI gateway callbacks where you can connect your UART transport.
|
- Placeholder DALI gateway callbacks where you can connect your UART transport.
|
||||||
|
|
||||||
## Modbus Mapping Notes
|
## Modbus Notes
|
||||||
|
|
||||||
- The example listens on port `1502` by default.
|
The gateway implementation in `gateway/components/gateway_modbus` owns Modbus TCP, generated DALI point tables, and provisioned Modbus overrides. Use `gateway/apps/gateway` to exercise Modbus behavior.
|
||||||
- Holding register `0` maps to bridge register `40001`.
|
|
||||||
- The default config is stored to NVS on first boot and reused on later boots.
|
|
||||||
@@ -3,32 +3,17 @@
|
|||||||
#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 "modbus_bridge.hpp"
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
|
||||||
#include "freertos/task.h"
|
|
||||||
#include "lwip/inet.h"
|
|
||||||
#include "lwip/sockets.h"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr const char* kTag = "dali_bridge_example";
|
constexpr const char* kTag = "dali_bridge_example";
|
||||||
constexpr int kModbusTaskStack = 6144;
|
|
||||||
constexpr int kDefaultModbusListenPort = 1502;
|
|
||||||
|
|
||||||
struct ExampleContext {
|
|
||||||
DaliModbusBridge* modbus = nullptr;
|
|
||||||
uint16_t listenPort = kDefaultModbusListenPort;
|
|
||||||
};
|
|
||||||
|
|
||||||
ExampleContext gExampleContext;
|
|
||||||
|
|
||||||
bool writeGateway(const uint8_t* data, size_t len) {
|
bool writeGateway(const uint8_t* data, size_t len) {
|
||||||
ESP_LOGI(kTag, "placeholder DALI gateway write len=%u first=0x%02X",
|
ESP_LOGI(kTag, "placeholder DALI gateway write len=%u first=0x%02X",
|
||||||
@@ -37,9 +22,9 @@ bool writeGateway(const uint8_t* data, size_t len) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> readGateway(size_t len, uint32_t timeoutMs) {
|
std::vector<uint8_t> readGateway(size_t len, uint32_t timeout_ms) {
|
||||||
ESP_LOGI(kTag, "placeholder DALI gateway read len=%u timeout=%u",
|
ESP_LOGI(kTag, "placeholder DALI gateway read len=%u timeout=%u",
|
||||||
static_cast<unsigned>(len), static_cast<unsigned>(timeoutMs));
|
static_cast<unsigned>(len), static_cast<unsigned>(timeout_ms));
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,65 +35,10 @@ std::vector<uint8_t> transactGateway(const uint8_t* data, size_t len) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t readBe16(const uint8_t* data) {
|
BridgeModel makeBrightnessModel() {
|
||||||
return static_cast<uint16_t>((static_cast<uint16_t>(data[0]) << 8) | data[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
void writeBe16(uint8_t* data, uint16_t value) {
|
|
||||||
data[0] = static_cast<uint8_t>((value >> 8) & 0xFF);
|
|
||||||
data[1] = static_cast<uint8_t>(value & 0xFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool recvAll(int sock, uint8_t* buffer, size_t len) {
|
|
||||||
size_t received = 0;
|
|
||||||
while (received < len) {
|
|
||||||
const int ret = recv(sock, buffer + received, len - received, 0);
|
|
||||||
if (ret <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
received += static_cast<size_t>(ret);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool sendAll(int sock, const uint8_t* buffer, size_t len) {
|
|
||||||
size_t sent = 0;
|
|
||||||
while (sent < len) {
|
|
||||||
const int ret = send(sock, buffer + sent, len - sent, 0);
|
|
||||||
if (ret <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
sent += static_cast<size_t>(ret);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int normalizeHoldingRegister(uint16_t zeroBasedAddress) {
|
|
||||||
return 40001 + static_cast<int>(zeroBasedAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool sendModbusFrame(int sock, const uint8_t* mbap, const std::vector<uint8_t>& pdu) {
|
|
||||||
std::vector<uint8_t> frame(7 + pdu.size());
|
|
||||||
std::memcpy(frame.data(), mbap, 7);
|
|
||||||
writeBe16(&frame[4], static_cast<uint16_t>(pdu.size() + 1));
|
|
||||||
std::memcpy(frame.data() + 7, pdu.data(), pdu.size());
|
|
||||||
return sendAll(sock, frame.data(), frame.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool sendModbusException(int sock, const uint8_t* mbap, uint8_t functionCode, uint8_t exceptionCode) {
|
|
||||||
const std::vector<uint8_t> pdu{static_cast<uint8_t>(functionCode | 0x80), exceptionCode};
|
|
||||||
return sendModbusFrame(sock, mbap, pdu);
|
|
||||||
}
|
|
||||||
|
|
||||||
BridgeModel makeModbusBrightnessModel() {
|
|
||||||
BridgeModel model;
|
BridgeModel model;
|
||||||
model.id = "modbus-light-1";
|
model.id = "line-1-brightness";
|
||||||
model.name = "Modbus line 1 brightness";
|
model.name = "Line 1 brightness";
|
||||||
model.protocol = BridgeProtocolKind::modbus;
|
|
||||||
model.external.network = "line-a";
|
|
||||||
model.external.device = "plc-1";
|
|
||||||
model.external.objectType = BridgeObjectType::holdingRegister;
|
|
||||||
model.external.registerAddress = 40001;
|
|
||||||
model.dali.shortAddress = 1;
|
model.dali.shortAddress = 1;
|
||||||
model.operation = BridgeOperation::setBrightness;
|
model.operation = BridgeOperation::setBrightness;
|
||||||
model.valueEncoding = BridgeValueEncoding::integer;
|
model.valueEncoding = BridgeValueEncoding::integer;
|
||||||
@@ -117,6 +47,15 @@ BridgeModel makeModbusBrightnessModel() {
|
|||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BridgeModel makeStatusQueryModel() {
|
||||||
|
BridgeModel model;
|
||||||
|
model.id = "line-1-status";
|
||||||
|
model.name = "Line 1 status";
|
||||||
|
model.dali.shortAddress = 1;
|
||||||
|
model.operation = BridgeOperation::getStatus;
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
BridgeModel makeBacnetBrightnessModel() {
|
BridgeModel makeBacnetBrightnessModel() {
|
||||||
BridgeModel model;
|
BridgeModel model;
|
||||||
model.id = "bacnet-zone-2";
|
model.id = "bacnet-zone-2";
|
||||||
@@ -133,31 +72,11 @@ BridgeModel makeBacnetBrightnessModel() {
|
|||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
BridgeModel makeStatusQueryModel() {
|
|
||||||
BridgeModel model;
|
|
||||||
model.id = "modbus-light-1-status";
|
|
||||||
model.name = "Modbus line 1 status";
|
|
||||||
model.protocol = BridgeProtocolKind::modbus;
|
|
||||||
model.external.network = "line-a";
|
|
||||||
model.external.device = "plc-1";
|
|
||||||
model.external.objectType = BridgeObjectType::holdingRegister;
|
|
||||||
model.external.registerAddress = 40002;
|
|
||||||
model.dali.shortAddress = 1;
|
|
||||||
model.operation = BridgeOperation::getStatus;
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
BridgeRuntimeConfig makeDefaultRuntimeConfig() {
|
BridgeRuntimeConfig makeDefaultRuntimeConfig() {
|
||||||
BridgeRuntimeConfig config;
|
BridgeRuntimeConfig config;
|
||||||
config.models.push_back(makeModbusBrightnessModel());
|
config.models.push_back(makeBrightnessModel());
|
||||||
config.models.push_back(makeBacnetBrightnessModel());
|
|
||||||
config.models.push_back(makeStatusQueryModel());
|
config.models.push_back(makeStatusQueryModel());
|
||||||
|
config.models.push_back(makeBacnetBrightnessModel());
|
||||||
ModbusBridgeConfig modbus;
|
|
||||||
modbus.transport = "tcp-server";
|
|
||||||
modbus.port = kDefaultModbusListenPort;
|
|
||||||
modbus.unitID = 7;
|
|
||||||
config.modbus = modbus;
|
|
||||||
|
|
||||||
BacnetBridgeConfig bacnet;
|
BacnetBridgeConfig bacnet;
|
||||||
bacnet.deviceInstance = 1001;
|
bacnet.deviceInstance = 1001;
|
||||||
@@ -174,151 +93,31 @@ void logResult(const char* label, const DaliBridgeResult& result) {
|
|||||||
result.error.empty() ? "<none>" : result.error.c_str());
|
result.error.empty() ? "<none>" : result.error.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleModbusClient(int clientSock, const ExampleContext& context) {
|
|
||||||
uint8_t header[7];
|
|
||||||
while (recvAll(clientSock, header, sizeof(header))) {
|
|
||||||
const uint16_t protocolID = readBe16(&header[2]);
|
|
||||||
const uint16_t length = readBe16(&header[4]);
|
|
||||||
if (protocolID != 0 || length < 2) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<uint8_t> pdu(length - 1);
|
|
||||||
if (!recvAll(clientSock, pdu.data(), pdu.size()) || pdu.empty()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint8_t functionCode = pdu[0];
|
|
||||||
if (functionCode == 0x06 && pdu.size() == 5) {
|
|
||||||
const uint16_t registerAddress = readBe16(&pdu[1]);
|
|
||||||
const uint16_t value = readBe16(&pdu[3]);
|
|
||||||
const int holdingRegister = normalizeHoldingRegister(registerAddress);
|
|
||||||
const DaliBridgeResult result = context.modbus->handleHoldingRegisterWrite(holdingRegister, value);
|
|
||||||
logResult("modbus tcp write single", result);
|
|
||||||
if (!result.ok) {
|
|
||||||
sendModbusException(clientSock, header, functionCode, 0x04);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
sendModbusFrame(clientSock, header, pdu);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (functionCode == 0x10 && pdu.size() >= 6) {
|
|
||||||
const uint16_t startAddress = readBe16(&pdu[1]);
|
|
||||||
const uint16_t quantity = readBe16(&pdu[3]);
|
|
||||||
const uint8_t byteCount = pdu[5];
|
|
||||||
if (pdu.size() != static_cast<size_t>(6 + byteCount) || byteCount != quantity * 2) {
|
|
||||||
sendModbusException(clientSock, header, functionCode, 0x03);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ok = true;
|
|
||||||
for (uint16_t index = 0; index < quantity; ++index) {
|
|
||||||
const size_t offset = 6 + (index * 2);
|
|
||||||
const uint16_t value = readBe16(&pdu[offset]);
|
|
||||||
const int holdingRegister = normalizeHoldingRegister(static_cast<uint16_t>(startAddress + index));
|
|
||||||
const DaliBridgeResult result = context.modbus->handleHoldingRegisterWrite(holdingRegister, value);
|
|
||||||
logResult("modbus tcp write multiple", result);
|
|
||||||
if (!result.ok) {
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ok) {
|
|
||||||
sendModbusException(clientSock, header, functionCode, 0x04);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<uint8_t> response(5);
|
|
||||||
response[0] = functionCode;
|
|
||||||
writeBe16(&response[1], startAddress);
|
|
||||||
writeBe16(&response[3], quantity);
|
|
||||||
sendModbusFrame(clientSock, header, response);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendModbusException(clientSock, header, functionCode, 0x01);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void modbusServerTask(void* arg) {
|
|
||||||
auto* context = static_cast<ExampleContext*>(arg);
|
|
||||||
const int listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
|
||||||
if (listenSock < 0) {
|
|
||||||
ESP_LOGE(kTag, "failed to create Modbus listen socket");
|
|
||||||
vTaskDelete(nullptr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int reuse = 1;
|
|
||||||
setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
|
|
||||||
|
|
||||||
sockaddr_in address = {};
|
|
||||||
address.sin_family = AF_INET;
|
|
||||||
address.sin_addr.s_addr = htonl(INADDR_ANY);
|
|
||||||
address.sin_port = htons(context->listenPort);
|
|
||||||
|
|
||||||
if (bind(listenSock, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0 ||
|
|
||||||
listen(listenSock, 2) != 0) {
|
|
||||||
ESP_LOGE(kTag, "failed to bind/listen Modbus TCP on port %u", context->listenPort);
|
|
||||||
close(listenSock);
|
|
||||||
vTaskDelete(nullptr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI(kTag, "Modbus TCP listener ready on port %u", context->listenPort);
|
|
||||||
while (true) {
|
|
||||||
sockaddr_in clientAddress = {};
|
|
||||||
socklen_t clientLen = sizeof(clientAddress);
|
|
||||||
const int clientSock = accept(listenSock, reinterpret_cast<sockaddr*>(&clientAddress), &clientLen);
|
|
||||||
if (clientSock < 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ESP_LOGI(kTag, "Modbus client connected");
|
|
||||||
handleModbusClient(clientSock, *context);
|
|
||||||
close(clientSock);
|
|
||||||
ESP_LOGI(kTag, "Modbus client disconnected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
extern "C" void app_main(void) {
|
extern "C" void app_main(void) {
|
||||||
esp_err_t nvsErr = nvs_flash_init();
|
esp_err_t nvs_err = nvs_flash_init();
|
||||||
if (nvsErr != ESP_OK) {
|
if (nvs_err != ESP_OK) {
|
||||||
ESP_LOGW(kTag, "nvs_flash_init failed: %s", esp_err_to_name(nvsErr));
|
ESP_LOGW(kTag, "nvs_flash_init failed: %s", esp_err_to_name(nvs_err));
|
||||||
}
|
}
|
||||||
|
|
||||||
BridgeProvisioningStore provisioningStore;
|
BridgeProvisioningStore provisioning_store;
|
||||||
BridgeRuntimeConfig runtimeConfig;
|
BridgeRuntimeConfig runtime_config;
|
||||||
if (provisioningStore.load(&runtimeConfig) != ESP_OK || runtimeConfig.models.empty()) {
|
if (provisioning_store.load(&runtime_config) != ESP_OK || runtime_config.models.empty()) {
|
||||||
runtimeConfig = makeDefaultRuntimeConfig();
|
runtime_config = makeDefaultRuntimeConfig();
|
||||||
provisioningStore.save(runtimeConfig);
|
provisioning_store.save(runtime_config);
|
||||||
}
|
}
|
||||||
|
|
||||||
static DaliComm comm(writeGateway, readGateway, transactGateway);
|
static DaliComm comm(writeGateway, readGateway, transactGateway);
|
||||||
static DaliBridgeEngine engine(comm);
|
static DaliBridgeEngine engine(comm);
|
||||||
|
|
||||||
for (const auto& model : runtimeConfig.models) {
|
for (const auto& model : runtime_config.models) {
|
||||||
engine.upsertModel(model);
|
engine.upsertModel(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
static DaliModbusBridge modbus(engine);
|
|
||||||
ModbusBridgeConfig modbusConfig = runtimeConfig.modbus.value_or(ModbusBridgeConfig{});
|
|
||||||
modbus.setConfig(modbusConfig);
|
|
||||||
|
|
||||||
static DaliBacnetBridge bacnet(engine);
|
static DaliBacnetBridge bacnet(engine);
|
||||||
BacnetBridgeConfig bacnetConfig = runtimeConfig.bacnet.value_or(BacnetBridgeConfig{});
|
BacnetBridgeConfig bacnet_config = runtime_config.bacnet.value_or(BacnetBridgeConfig{});
|
||||||
bacnet.setConfig(bacnetConfig);
|
bacnet.setConfig(bacnet_config);
|
||||||
|
|
||||||
gExampleContext.modbus = &modbus;
|
|
||||||
gExampleContext.listenPort = modbus.config().port == 0 ? kDefaultModbusListenPort : modbus.config().port;
|
|
||||||
|
|
||||||
for (const auto& binding : modbus.describeHoldingRegisters()) {
|
|
||||||
ESP_LOGI(kTag, "modbus binding model=%s register=%d", binding.modelID.c_str(),
|
|
||||||
binding.registerAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& binding : bacnet.describeObjects()) {
|
for (const auto& binding : bacnet.describeObjects()) {
|
||||||
ESP_LOGI(kTag, "bacnet binding model=%s object=%s:%d property=%s",
|
ESP_LOGI(kTag, "bacnet binding model=%s object=%s:%d property=%s",
|
||||||
@@ -326,18 +125,18 @@ extern "C" void app_main(void) {
|
|||||||
binding.objectInstance, binding.property.c_str());
|
binding.objectInstance, binding.property.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
const DaliBridgeResult modbusResult = modbus.handleHoldingRegisterWrite(40001, 180);
|
DaliBridgeRequest brightness_request;
|
||||||
logResult("modbus write", modbusResult);
|
brightness_request.sequence = "startup-brightness";
|
||||||
|
brightness_request.modelID = "line-1-brightness";
|
||||||
|
brightness_request.value = 180;
|
||||||
|
logResult("bridge brightness", engine.execute(brightness_request));
|
||||||
|
|
||||||
const DaliBridgeResult bacnetResult =
|
const DaliBridgeResult bacnet_result =
|
||||||
bacnet.handlePropertyWrite(BridgeObjectType::analogOutput, 2, "presentValue", 75.0);
|
bacnet.handlePropertyWrite(BridgeObjectType::analogOutput, 2, "presentValue", 75.0);
|
||||||
logResult("bacnet write", bacnetResult);
|
logResult("bacnet write", bacnet_result);
|
||||||
|
|
||||||
DaliBridgeRequest statusRequest;
|
DaliBridgeRequest status_request;
|
||||||
statusRequest.sequence = "startup-status";
|
status_request.sequence = "startup-status";
|
||||||
statusRequest.modelID = "modbus-light-1-status";
|
status_request.modelID = "line-1-status";
|
||||||
const DaliBridgeResult statusResult = engine.execute(statusRequest);
|
logResult("status query", engine.execute(status_request));
|
||||||
logResult("status query", statusResult);
|
}
|
||||||
|
|
||||||
xTaskCreate(modbusServerTask, "modbus_tcp", kModbusTaskStack, &gExampleContext, 5, nullptr);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,11 +18,15 @@ enum class BridgeObjectType {
|
|||||||
inputRegister = 2,
|
inputRegister = 2,
|
||||||
coil = 3,
|
coil = 3,
|
||||||
discreteInput = 4,
|
discreteInput = 4,
|
||||||
|
analogInput = 10,
|
||||||
analogValue = 5,
|
analogValue = 5,
|
||||||
analogOutput = 6,
|
analogOutput = 6,
|
||||||
|
binaryInput = 11,
|
||||||
binaryValue = 7,
|
binaryValue = 7,
|
||||||
binaryOutput = 8,
|
binaryOutput = 8,
|
||||||
|
multiStateInput = 12,
|
||||||
multiStateValue = 9,
|
multiStateValue = 9,
|
||||||
|
multiStateOutput = 13,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class BridgeDaliTargetKind {
|
enum class BridgeDaliTargetKind {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include "bacnet_bridge.hpp"
|
#include "bacnet_bridge.hpp"
|
||||||
#include "bridge_model.hpp"
|
#include "bridge_model.hpp"
|
||||||
#include "modbus_bridge.hpp"
|
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -18,7 +17,6 @@ using esp_err_t = int;
|
|||||||
|
|
||||||
struct BridgeRuntimeConfig {
|
struct BridgeRuntimeConfig {
|
||||||
std::vector<BridgeModel> models;
|
std::vector<BridgeModel> models;
|
||||||
std::optional<ModbusBridgeConfig> modbus;
|
|
||||||
std::optional<BacnetBridgeConfig> bacnet;
|
std::optional<BacnetBridgeConfig> bacnet;
|
||||||
DaliValue::Object metadata;
|
DaliValue::Object metadata;
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
#include "color.hpp"
|
#include "color.hpp"
|
||||||
#include "errors.hpp"
|
#include "errors.hpp"
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
#include "modbus_bridge.hpp"
|
|
||||||
#include "query_scheduler.hpp"
|
#include "query_scheduler.hpp"
|
||||||
#include "sequence.hpp"
|
#include "sequence.hpp"
|
||||||
#include "sequence_store.hpp"
|
#include "sequence_store.hpp"
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "bridge.hpp"
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
struct ModbusBridgeConfig {
|
|
||||||
std::string transport = "tcp";
|
|
||||||
std::string host;
|
|
||||||
uint16_t port = 502;
|
|
||||||
uint8_t unitID = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ModbusRegisterBinding {
|
|
||||||
std::string modelID;
|
|
||||||
int registerAddress = -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
class DaliModbusBridge {
|
|
||||||
public:
|
|
||||||
explicit DaliModbusBridge(DaliBridgeEngine& engine);
|
|
||||||
|
|
||||||
void setConfig(const ModbusBridgeConfig& config);
|
|
||||||
const ModbusBridgeConfig& config() const;
|
|
||||||
|
|
||||||
DaliBridgeResult handleHoldingRegisterWrite(int registerAddress, int value) const;
|
|
||||||
std::optional<ModbusRegisterBinding> findHoldingRegister(int registerAddress) const;
|
|
||||||
std::vector<ModbusRegisterBinding> describeHoldingRegisters() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
DaliBridgeEngine& engine_;
|
|
||||||
ModbusBridgeConfig config_;
|
|
||||||
};
|
|
||||||
@@ -245,16 +245,24 @@ const char* bridgeObjectTypeToString(BridgeObjectType type) {
|
|||||||
return "coil";
|
return "coil";
|
||||||
case BridgeObjectType::discreteInput:
|
case BridgeObjectType::discreteInput:
|
||||||
return "discrete_input";
|
return "discrete_input";
|
||||||
|
case BridgeObjectType::analogInput:
|
||||||
|
return "analog_input";
|
||||||
case BridgeObjectType::analogValue:
|
case BridgeObjectType::analogValue:
|
||||||
return "analog_value";
|
return "analog_value";
|
||||||
case BridgeObjectType::analogOutput:
|
case BridgeObjectType::analogOutput:
|
||||||
return "analog_output";
|
return "analog_output";
|
||||||
|
case BridgeObjectType::binaryInput:
|
||||||
|
return "binary_input";
|
||||||
case BridgeObjectType::binaryValue:
|
case BridgeObjectType::binaryValue:
|
||||||
return "binary_value";
|
return "binary_value";
|
||||||
case BridgeObjectType::binaryOutput:
|
case BridgeObjectType::binaryOutput:
|
||||||
return "binary_output";
|
return "binary_output";
|
||||||
|
case BridgeObjectType::multiStateInput:
|
||||||
|
return "multi_state_input";
|
||||||
case BridgeObjectType::multiStateValue:
|
case BridgeObjectType::multiStateValue:
|
||||||
return "multi_state_value";
|
return "multi_state_value";
|
||||||
|
case BridgeObjectType::multiStateOutput:
|
||||||
|
return "multi_state_output";
|
||||||
case BridgeObjectType::unknown:
|
case BridgeObjectType::unknown:
|
||||||
default:
|
default:
|
||||||
return "unknown";
|
return "unknown";
|
||||||
@@ -267,11 +275,15 @@ BridgeObjectType bridgeObjectTypeFromString(const std::string& value) {
|
|||||||
if (normalized == "input_register") return BridgeObjectType::inputRegister;
|
if (normalized == "input_register") return BridgeObjectType::inputRegister;
|
||||||
if (normalized == "coil") return BridgeObjectType::coil;
|
if (normalized == "coil") return BridgeObjectType::coil;
|
||||||
if (normalized == "discrete_input") return BridgeObjectType::discreteInput;
|
if (normalized == "discrete_input") return BridgeObjectType::discreteInput;
|
||||||
|
if (normalized == "analog_input") return BridgeObjectType::analogInput;
|
||||||
if (normalized == "analog_value") return BridgeObjectType::analogValue;
|
if (normalized == "analog_value") return BridgeObjectType::analogValue;
|
||||||
if (normalized == "analog_output") return BridgeObjectType::analogOutput;
|
if (normalized == "analog_output") return BridgeObjectType::analogOutput;
|
||||||
|
if (normalized == "binary_input") return BridgeObjectType::binaryInput;
|
||||||
if (normalized == "binary_value") return BridgeObjectType::binaryValue;
|
if (normalized == "binary_value") return BridgeObjectType::binaryValue;
|
||||||
if (normalized == "binary_output") return BridgeObjectType::binaryOutput;
|
if (normalized == "binary_output") return BridgeObjectType::binaryOutput;
|
||||||
|
if (normalized == "multi_state_input") return BridgeObjectType::multiStateInput;
|
||||||
if (normalized == "multi_state_value") return BridgeObjectType::multiStateValue;
|
if (normalized == "multi_state_value") return BridgeObjectType::multiStateValue;
|
||||||
|
if (normalized == "multi_state_output") return BridgeObjectType::multiStateOutput;
|
||||||
return BridgeObjectType::unknown;
|
return BridgeObjectType::unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,28 +101,6 @@ esp_err_t readString(nvs_handle_t handle, const char* key, std::string* value) {
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<ModbusBridgeConfig> modbusFromJson(const DaliValue* value) {
|
|
||||||
if (value == nullptr || value->asObject() == nullptr) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
const auto& json = *value->asObject();
|
|
||||||
ModbusBridgeConfig config;
|
|
||||||
config.transport = getObjectString(json, "transport").value_or("tcp");
|
|
||||||
config.host = getObjectString(json, "host").value_or("");
|
|
||||||
config.port = static_cast<uint16_t>(getObjectInt(json, "port").value_or(502));
|
|
||||||
config.unitID = static_cast<uint8_t>(getObjectInt(json, "unitID").value_or(1));
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
DaliValue modbusToJson(const ModbusBridgeConfig& 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.unitID);
|
|
||||||
return DaliValue(std::move(out));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<BacnetBridgeConfig> bacnetFromJson(const DaliValue* value) {
|
std::optional<BacnetBridgeConfig> bacnetFromJson(const DaliValue* value) {
|
||||||
if (value == nullptr || value->asObject() == nullptr) {
|
if (value == nullptr || value->asObject() == nullptr) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -249,7 +227,6 @@ BridgeRuntimeConfig BridgeRuntimeConfig::fromJson(const DaliValue::Object& json)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.modbus = modbusFromJson(getObjectValue(json, "modbus"));
|
|
||||||
config.bacnet = bacnetFromJson(getObjectValue(json, "bacnet"));
|
config.bacnet = bacnetFromJson(getObjectValue(json, "bacnet"));
|
||||||
if (const auto* metadata = getObjectValue(json, "meta")) {
|
if (const auto* metadata = getObjectValue(json, "meta")) {
|
||||||
if (const auto* object = metadata->asObject()) {
|
if (const auto* object = metadata->asObject()) {
|
||||||
@@ -267,7 +244,6 @@ DaliValue::Object BridgeRuntimeConfig::toJson() const {
|
|||||||
modelsValue.emplace_back(model.toJson());
|
modelsValue.emplace_back(model.toJson());
|
||||||
}
|
}
|
||||||
out["models"] = std::move(modelsValue);
|
out["models"] = std::move(modelsValue);
|
||||||
if (modbus.has_value()) out["modbus"] = modbusToJson(modbus.value());
|
|
||||||
if (bacnet.has_value()) out["bacnet"] = bacnetToJson(bacnet.value());
|
if (bacnet.has_value()) out["bacnet"] = bacnetToJson(bacnet.value());
|
||||||
if (!metadata.empty()) out["meta"] = metadata;
|
if (!metadata.empty()) out["meta"] = metadata;
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
#include "modbus_bridge.hpp"
|
|
||||||
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
DaliModbusBridge::DaliModbusBridge(DaliBridgeEngine& engine) : engine_(engine) {}
|
|
||||||
|
|
||||||
void DaliModbusBridge::setConfig(const ModbusBridgeConfig& config) { config_ = config; }
|
|
||||||
|
|
||||||
const ModbusBridgeConfig& DaliModbusBridge::config() const { return config_; }
|
|
||||||
|
|
||||||
DaliBridgeResult DaliModbusBridge::handleHoldingRegisterWrite(int registerAddress, int value) const {
|
|
||||||
const auto binding = findHoldingRegister(registerAddress);
|
|
||||||
DaliBridgeRequest request;
|
|
||||||
request.sequence = "modbus-" + std::to_string(registerAddress);
|
|
||||||
request.value = value;
|
|
||||||
|
|
||||||
if (!binding.has_value()) {
|
|
||||||
DaliBridgeResult result;
|
|
||||||
result.sequence = request.sequence;
|
|
||||||
result.error = "unmapped holding register";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
request.modelID = binding->modelID;
|
|
||||||
return engine_.execute(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<ModbusRegisterBinding> DaliModbusBridge::findHoldingRegister(int registerAddress) const {
|
|
||||||
for (const auto& model : engine_.listModels()) {
|
|
||||||
if (model.protocol != BridgeProtocolKind::modbus) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (model.external.objectType != BridgeObjectType::holdingRegister) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (model.external.registerAddress.value_or(-1) != registerAddress) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return ModbusRegisterBinding{model.id, registerAddress};
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<ModbusRegisterBinding> DaliModbusBridge::describeHoldingRegisters() const {
|
|
||||||
std::vector<ModbusRegisterBinding> bindings;
|
|
||||||
for (const auto& model : engine_.listModels()) {
|
|
||||||
if (model.protocol != BridgeProtocolKind::modbus ||
|
|
||||||
model.external.objectType != BridgeObjectType::holdingRegister ||
|
|
||||||
!model.external.registerAddress.has_value()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
bindings.push_back(ModbusRegisterBinding{model.id, model.external.registerAddress.value()});
|
|
||||||
}
|
|
||||||
return bindings;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user