Implement DALI Bridge Engine and Model Management
- Added `bridge.cpp` to handle DALI bridge operations including model management, command execution, and response formatting. - Introduced `bridge_model.cpp` for defining bridge models, value transformations, and JSON serialization/deserialization. - Created `bridge_provisioning.cpp` for managing bridge configuration storage and retrieval using NVS on ESP platform. - Enhanced `gateway_cloud.cpp` to integrate DALI bridge requests and responses with cloud communication. - Introduced `modbus_bridge.cpp` to handle Modbus-specific operations and register management. - Implemented utility functions for converting between DaliValue and cJSON formats. - Added error handling and metadata management in bridge responses.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
**/build/
|
||||
**/managed_components/
|
||||
**/dependencies.lock
|
||||
**/sdkconfig
|
||||
**/sdkconfig.old
|
||||
@@ -3,6 +3,10 @@ idf_component_register(
|
||||
"src/dali_comm.cpp"
|
||||
"src/base.cpp"
|
||||
"src/addr.cpp"
|
||||
"src/bridge.cpp"
|
||||
"src/bridge_model.cpp"
|
||||
"src/bridge_provisioning.cpp"
|
||||
"src/bacnet_bridge.cpp"
|
||||
"src/decode.cpp"
|
||||
"src/device.cpp"
|
||||
"src/dt1.cpp"
|
||||
@@ -12,6 +16,7 @@ idf_component_register(
|
||||
"src/color.cpp"
|
||||
"src/gateway_cloud.cpp"
|
||||
"src/gateway_provisioning.cpp"
|
||||
"src/modbus_bridge.cpp"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES mqtt cjson nvs_flash
|
||||
)
|
||||
|
||||
@@ -42,11 +42,91 @@ std::vector<int> rgb = dali.dt8.getColourRGB(5);
|
||||
- App-side model parity modules are included for `device.dart`, `sequence.dart`, and `sequence_store.dart` via `device.hpp`, `sequence.hpp`, and `sequence_store.hpp`.
|
||||
- Utility APIs from `errors.dart`, `log.dart`, `query_scheduler.dart`, and `bus_monitor.dart` are available as embedded-friendly C++ headers.
|
||||
|
||||
## Generic Bridge Layer
|
||||
|
||||
The component now includes a protocol-agnostic bridge layer for mapping external fieldbus models into DALI operations.
|
||||
|
||||
### Bridge Building Blocks
|
||||
|
||||
- `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_provisioning.hpp` provides `BridgeProvisioningStore` for persisting bridge models and protocol config in ESP-IDF NVS.
|
||||
- `modbus_bridge.hpp` provides a Modbus skeleton adapter keyed by holding register bindings.
|
||||
- `bacnet_bridge.hpp` provides a BACnet skeleton adapter keyed by object type, instance, and property bindings.
|
||||
|
||||
### Example Model Mapping
|
||||
|
||||
```cpp
|
||||
BridgeModel modbusBrightness;
|
||||
modbusBrightness.id = "modbus-light-1";
|
||||
modbusBrightness.name = "Line 1 brightness";
|
||||
modbusBrightness.protocol = BridgeProtocolKind::modbus;
|
||||
modbusBrightness.external.objectType = BridgeObjectType::holdingRegister;
|
||||
modbusBrightness.external.registerAddress = 40001;
|
||||
modbusBrightness.dali.shortAddress = 1;
|
||||
modbusBrightness.operation = BridgeOperation::setBrightness;
|
||||
modbusBrightness.valueTransform.clampMin = 0;
|
||||
modbusBrightness.valueTransform.clampMax = 254;
|
||||
|
||||
DaliBridgeEngine engine(comm);
|
||||
engine.upsertModel(modbusBrightness);
|
||||
|
||||
DaliModbusBridge modbus(engine);
|
||||
modbus.handleHoldingRegisterWrite(40001, 180);
|
||||
```
|
||||
|
||||
### Supported Bridge Operations
|
||||
|
||||
- `send`
|
||||
- `send_ext`
|
||||
- `query`
|
||||
- `set_brightness`
|
||||
- `set_brightness_percent`
|
||||
- `on`
|
||||
- `off`
|
||||
- `recall_max_level`
|
||||
- `recall_min_level`
|
||||
- `set_color_temperature`
|
||||
- `get_brightness`
|
||||
- `get_status`
|
||||
- `get_color_temperature`
|
||||
- `get_color_status`
|
||||
- `get_emergency_level`
|
||||
- `get_emergency_status`
|
||||
- `get_emergency_failure_status`
|
||||
- `start_emergency_function_test`
|
||||
- `stop_emergency_test`
|
||||
|
||||
Query-style operations return `data` when available and may include decoded flags in `meta`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Optional query support: provide a `transact` callback that returns the gateway reply; otherwise, query methods return `std::nullopt`.
|
||||
- `Dali` facade in `include/dali.hpp` mirrors `lib/dali/dali.dart` and wires `base`, `decode`, `dt1`, `dt8`, and `addr` together.
|
||||
- The `t`, `d`, and `g` parameters in Dart are not required here; timing/gateway selection is driven by your callbacks.
|
||||
- `DaliCloudBridge` now uses the shared bridge engine internally, so MQTT downlinks can target either raw DALI frames or registered bridge models.
|
||||
|
||||
## Bridge Provisioning via NVS
|
||||
|
||||
Use `BridgeProvisioningStore` to persist bridge models and protocol-specific config for Modbus and BACnet:
|
||||
|
||||
```cpp
|
||||
BridgeRuntimeConfig runtime;
|
||||
runtime.models.push_back(modbusBrightness);
|
||||
|
||||
ModbusBridgeConfig modbus;
|
||||
modbus.transport = "tcp-server";
|
||||
modbus.port = 1502;
|
||||
runtime.modbus = modbus;
|
||||
|
||||
BridgeProvisioningStore store;
|
||||
store.save(runtime);
|
||||
|
||||
BridgeRuntimeConfig loaded;
|
||||
if (store.load(&loaded) == ESP_OK) {
|
||||
// register loaded.models with DaliBridgeEngine
|
||||
}
|
||||
```
|
||||
|
||||
## Cloud Bridge (ESP32 Gateway)
|
||||
|
||||
@@ -65,12 +145,19 @@ The component now includes `DaliCloudBridge` in `include/gateway_cloud.hpp` to c
|
||||
{
|
||||
"type": "dali_cmd",
|
||||
"seq": "123",
|
||||
"model": "modbus-light-1",
|
||||
"op": "send|send_ext|query",
|
||||
"addr": 5,
|
||||
"cmd": 160
|
||||
"cmd": 160,
|
||||
"shortAddress": 1,
|
||||
"value": 180
|
||||
}
|
||||
```
|
||||
|
||||
`model`, `shortAddress`, and `value` are optional. If `model` is provided, the bridge resolves its mapped DALI target and default operation.
|
||||
|
||||
Successful query responses may also include a `meta` object with decoded status flags.
|
||||
|
||||
### Uplink JSON Envelope
|
||||
|
||||
```json
|
||||
@@ -118,3 +205,27 @@ if (store.load(&loaded) == ESP_OK) {
|
||||
bridge.start(loaded);
|
||||
}
|
||||
```
|
||||
|
||||
## ESP32-S3 Example Project
|
||||
|
||||
A standalone ESP-IDF example app is available at `examples/esp32s3_bridge/`.
|
||||
|
||||
### Environment Setup
|
||||
|
||||
Source the helper script from your shell so the exported ESP-IDF variables stay in your session:
|
||||
|
||||
```bash
|
||||
. ./scripts/export_esp_idf.sh
|
||||
```
|
||||
|
||||
The helper script sources `~/esp/v5.5.2/esp-idf/export.sh`.
|
||||
|
||||
### Build the Example
|
||||
|
||||
```bash
|
||||
cd examples/esp32s3_bridge
|
||||
idf.py set-target esp32s3
|
||||
idf.py build
|
||||
```
|
||||
|
||||
The example persists its bridge config in NVS, starts a real Modbus TCP listener, registers Modbus and BACnet bridge models, and routes Modbus writes through the shared bridge engine. The DALI gateway callbacks are still placeholders where you should connect your UART or transport driver.
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
if(NOT IDF_TARGET)
|
||||
set(IDF_TARGET "esp32s3" CACHE STRING "ESP-IDF target")
|
||||
endif()
|
||||
|
||||
set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../..")
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(dali_cpp_esp32s3_bridge)
|
||||
@@ -0,0 +1,33 @@
|
||||
# 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.
|
||||
|
||||
## Environment
|
||||
|
||||
```bash
|
||||
cd /Users/tonylu/StudioProjects/dalimaster/dali_cpp
|
||||
. ./scripts/export_esp_idf.sh
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd /Users/tonylu/StudioProjects/dalimaster/dali_cpp/examples/esp32s3_bridge
|
||||
idf.py set-target esp32s3
|
||||
idf.py build
|
||||
```
|
||||
|
||||
## What It Shows
|
||||
|
||||
- `DaliBridgeEngine` resolving model bindings.
|
||||
- `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.
|
||||
- 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.
|
||||
|
||||
## Modbus Mapping Notes
|
||||
|
||||
- The example listens on port `1502` by default.
|
||||
- Holding register `0` maps to bridge register `40001`.
|
||||
- The default config is stored to NVS on first boot and reused on later boots.
|
||||
@@ -0,0 +1,2 @@
|
||||
idf_component_register(SRCS "main.cpp"
|
||||
INCLUDE_DIRS ".")
|
||||
@@ -0,0 +1,343 @@
|
||||
#include "bacnet_bridge.hpp"
|
||||
#include "bridge.hpp"
|
||||
#include "bridge_model.hpp"
|
||||
#include "bridge_provisioning.hpp"
|
||||
#include "dali_comm.hpp"
|
||||
#include "modbus_bridge.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "lwip/inet.h"
|
||||
#include "lwip/sockets.h"
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
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) {
|
||||
ESP_LOGI(kTag, "placeholder DALI gateway write len=%u first=0x%02X",
|
||||
static_cast<unsigned>(len), len > 0 ? data[0] : 0U);
|
||||
(void)data;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> readGateway(size_t len, uint32_t timeoutMs) {
|
||||
ESP_LOGI(kTag, "placeholder DALI gateway read len=%u timeout=%u",
|
||||
static_cast<unsigned>(len), static_cast<unsigned>(timeoutMs));
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> transactGateway(const uint8_t* data, size_t len) {
|
||||
ESP_LOGI(kTag, "placeholder DALI gateway transact len=%u first=0x%02X",
|
||||
static_cast<unsigned>(len), len > 0 ? data[0] : 0U);
|
||||
(void)data;
|
||||
return {};
|
||||
}
|
||||
|
||||
uint16_t readBe16(const uint8_t* data) {
|
||||
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;
|
||||
model.id = "modbus-light-1";
|
||||
model.name = "Modbus 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.operation = BridgeOperation::setBrightness;
|
||||
model.valueEncoding = BridgeValueEncoding::integer;
|
||||
model.valueTransform.clampMin = 0;
|
||||
model.valueTransform.clampMax = 254;
|
||||
return model;
|
||||
}
|
||||
|
||||
BridgeModel makeBacnetBrightnessModel() {
|
||||
BridgeModel model;
|
||||
model.id = "bacnet-zone-2";
|
||||
model.name = "BACnet zone 2 level";
|
||||
model.protocol = BridgeProtocolKind::bacnet;
|
||||
model.external.network = "floor-2";
|
||||
model.external.device = "bacnet-controller";
|
||||
model.external.objectType = BridgeObjectType::analogOutput;
|
||||
model.external.objectInstance = 2;
|
||||
model.external.property = "presentValue";
|
||||
model.dali.shortAddress = 2;
|
||||
model.operation = BridgeOperation::setBrightnessPercent;
|
||||
model.valueEncoding = BridgeValueEncoding::percentage;
|
||||
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 config;
|
||||
config.models.push_back(makeModbusBrightnessModel());
|
||||
config.models.push_back(makeBacnetBrightnessModel());
|
||||
config.models.push_back(makeStatusQueryModel());
|
||||
|
||||
ModbusBridgeConfig modbus;
|
||||
modbus.transport = "tcp-server";
|
||||
modbus.port = kDefaultModbusListenPort;
|
||||
modbus.unitID = 7;
|
||||
config.modbus = modbus;
|
||||
|
||||
BacnetBridgeConfig bacnet;
|
||||
bacnet.deviceInstance = 1001;
|
||||
bacnet.localAddress = "192.168.10.20";
|
||||
config.bacnet = bacnet;
|
||||
|
||||
config.metadata["example"] = "esp32s3_bridge";
|
||||
return config;
|
||||
}
|
||||
|
||||
void logResult(const char* label, const DaliBridgeResult& result) {
|
||||
ESP_LOGI(kTag, "%s ok=%d op=%s data=%d error=%s", label, result.ok,
|
||||
bridgeOperationToString(result.operation), result.data.value_or(-1),
|
||||
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
|
||||
|
||||
extern "C" void app_main(void) {
|
||||
esp_err_t nvsErr = nvs_flash_init();
|
||||
if (nvsErr != ESP_OK) {
|
||||
ESP_LOGW(kTag, "nvs_flash_init failed: %s", esp_err_to_name(nvsErr));
|
||||
}
|
||||
|
||||
BridgeProvisioningStore provisioningStore;
|
||||
BridgeRuntimeConfig runtimeConfig;
|
||||
if (provisioningStore.load(&runtimeConfig) != ESP_OK || runtimeConfig.models.empty()) {
|
||||
runtimeConfig = makeDefaultRuntimeConfig();
|
||||
provisioningStore.save(runtimeConfig);
|
||||
}
|
||||
|
||||
static DaliComm comm(writeGateway, readGateway, transactGateway);
|
||||
static DaliBridgeEngine engine(comm);
|
||||
|
||||
for (const auto& model : runtimeConfig.models) {
|
||||
engine.upsertModel(model);
|
||||
}
|
||||
|
||||
static DaliModbusBridge modbus(engine);
|
||||
ModbusBridgeConfig modbusConfig = runtimeConfig.modbus.value_or(ModbusBridgeConfig{});
|
||||
modbus.setConfig(modbusConfig);
|
||||
|
||||
static DaliBacnetBridge bacnet(engine);
|
||||
BacnetBridgeConfig bacnetConfig = runtimeConfig.bacnet.value_or(BacnetBridgeConfig{});
|
||||
bacnet.setConfig(bacnetConfig);
|
||||
|
||||
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()) {
|
||||
ESP_LOGI(kTag, "bacnet binding model=%s object=%s:%d property=%s",
|
||||
binding.modelID.c_str(), bridgeObjectTypeToString(binding.objectType),
|
||||
binding.objectInstance, binding.property.c_str());
|
||||
}
|
||||
|
||||
const DaliBridgeResult modbusResult = modbus.handleHoldingRegisterWrite(40001, 180);
|
||||
logResult("modbus write", modbusResult);
|
||||
|
||||
const DaliBridgeResult bacnetResult =
|
||||
bacnet.handlePropertyWrite(BridgeObjectType::analogOutput, 2, "presentValue", 75.0);
|
||||
logResult("bacnet write", bacnetResult);
|
||||
|
||||
DaliBridgeRequest statusRequest;
|
||||
statusRequest.sequence = "startup-status";
|
||||
statusRequest.modelID = "modbus-light-1-status";
|
||||
const DaliBridgeResult statusResult = engine.execute(statusRequest);
|
||||
logResult("status query", statusResult);
|
||||
|
||||
xTaskCreate(modbusServerTask, "modbus_tcp", kModbusTaskStack, &gExampleContext, 5, nullptr);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "bridge.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct BacnetBridgeConfig {
|
||||
uint32_t deviceInstance = 4194303;
|
||||
std::string localAddress;
|
||||
uint16_t udpPort = 47808;
|
||||
};
|
||||
|
||||
struct BacnetObjectBinding {
|
||||
std::string modelID;
|
||||
BridgeObjectType objectType = BridgeObjectType::unknown;
|
||||
int objectInstance = -1;
|
||||
std::string property;
|
||||
};
|
||||
|
||||
class DaliBacnetBridge {
|
||||
public:
|
||||
explicit DaliBacnetBridge(DaliBridgeEngine& engine);
|
||||
|
||||
void setConfig(const BacnetBridgeConfig& config);
|
||||
const BacnetBridgeConfig& config() const;
|
||||
|
||||
DaliBridgeResult handlePropertyWrite(BridgeObjectType objectType,
|
||||
int objectInstance,
|
||||
const std::string& property,
|
||||
const DaliValue& value) const;
|
||||
std::optional<BacnetObjectBinding> findObject(BridgeObjectType objectType,
|
||||
int objectInstance,
|
||||
const std::string& property) const;
|
||||
std::vector<BacnetObjectBinding> describeObjects() const;
|
||||
|
||||
private:
|
||||
DaliBridgeEngine& engine_;
|
||||
BacnetBridgeConfig config_;
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include "bridge_model.hpp"
|
||||
#include "dali_comm.hpp"
|
||||
#include "dt1.hpp"
|
||||
#include "dt8.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct DaliBridgeRequest {
|
||||
std::string sequence;
|
||||
std::string modelID;
|
||||
std::optional<BridgeOperation> operation;
|
||||
std::optional<int> rawAddress;
|
||||
std::optional<int> rawCommand;
|
||||
std::optional<int> shortAddress;
|
||||
DaliValue value;
|
||||
DaliValue::Object metadata;
|
||||
};
|
||||
|
||||
struct DaliBridgeResult {
|
||||
std::string sequence;
|
||||
std::string modelID;
|
||||
BridgeOperation operation = BridgeOperation::unknown;
|
||||
bool ok = false;
|
||||
std::optional<int> data;
|
||||
std::string error;
|
||||
DaliValue::Object metadata;
|
||||
|
||||
DaliValue::Object toJson() const;
|
||||
};
|
||||
|
||||
class DaliBridgeEngine {
|
||||
public:
|
||||
explicit DaliBridgeEngine(DaliComm& comm);
|
||||
|
||||
bool upsertModel(const BridgeModel& model);
|
||||
bool removeModel(const std::string& modelID);
|
||||
const BridgeModel* findModel(const std::string& modelID) const;
|
||||
std::vector<BridgeModel> listModels() const;
|
||||
|
||||
DaliBridgeResult execute(const DaliBridgeRequest& request);
|
||||
|
||||
private:
|
||||
DaliComm& comm_;
|
||||
DaliBase base_;
|
||||
DaliDT1 dt1_;
|
||||
DaliDT8 dt8_;
|
||||
std::map<std::string, BridgeModel> models_;
|
||||
|
||||
DaliBridgeResult executeResolved(const DaliBridgeRequest& request,
|
||||
const BridgeModel* model,
|
||||
BridgeOperation operation);
|
||||
std::optional<int> resolveShortAddress(const DaliBridgeRequest& request,
|
||||
const BridgeModel* model) const;
|
||||
std::optional<int> resolveRawAddress(const DaliBridgeRequest& request,
|
||||
const BridgeModel* model) const;
|
||||
std::optional<int> resolveRawCommand(const DaliBridgeRequest& request,
|
||||
const BridgeModel* model) const;
|
||||
std::optional<int> resolveIntValue(const DaliBridgeRequest& request, const BridgeModel* model) const;
|
||||
std::optional<double> resolveDoubleValue(const DaliBridgeRequest& request) const;
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
#include "model_value.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
enum class BridgeProtocolKind {
|
||||
unknown = 0,
|
||||
mqtt = 1,
|
||||
modbus = 2,
|
||||
bacnet = 3,
|
||||
};
|
||||
|
||||
enum class BridgeObjectType {
|
||||
unknown = 0,
|
||||
holdingRegister = 1,
|
||||
inputRegister = 2,
|
||||
coil = 3,
|
||||
discreteInput = 4,
|
||||
analogValue = 5,
|
||||
analogOutput = 6,
|
||||
binaryValue = 7,
|
||||
binaryOutput = 8,
|
||||
multiStateValue = 9,
|
||||
};
|
||||
|
||||
enum class BridgeOperation {
|
||||
unknown = 0,
|
||||
send = 1,
|
||||
sendExt = 2,
|
||||
query = 3,
|
||||
setBrightness = 4,
|
||||
setBrightnessPercent = 5,
|
||||
on = 6,
|
||||
off = 7,
|
||||
recallMaxLevel = 8,
|
||||
recallMinLevel = 9,
|
||||
setColorTemperature = 10,
|
||||
getBrightness = 11,
|
||||
getStatus = 12,
|
||||
getColorTemperature = 13,
|
||||
getColorStatus = 14,
|
||||
getEmergencyLevel = 15,
|
||||
getEmergencyStatus = 16,
|
||||
getEmergencyFailureStatus = 17,
|
||||
startEmergencyFunctionTest = 18,
|
||||
stopEmergencyTest = 19,
|
||||
};
|
||||
|
||||
enum class BridgeValueEncoding {
|
||||
none = 0,
|
||||
integer = 1,
|
||||
percentage = 2,
|
||||
kelvin = 3,
|
||||
};
|
||||
|
||||
struct BridgeValueTransform {
|
||||
double scale = 1.0;
|
||||
double offset = 0.0;
|
||||
std::optional<int> clampMin;
|
||||
std::optional<int> clampMax;
|
||||
|
||||
static BridgeValueTransform fromJson(const DaliValue::Object* json);
|
||||
DaliValue::Object toJson() const;
|
||||
int apply(double raw) const;
|
||||
};
|
||||
|
||||
struct BridgeExternalPoint {
|
||||
std::string network;
|
||||
std::string device;
|
||||
BridgeObjectType objectType = BridgeObjectType::unknown;
|
||||
std::optional<int> objectInstance;
|
||||
std::optional<int> registerAddress;
|
||||
std::optional<int> bitIndex;
|
||||
std::string property;
|
||||
|
||||
static BridgeExternalPoint fromJson(const DaliValue::Object* json);
|
||||
DaliValue::Object toJson() const;
|
||||
};
|
||||
|
||||
struct BridgeDaliTarget {
|
||||
std::optional<int> shortAddress;
|
||||
std::optional<int> rawAddress;
|
||||
std::optional<int> rawCommand;
|
||||
|
||||
static BridgeDaliTarget fromJson(const DaliValue::Object* json);
|
||||
DaliValue::Object toJson() const;
|
||||
};
|
||||
|
||||
struct BridgeModel {
|
||||
std::string id;
|
||||
std::string name;
|
||||
BridgeProtocolKind protocol = BridgeProtocolKind::unknown;
|
||||
BridgeExternalPoint external;
|
||||
BridgeDaliTarget dali;
|
||||
BridgeOperation operation = BridgeOperation::unknown;
|
||||
BridgeValueEncoding valueEncoding = BridgeValueEncoding::integer;
|
||||
BridgeValueTransform valueTransform;
|
||||
DaliValue::Object metadata;
|
||||
|
||||
static BridgeModel fromJson(const DaliValue::Object& json);
|
||||
DaliValue::Object toJson() const;
|
||||
std::string displayName() const;
|
||||
};
|
||||
|
||||
const char* bridgeProtocolKindToString(BridgeProtocolKind kind);
|
||||
BridgeProtocolKind bridgeProtocolKindFromString(const std::string& value);
|
||||
|
||||
const char* bridgeObjectTypeToString(BridgeObjectType type);
|
||||
BridgeObjectType bridgeObjectTypeFromString(const std::string& value);
|
||||
|
||||
const char* bridgeOperationToString(BridgeOperation operation);
|
||||
BridgeOperation bridgeOperationFromString(const std::string& value);
|
||||
|
||||
const char* bridgeValueEncodingToString(BridgeValueEncoding encoding);
|
||||
BridgeValueEncoding bridgeValueEncodingFromString(const std::string& value);
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "bacnet_bridge.hpp"
|
||||
#include "bridge_model.hpp"
|
||||
#include "modbus_bridge.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
extern "C" {
|
||||
#include "esp_err.h"
|
||||
}
|
||||
#else
|
||||
using esp_err_t = int;
|
||||
#endif
|
||||
|
||||
struct BridgeRuntimeConfig {
|
||||
std::vector<BridgeModel> models;
|
||||
std::optional<ModbusBridgeConfig> modbus;
|
||||
std::optional<BacnetBridgeConfig> bacnet;
|
||||
DaliValue::Object metadata;
|
||||
|
||||
static BridgeRuntimeConfig fromJson(const DaliValue::Object& json);
|
||||
DaliValue::Object toJson() const;
|
||||
};
|
||||
|
||||
class BridgeProvisioningStore {
|
||||
public:
|
||||
explicit BridgeProvisioningStore(std::string nvsNamespace = "dali_bridge")
|
||||
: nvsNamespace_(std::move(nvsNamespace)) {}
|
||||
|
||||
esp_err_t save(const BridgeRuntimeConfig& config) const;
|
||||
esp_err_t load(BridgeRuntimeConfig* config) const;
|
||||
esp_err_t clear() const;
|
||||
|
||||
private:
|
||||
std::string nvsNamespace_;
|
||||
};
|
||||
@@ -1,6 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include "bacnet_bridge.hpp"
|
||||
#include "bridge.hpp"
|
||||
#include "bridge_model.hpp"
|
||||
#include "bridge_provisioning.hpp"
|
||||
#include "bus_monitor.hpp"
|
||||
#include "comm.hpp"
|
||||
#include "dali_comm.hpp"
|
||||
@@ -14,6 +18,7 @@
|
||||
#include "color.hpp"
|
||||
#include "errors.hpp"
|
||||
#include "log.hpp"
|
||||
#include "modbus_bridge.hpp"
|
||||
#include "query_scheduler.hpp"
|
||||
#include "sequence.hpp"
|
||||
#include "sequence_store.hpp"
|
||||
|
||||
@@ -11,6 +11,7 @@ class ColorStatus {
|
||||
public:
|
||||
explicit ColorStatus(int status) : status_(status) {}
|
||||
|
||||
int raw() const { return status_; }
|
||||
bool xyOutOfRange() const { return (status_ & 0x01) != 0; }
|
||||
bool ctOutOfRange() const { return (status_ & 0x02) != 0; }
|
||||
bool autoCalibrationActive() const { return (status_ & 0x04) != 0; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "bridge.hpp"
|
||||
#include "dali_comm.hpp"
|
||||
|
||||
#include <atomic>
|
||||
@@ -27,6 +28,8 @@ class DaliCloudBridge {
|
||||
bool start(const GatewayCloudConfig& config);
|
||||
void stop();
|
||||
bool isConnected() const;
|
||||
DaliBridgeEngine& bridge() { return bridge_; }
|
||||
const DaliBridgeEngine& bridge() const { return bridge_; }
|
||||
|
||||
bool publishStatus(const std::string& status);
|
||||
bool publishRegister(const std::string& payloadJson);
|
||||
@@ -49,6 +52,7 @@ class DaliCloudBridge {
|
||||
std::string topicRegister() const;
|
||||
|
||||
DaliComm& comm_;
|
||||
DaliBridgeEngine bridge_;
|
||||
GatewayCloudConfig config_;
|
||||
std::atomic<bool> connected_{false};
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#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_;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
ESP_IDF_EXPORT_PATH="${HOME}/esp/v5.5.2/esp-idf/export.sh"
|
||||
|
||||
if [ ! -f "$ESP_IDF_EXPORT_PATH" ]; then
|
||||
echo "ESP-IDF export script not found at $ESP_IDF_EXPORT_PATH" >&2
|
||||
return 1 2>/dev/null || exit 1
|
||||
fi
|
||||
|
||||
. "$ESP_IDF_EXPORT_PATH"
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "bacnet_bridge.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
DaliBacnetBridge::DaliBacnetBridge(DaliBridgeEngine& engine) : engine_(engine) {}
|
||||
|
||||
void DaliBacnetBridge::setConfig(const BacnetBridgeConfig& config) { config_ = config; }
|
||||
|
||||
const BacnetBridgeConfig& DaliBacnetBridge::config() const { return config_; }
|
||||
|
||||
DaliBridgeResult DaliBacnetBridge::handlePropertyWrite(BridgeObjectType objectType,
|
||||
int objectInstance,
|
||||
const std::string& property,
|
||||
const DaliValue& value) const {
|
||||
const auto binding = findObject(objectType, objectInstance, property);
|
||||
DaliBridgeRequest request;
|
||||
request.sequence = "bacnet-" + std::to_string(objectInstance);
|
||||
request.value = value;
|
||||
|
||||
if (!binding.has_value()) {
|
||||
DaliBridgeResult result;
|
||||
result.sequence = request.sequence;
|
||||
result.error = "unmapped bacnet object";
|
||||
return result;
|
||||
}
|
||||
|
||||
request.modelID = binding->modelID;
|
||||
return engine_.execute(request);
|
||||
}
|
||||
|
||||
std::optional<BacnetObjectBinding> DaliBacnetBridge::findObject(BridgeObjectType objectType,
|
||||
int objectInstance,
|
||||
const std::string& property) const {
|
||||
for (const auto& model : engine_.listModels()) {
|
||||
if (model.protocol != BridgeProtocolKind::bacnet) {
|
||||
continue;
|
||||
}
|
||||
if (model.external.objectType != objectType) {
|
||||
continue;
|
||||
}
|
||||
if (model.external.objectInstance.value_or(-1) != objectInstance) {
|
||||
continue;
|
||||
}
|
||||
if (!model.external.property.empty() && model.external.property != property) {
|
||||
continue;
|
||||
}
|
||||
return BacnetObjectBinding{model.id, objectType, objectInstance, property};
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<BacnetObjectBinding> DaliBacnetBridge::describeObjects() const {
|
||||
std::vector<BacnetObjectBinding> bindings;
|
||||
for (const auto& model : engine_.listModels()) {
|
||||
if (model.protocol != BridgeProtocolKind::bacnet || !model.external.objectInstance.has_value()) {
|
||||
continue;
|
||||
}
|
||||
bindings.push_back(BacnetObjectBinding{model.id, model.external.objectType,
|
||||
model.external.objectInstance.value(),
|
||||
model.external.property});
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
#include "bridge.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
void addStatusMetadata(DaliBridgeResult* result, int rawStatus) {
|
||||
const DaliStatus status = DaliStatus::fromByte(static_cast<uint8_t>(rawStatus));
|
||||
result->metadata["controlGearPresent"] = status.controlGearPresent;
|
||||
result->metadata["lampFailure"] = status.lampFailure;
|
||||
result->metadata["lampPowerOn"] = status.lampPowerOn;
|
||||
result->metadata["limitError"] = status.limitError;
|
||||
result->metadata["fadingCompleted"] = status.fadingCompleted;
|
||||
result->metadata["resetState"] = status.resetState;
|
||||
result->metadata["missingShortAddress"] = status.missingShortAddress;
|
||||
result->metadata["psFault"] = status.psFault;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DaliValue::Object DaliBridgeResult::toJson() const {
|
||||
DaliValue::Object out;
|
||||
out["type"] = "dali_resp";
|
||||
out["seq"] = sequence;
|
||||
if (!modelID.empty()) out["model"] = modelID;
|
||||
out["op"] = bridgeOperationToString(operation);
|
||||
out["ok"] = ok;
|
||||
if (data.has_value()) out["data"] = data.value();
|
||||
if (!error.empty()) out["error"] = error;
|
||||
if (!metadata.empty()) out["meta"] = metadata;
|
||||
return out;
|
||||
}
|
||||
|
||||
DaliBridgeEngine::DaliBridgeEngine(DaliComm& comm) : comm_(comm), base_(comm), dt1_(base_), dt8_(base_) {}
|
||||
|
||||
|
||||
bool DaliBridgeEngine::upsertModel(const BridgeModel& model) {
|
||||
if (model.id.empty()) {
|
||||
return false;
|
||||
}
|
||||
models_[model.id] = model;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DaliBridgeEngine::removeModel(const std::string& modelID) {
|
||||
return models_.erase(modelID) > 0;
|
||||
}
|
||||
|
||||
const BridgeModel* DaliBridgeEngine::findModel(const std::string& modelID) const {
|
||||
const auto it = models_.find(modelID);
|
||||
if (it == models_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
std::vector<BridgeModel> DaliBridgeEngine::listModels() const {
|
||||
std::vector<BridgeModel> out;
|
||||
out.reserve(models_.size());
|
||||
for (const auto& entry : models_) {
|
||||
out.push_back(entry.second);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
DaliBridgeResult DaliBridgeEngine::execute(const DaliBridgeRequest& request) {
|
||||
const BridgeModel* model = nullptr;
|
||||
if (!request.modelID.empty()) {
|
||||
model = findModel(request.modelID);
|
||||
if (model == nullptr) {
|
||||
DaliBridgeResult result;
|
||||
result.sequence = request.sequence;
|
||||
result.modelID = request.modelID;
|
||||
result.error = "unknown model";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const BridgeOperation operation =
|
||||
request.operation.value_or(model != nullptr ? model->operation : BridgeOperation::unknown);
|
||||
if (operation == BridgeOperation::unknown) {
|
||||
DaliBridgeResult result;
|
||||
result.sequence = request.sequence;
|
||||
result.modelID = request.modelID;
|
||||
result.error = "missing operation";
|
||||
return result;
|
||||
}
|
||||
|
||||
return executeResolved(request, model, operation);
|
||||
}
|
||||
|
||||
DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& request,
|
||||
const BridgeModel* model,
|
||||
BridgeOperation operation) {
|
||||
DaliBridgeResult result;
|
||||
result.sequence = request.sequence;
|
||||
result.modelID = model != nullptr ? model->id : request.modelID;
|
||||
result.operation = operation;
|
||||
|
||||
if (model != nullptr) {
|
||||
result.metadata["protocol"] = bridgeProtocolKindToString(model->protocol);
|
||||
result.metadata["modelName"] = model->displayName();
|
||||
}
|
||||
|
||||
switch (operation) {
|
||||
case BridgeOperation::send:
|
||||
case BridgeOperation::sendExt:
|
||||
case BridgeOperation::query: {
|
||||
const auto addr = resolveRawAddress(request, model);
|
||||
const auto cmd = resolveRawCommand(request, model);
|
||||
if (!addr.has_value() || !cmd.has_value() || addr.value() < 0 || addr.value() > 255 ||
|
||||
cmd.value() < 0 || cmd.value() > 255) {
|
||||
result.error = "invalid addr/cmd";
|
||||
return result;
|
||||
}
|
||||
if (operation == BridgeOperation::send) {
|
||||
result.ok = comm_.sendRaw(static_cast<uint8_t>(addr.value()), static_cast<uint8_t>(cmd.value()));
|
||||
} else if (operation == BridgeOperation::sendExt) {
|
||||
result.ok =
|
||||
comm_.sendExtRaw(static_cast<uint8_t>(addr.value()), static_cast<uint8_t>(cmd.value()));
|
||||
} else {
|
||||
const auto response =
|
||||
comm_.queryRaw(static_cast<uint8_t>(addr.value()), static_cast<uint8_t>(cmd.value()));
|
||||
if (!response.has_value()) {
|
||||
result.error = "no response";
|
||||
return result;
|
||||
}
|
||||
result.ok = true;
|
||||
result.data = static_cast<int>(response.value());
|
||||
}
|
||||
if (!result.ok && result.error.empty()) {
|
||||
result.error = "dispatch failed";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
case BridgeOperation::setBrightness: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
const auto value = resolveIntValue(request, model);
|
||||
if (!address.has_value() || !value.has_value()) {
|
||||
result.error = "missing address/value";
|
||||
return result;
|
||||
}
|
||||
result.ok = base_.setBright(address.value(), value.value());
|
||||
result.data = value;
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::setBrightnessPercent: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
const auto value = resolveDoubleValue(request);
|
||||
if (!address.has_value() || !value.has_value()) {
|
||||
result.error = "missing address/value";
|
||||
return result;
|
||||
}
|
||||
result.ok = base_.setBrightPercentage(address.value(), value.value());
|
||||
result.data = static_cast<int>(std::lround(value.value()));
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::on: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
result.ok = base_.on(address.value());
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::off: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
result.ok = base_.off(address.value());
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::recallMaxLevel: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
result.ok = base_.recallMaxLevel(address.value());
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::recallMinLevel: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
result.ok = base_.recallMinLevel(address.value());
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::setColorTemperature: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
const auto value = resolveIntValue(request, model);
|
||||
if (!address.has_value() || !value.has_value()) {
|
||||
result.error = "missing address/value";
|
||||
return result;
|
||||
}
|
||||
result.ok = dt8_.setColorTemperature(address.value(), value.value());
|
||||
result.data = value;
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::getBrightness: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
const auto value = base_.getBright(address.value());
|
||||
if (!value.has_value()) {
|
||||
result.error = "no response";
|
||||
return result;
|
||||
}
|
||||
result.ok = true;
|
||||
result.data = value;
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::getStatus: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
const auto value = base_.getStatus(address.value());
|
||||
if (!value.has_value()) {
|
||||
result.error = "no response";
|
||||
return result;
|
||||
}
|
||||
result.ok = true;
|
||||
result.data = value;
|
||||
addStatusMetadata(&result, value.value());
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::getColorTemperature: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
const auto value = dt8_.getColorTemperature(address.value());
|
||||
if (!value.has_value()) {
|
||||
result.error = "no response";
|
||||
return result;
|
||||
}
|
||||
result.ok = true;
|
||||
result.data = value;
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::getColorStatus: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
const auto value = dt8_.getColorStatus(address.value());
|
||||
if (!value.has_value()) {
|
||||
result.error = "no response";
|
||||
return result;
|
||||
}
|
||||
result.ok = true;
|
||||
result.data = value->raw();
|
||||
result.metadata["xyOutOfRange"] = value->xyOutOfRange();
|
||||
result.metadata["ctOutOfRange"] = value->ctOutOfRange();
|
||||
result.metadata["autoCalibrationActive"] = value->autoCalibrationActive();
|
||||
result.metadata["autoCalibrationSuccess"] = value->autoCalibrationSuccess();
|
||||
result.metadata["xyActive"] = value->xyActive();
|
||||
result.metadata["ctActive"] = value->ctActive();
|
||||
result.metadata["primaryNActive"] = value->primaryNActive();
|
||||
result.metadata["rgbwafActive"] = value->rgbwafActive();
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::getEmergencyLevel: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
const auto value = dt1_.getEmergencyLevel(address.value());
|
||||
if (!value.has_value()) {
|
||||
result.error = "no response";
|
||||
return result;
|
||||
}
|
||||
result.ok = true;
|
||||
result.data = value;
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::getEmergencyStatus: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
const auto status = dt1_.getEmergencyStatusDecoded(address.value());
|
||||
if (!status.has_value()) {
|
||||
result.error = "no response";
|
||||
return result;
|
||||
}
|
||||
result.ok = true;
|
||||
result.data = status->raw();
|
||||
result.metadata["inhibitMode"] = status->inhibitMode();
|
||||
result.metadata["functionTestResultValid"] = status->functionTestResultValid();
|
||||
result.metadata["durationTestResultValid"] = status->durationTestResultValid();
|
||||
result.metadata["batteryFullyCharged"] = status->batteryFullyCharged();
|
||||
result.metadata["functionTestRequestPending"] = status->functionTestRequestPending();
|
||||
result.metadata["durationTestRequestPending"] = status->durationTestRequestPending();
|
||||
result.metadata["identificationActive"] = status->identificationActive();
|
||||
result.metadata["physicallySelected"] = status->physicallySelected();
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::getEmergencyFailureStatus: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
const auto status = dt1_.getDT1TestStatusDetailed(address.value());
|
||||
if (!status.has_value()) {
|
||||
result.error = "no response";
|
||||
return result;
|
||||
}
|
||||
result.ok = true;
|
||||
result.data = status->failureStatus;
|
||||
if (status->emergencyStatus.has_value()) result.metadata["emergencyStatus"] = status->emergencyStatus.value();
|
||||
if (status->emergencyMode.has_value()) result.metadata["emergencyMode"] = status->emergencyMode.value();
|
||||
if (status->feature.has_value()) result.metadata["feature"] = status->feature.value();
|
||||
result.metadata["testInProgress"] = status->testInProgress;
|
||||
result.metadata["lampFailure"] = status->lampFailure;
|
||||
result.metadata["batteryFailure"] = status->batteryFailure;
|
||||
result.metadata["functionTestActive"] = status->functionTestActive;
|
||||
result.metadata["durationTestActive"] = status->durationTestActive;
|
||||
result.metadata["testDone"] = status->testDone;
|
||||
result.metadata["identifyActive"] = status->identifyActive;
|
||||
result.metadata["physicalSelectionActive"] = status->physicalSelectionActive;
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::startEmergencyFunctionTest: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
result.ok = dt1_.startFunctionTestCmd(address.value());
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::stopEmergencyTest: {
|
||||
const auto address = resolveShortAddress(request, model);
|
||||
if (!address.has_value()) {
|
||||
result.error = "missing short address";
|
||||
return result;
|
||||
}
|
||||
result.ok = dt1_.stopTest(address.value());
|
||||
break;
|
||||
}
|
||||
case BridgeOperation::unknown:
|
||||
default:
|
||||
result.error = "unsupported op";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!result.ok && result.error.empty()) {
|
||||
result.error = "dispatch failed";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<int> DaliBridgeEngine::resolveShortAddress(const DaliBridgeRequest& request,
|
||||
const BridgeModel* model) const {
|
||||
if (request.shortAddress.has_value()) {
|
||||
return request.shortAddress;
|
||||
}
|
||||
if (model != nullptr) {
|
||||
return model->dali.shortAddress;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> DaliBridgeEngine::resolveRawAddress(const DaliBridgeRequest& request,
|
||||
const BridgeModel* model) const {
|
||||
if (request.rawAddress.has_value()) {
|
||||
return request.rawAddress;
|
||||
}
|
||||
if (model != nullptr) {
|
||||
return model->dali.rawAddress;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> DaliBridgeEngine::resolveRawCommand(const DaliBridgeRequest& request,
|
||||
const BridgeModel* model) const {
|
||||
if (request.rawCommand.has_value()) {
|
||||
return request.rawCommand;
|
||||
}
|
||||
if (model != nullptr) {
|
||||
return model->dali.rawCommand;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> DaliBridgeEngine::resolveIntValue(const DaliBridgeRequest& request,
|
||||
const BridgeModel* model) const {
|
||||
if (request.value.isNull()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (model != nullptr) {
|
||||
const auto number = request.value.asDouble();
|
||||
if (!number.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return model->valueTransform.apply(number.value());
|
||||
}
|
||||
return request.value.asInt();
|
||||
}
|
||||
|
||||
std::optional<double> DaliBridgeEngine::resolveDoubleValue(const DaliBridgeRequest& request) const {
|
||||
return request.value.asDouble();
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
#include "bridge_model.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string normalize(const std::string& value) {
|
||||
std::string out = value;
|
||||
std::transform(out.begin(), out.end(), out.begin(), [](unsigned char ch) {
|
||||
if (ch == '-' || ch == ' ') {
|
||||
return static_cast<char>('_');
|
||||
}
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BridgeValueTransform BridgeValueTransform::fromJson(const DaliValue::Object* json) {
|
||||
BridgeValueTransform transform;
|
||||
if (json == nullptr) {
|
||||
return transform;
|
||||
}
|
||||
if (const auto scale = getObjectValue(*json, "scale")) {
|
||||
transform.scale = scale->asDouble().value_or(1.0);
|
||||
}
|
||||
if (const auto offset = getObjectValue(*json, "offset")) {
|
||||
transform.offset = offset->asDouble().value_or(0.0);
|
||||
}
|
||||
transform.clampMin = getObjectInt(*json, "clampMin");
|
||||
transform.clampMax = getObjectInt(*json, "clampMax");
|
||||
return transform;
|
||||
}
|
||||
|
||||
DaliValue::Object BridgeValueTransform::toJson() const {
|
||||
DaliValue::Object out;
|
||||
out["scale"] = scale;
|
||||
out["offset"] = offset;
|
||||
if (clampMin.has_value()) out["clampMin"] = clampMin.value();
|
||||
if (clampMax.has_value()) out["clampMax"] = clampMax.value();
|
||||
return out;
|
||||
}
|
||||
|
||||
int BridgeValueTransform::apply(double raw) const {
|
||||
int value = static_cast<int>(std::lround((raw * scale) + offset));
|
||||
if (clampMin.has_value() && value < clampMin.value()) {
|
||||
value = clampMin.value();
|
||||
}
|
||||
if (clampMax.has_value() && value > clampMax.value()) {
|
||||
value = clampMax.value();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
BridgeExternalPoint BridgeExternalPoint::fromJson(const DaliValue::Object* json) {
|
||||
BridgeExternalPoint point;
|
||||
if (json == nullptr) {
|
||||
return point;
|
||||
}
|
||||
point.network = getObjectString(*json, "network").value_or("");
|
||||
point.device = getObjectString(*json, "device").value_or("");
|
||||
point.objectType = bridgeObjectTypeFromString(getObjectString(*json, "objectType").value_or(""));
|
||||
point.objectInstance = getObjectInt(*json, "objectInstance");
|
||||
point.registerAddress = getObjectInt(*json, "registerAddress");
|
||||
point.bitIndex = getObjectInt(*json, "bitIndex");
|
||||
point.property = getObjectString(*json, "property").value_or("");
|
||||
return point;
|
||||
}
|
||||
|
||||
DaliValue::Object BridgeExternalPoint::toJson() const {
|
||||
DaliValue::Object out;
|
||||
if (!network.empty()) out["network"] = network;
|
||||
if (!device.empty()) out["device"] = device;
|
||||
out["objectType"] = bridgeObjectTypeToString(objectType);
|
||||
if (objectInstance.has_value()) out["objectInstance"] = objectInstance.value();
|
||||
if (registerAddress.has_value()) out["registerAddress"] = registerAddress.value();
|
||||
if (bitIndex.has_value()) out["bitIndex"] = bitIndex.value();
|
||||
if (!property.empty()) out["property"] = property;
|
||||
return out;
|
||||
}
|
||||
|
||||
BridgeDaliTarget BridgeDaliTarget::fromJson(const DaliValue::Object* json) {
|
||||
BridgeDaliTarget target;
|
||||
if (json == nullptr) {
|
||||
return target;
|
||||
}
|
||||
target.shortAddress = getObjectInt(*json, "shortAddress");
|
||||
target.rawAddress = getObjectInt(*json, "rawAddress");
|
||||
target.rawCommand = getObjectInt(*json, "rawCommand");
|
||||
return target;
|
||||
}
|
||||
|
||||
DaliValue::Object BridgeDaliTarget::toJson() const {
|
||||
DaliValue::Object out;
|
||||
if (shortAddress.has_value()) out["shortAddress"] = shortAddress.value();
|
||||
if (rawAddress.has_value()) out["rawAddress"] = rawAddress.value();
|
||||
if (rawCommand.has_value()) out["rawCommand"] = rawCommand.value();
|
||||
return out;
|
||||
}
|
||||
|
||||
BridgeModel BridgeModel::fromJson(const DaliValue::Object& json) {
|
||||
BridgeModel model;
|
||||
model.id = getObjectString(json, "id").value_or("");
|
||||
model.name = getObjectString(json, "name").value_or(model.id);
|
||||
model.protocol = bridgeProtocolKindFromString(getObjectString(json, "protocol").value_or(""));
|
||||
model.operation = bridgeOperationFromString(getObjectString(json, "operation").value_or(""));
|
||||
model.valueEncoding =
|
||||
bridgeValueEncodingFromString(getObjectString(json, "valueEncoding").value_or(""));
|
||||
if (const auto* external = getObjectValue(json, "external")) {
|
||||
model.external = BridgeExternalPoint::fromJson(external->asObject());
|
||||
}
|
||||
if (const auto* dali = getObjectValue(json, "dali")) {
|
||||
model.dali = BridgeDaliTarget::fromJson(dali->asObject());
|
||||
}
|
||||
if (const auto* transform = getObjectValue(json, "valueTransform")) {
|
||||
model.valueTransform = BridgeValueTransform::fromJson(transform->asObject());
|
||||
}
|
||||
if (const auto* metadata = getObjectValue(json, "meta")) {
|
||||
if (const auto* object = metadata->asObject()) {
|
||||
model.metadata = *object;
|
||||
}
|
||||
}
|
||||
if (model.name.empty()) {
|
||||
model.name = model.id;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
DaliValue::Object BridgeModel::toJson() const {
|
||||
DaliValue::Object out;
|
||||
out["id"] = id;
|
||||
out["name"] = name;
|
||||
out["protocol"] = bridgeProtocolKindToString(protocol);
|
||||
out["external"] = external.toJson();
|
||||
out["dali"] = dali.toJson();
|
||||
out["operation"] = bridgeOperationToString(operation);
|
||||
out["valueEncoding"] = bridgeValueEncodingToString(valueEncoding);
|
||||
out["valueTransform"] = valueTransform.toJson();
|
||||
if (!metadata.empty()) out["meta"] = metadata;
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string BridgeModel::displayName() const {
|
||||
return name.empty() ? id : name;
|
||||
}
|
||||
|
||||
const char* bridgeProtocolKindToString(BridgeProtocolKind kind) {
|
||||
switch (kind) {
|
||||
case BridgeProtocolKind::mqtt:
|
||||
return "mqtt";
|
||||
case BridgeProtocolKind::modbus:
|
||||
return "modbus";
|
||||
case BridgeProtocolKind::bacnet:
|
||||
return "bacnet";
|
||||
case BridgeProtocolKind::unknown:
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
BridgeProtocolKind bridgeProtocolKindFromString(const std::string& value) {
|
||||
const std::string normalized = normalize(value);
|
||||
if (normalized == "mqtt") return BridgeProtocolKind::mqtt;
|
||||
if (normalized == "modbus") return BridgeProtocolKind::modbus;
|
||||
if (normalized == "bacnet") return BridgeProtocolKind::bacnet;
|
||||
return BridgeProtocolKind::unknown;
|
||||
}
|
||||
|
||||
const char* bridgeObjectTypeToString(BridgeObjectType type) {
|
||||
switch (type) {
|
||||
case BridgeObjectType::holdingRegister:
|
||||
return "holding_register";
|
||||
case BridgeObjectType::inputRegister:
|
||||
return "input_register";
|
||||
case BridgeObjectType::coil:
|
||||
return "coil";
|
||||
case BridgeObjectType::discreteInput:
|
||||
return "discrete_input";
|
||||
case BridgeObjectType::analogValue:
|
||||
return "analog_value";
|
||||
case BridgeObjectType::analogOutput:
|
||||
return "analog_output";
|
||||
case BridgeObjectType::binaryValue:
|
||||
return "binary_value";
|
||||
case BridgeObjectType::binaryOutput:
|
||||
return "binary_output";
|
||||
case BridgeObjectType::multiStateValue:
|
||||
return "multi_state_value";
|
||||
case BridgeObjectType::unknown:
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
BridgeObjectType bridgeObjectTypeFromString(const std::string& value) {
|
||||
const std::string normalized = normalize(value);
|
||||
if (normalized == "holding_register") return BridgeObjectType::holdingRegister;
|
||||
if (normalized == "input_register") return BridgeObjectType::inputRegister;
|
||||
if (normalized == "coil") return BridgeObjectType::coil;
|
||||
if (normalized == "discrete_input") return BridgeObjectType::discreteInput;
|
||||
if (normalized == "analog_value") return BridgeObjectType::analogValue;
|
||||
if (normalized == "analog_output") return BridgeObjectType::analogOutput;
|
||||
if (normalized == "binary_value") return BridgeObjectType::binaryValue;
|
||||
if (normalized == "binary_output") return BridgeObjectType::binaryOutput;
|
||||
if (normalized == "multi_state_value") return BridgeObjectType::multiStateValue;
|
||||
return BridgeObjectType::unknown;
|
||||
}
|
||||
|
||||
const char* bridgeOperationToString(BridgeOperation operation) {
|
||||
switch (operation) {
|
||||
case BridgeOperation::send:
|
||||
return "send";
|
||||
case BridgeOperation::sendExt:
|
||||
return "send_ext";
|
||||
case BridgeOperation::query:
|
||||
return "query";
|
||||
case BridgeOperation::setBrightness:
|
||||
return "set_brightness";
|
||||
case BridgeOperation::setBrightnessPercent:
|
||||
return "set_brightness_percent";
|
||||
case BridgeOperation::on:
|
||||
return "on";
|
||||
case BridgeOperation::off:
|
||||
return "off";
|
||||
case BridgeOperation::recallMaxLevel:
|
||||
return "recall_max_level";
|
||||
case BridgeOperation::recallMinLevel:
|
||||
return "recall_min_level";
|
||||
case BridgeOperation::setColorTemperature:
|
||||
return "set_color_temperature";
|
||||
case BridgeOperation::getBrightness:
|
||||
return "get_brightness";
|
||||
case BridgeOperation::getStatus:
|
||||
return "get_status";
|
||||
case BridgeOperation::getColorTemperature:
|
||||
return "get_color_temperature";
|
||||
case BridgeOperation::getColorStatus:
|
||||
return "get_color_status";
|
||||
case BridgeOperation::getEmergencyLevel:
|
||||
return "get_emergency_level";
|
||||
case BridgeOperation::getEmergencyStatus:
|
||||
return "get_emergency_status";
|
||||
case BridgeOperation::getEmergencyFailureStatus:
|
||||
return "get_emergency_failure_status";
|
||||
case BridgeOperation::startEmergencyFunctionTest:
|
||||
return "start_emergency_function_test";
|
||||
case BridgeOperation::stopEmergencyTest:
|
||||
return "stop_emergency_test";
|
||||
case BridgeOperation::unknown:
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
BridgeOperation bridgeOperationFromString(const std::string& value) {
|
||||
const std::string normalized = normalize(value);
|
||||
if (normalized == "send") return BridgeOperation::send;
|
||||
if (normalized == "send_ext") return BridgeOperation::sendExt;
|
||||
if (normalized == "query") return BridgeOperation::query;
|
||||
if (normalized == "set_brightness") return BridgeOperation::setBrightness;
|
||||
if (normalized == "set_brightness_percent") return BridgeOperation::setBrightnessPercent;
|
||||
if (normalized == "on") return BridgeOperation::on;
|
||||
if (normalized == "off") return BridgeOperation::off;
|
||||
if (normalized == "recall_max_level") return BridgeOperation::recallMaxLevel;
|
||||
if (normalized == "recall_min_level") return BridgeOperation::recallMinLevel;
|
||||
if (normalized == "set_color_temperature") return BridgeOperation::setColorTemperature;
|
||||
if (normalized == "get_brightness") return BridgeOperation::getBrightness;
|
||||
if (normalized == "get_status") return BridgeOperation::getStatus;
|
||||
if (normalized == "get_color_temperature") return BridgeOperation::getColorTemperature;
|
||||
if (normalized == "get_color_status") return BridgeOperation::getColorStatus;
|
||||
if (normalized == "get_emergency_level") return BridgeOperation::getEmergencyLevel;
|
||||
if (normalized == "get_emergency_status") return BridgeOperation::getEmergencyStatus;
|
||||
if (normalized == "get_emergency_failure_status") return BridgeOperation::getEmergencyFailureStatus;
|
||||
if (normalized == "start_emergency_function_test") {
|
||||
return BridgeOperation::startEmergencyFunctionTest;
|
||||
}
|
||||
if (normalized == "stop_emergency_test") return BridgeOperation::stopEmergencyTest;
|
||||
return BridgeOperation::unknown;
|
||||
}
|
||||
|
||||
const char* bridgeValueEncodingToString(BridgeValueEncoding encoding) {
|
||||
switch (encoding) {
|
||||
case BridgeValueEncoding::integer:
|
||||
return "integer";
|
||||
case BridgeValueEncoding::percentage:
|
||||
return "percentage";
|
||||
case BridgeValueEncoding::kelvin:
|
||||
return "kelvin";
|
||||
case BridgeValueEncoding::none:
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
BridgeValueEncoding bridgeValueEncodingFromString(const std::string& value) {
|
||||
const std::string normalized = normalize(value);
|
||||
if (normalized == "integer") return BridgeValueEncoding::integer;
|
||||
if (normalized == "percentage") return BridgeValueEncoding::percentage;
|
||||
if (normalized == "kelvin") return BridgeValueEncoding::kelvin;
|
||||
return BridgeValueEncoding::none;
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
#include "bridge_provisioning.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
extern "C" {
|
||||
#include "cJSON.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kTag = "bridge_provision";
|
||||
constexpr const char* kKeyConfig = "bridge_cfg";
|
||||
|
||||
cJSON* toCjson(const DaliValue& value) {
|
||||
if (value.isNull()) {
|
||||
return cJSON_CreateNull();
|
||||
}
|
||||
if (value.isBool()) {
|
||||
return cJSON_CreateBool(value.asBool().value_or(false));
|
||||
}
|
||||
if (value.isInt()) {
|
||||
return cJSON_CreateNumber(value.asInt().value_or(0));
|
||||
}
|
||||
if (value.isDouble()) {
|
||||
return cJSON_CreateNumber(value.asDouble().value_or(0.0));
|
||||
}
|
||||
if (value.isString()) {
|
||||
return cJSON_CreateString(value.asString().value_or("").c_str());
|
||||
}
|
||||
if (const auto* array = value.asArray()) {
|
||||
cJSON* out = cJSON_CreateArray();
|
||||
for (const auto& item : *array) {
|
||||
cJSON_AddItemToArray(out, toCjson(item));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (const auto* object = value.asObject()) {
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
for (const auto& entry : *object) {
|
||||
cJSON_AddItemToObject(out, entry.first.c_str(), toCjson(entry.second));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return cJSON_CreateNull();
|
||||
}
|
||||
|
||||
DaliValue fromCjson(const cJSON* item) {
|
||||
if (item == nullptr || cJSON_IsNull(item)) {
|
||||
return DaliValue();
|
||||
}
|
||||
if (cJSON_IsBool(item)) {
|
||||
return DaliValue(cJSON_IsTrue(item));
|
||||
}
|
||||
if (cJSON_IsNumber(item)) {
|
||||
if (item->valuedouble == static_cast<double>(item->valueint)) {
|
||||
return DaliValue(item->valueint);
|
||||
}
|
||||
return DaliValue(item->valuedouble);
|
||||
}
|
||||
if (cJSON_IsString(item) && item->valuestring != nullptr) {
|
||||
return DaliValue(std::string(item->valuestring));
|
||||
}
|
||||
if (cJSON_IsArray(item)) {
|
||||
DaliValue::Array out;
|
||||
for (const cJSON* child = item->child; child != nullptr; child = child->next) {
|
||||
out.push_back(fromCjson(child));
|
||||
}
|
||||
return DaliValue(std::move(out));
|
||||
}
|
||||
if (cJSON_IsObject(item)) {
|
||||
DaliValue::Object out;
|
||||
for (const cJSON* child = item->child; child != nullptr; child = child->next) {
|
||||
if (child->string != nullptr) {
|
||||
out[child->string] = fromCjson(child);
|
||||
}
|
||||
}
|
||||
return DaliValue(std::move(out));
|
||||
}
|
||||
return DaliValue();
|
||||
}
|
||||
|
||||
esp_err_t readString(nvs_handle_t handle, const char* key, std::string* value) {
|
||||
size_t required = 0;
|
||||
esp_err_t err = nvs_get_str(handle, key, nullptr, &required);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
std::string buffer(required, '\0');
|
||||
err = nvs_get_str(handle, key, buffer.data(), &required);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
if (!buffer.empty() && buffer.back() == '\0') {
|
||||
buffer.pop_back();
|
||||
}
|
||||
*value = buffer;
|
||||
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) {
|
||||
if (value == nullptr || value->asObject() == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto& json = *value->asObject();
|
||||
BacnetBridgeConfig config;
|
||||
config.deviceInstance = static_cast<uint32_t>(getObjectInt(json, "deviceInstance").value_or(4194303));
|
||||
config.localAddress = getObjectString(json, "localAddress").value_or("");
|
||||
config.udpPort = static_cast<uint16_t>(getObjectInt(json, "udpPort").value_or(47808));
|
||||
return config;
|
||||
}
|
||||
|
||||
DaliValue bacnetToJson(const BacnetBridgeConfig& config) {
|
||||
DaliValue::Object out;
|
||||
out["deviceInstance"] = static_cast<int64_t>(config.deviceInstance);
|
||||
out["localAddress"] = config.localAddress;
|
||||
out["udpPort"] = static_cast<int>(config.udpPort);
|
||||
return DaliValue(std::move(out));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BridgeRuntimeConfig BridgeRuntimeConfig::fromJson(const DaliValue::Object& json) {
|
||||
BridgeRuntimeConfig config;
|
||||
if (const auto* modelsValue = getObjectValue(json, "models")) {
|
||||
if (const auto* models = modelsValue->asArray()) {
|
||||
config.models.reserve(models->size());
|
||||
for (const auto& modelValue : *models) {
|
||||
if (const auto* object = modelValue.asObject()) {
|
||||
config.models.push_back(BridgeModel::fromJson(*object));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
config.modbus = modbusFromJson(getObjectValue(json, "modbus"));
|
||||
config.bacnet = bacnetFromJson(getObjectValue(json, "bacnet"));
|
||||
if (const auto* metadata = getObjectValue(json, "meta")) {
|
||||
if (const auto* object = metadata->asObject()) {
|
||||
config.metadata = *object;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
DaliValue::Object BridgeRuntimeConfig::toJson() const {
|
||||
DaliValue::Object out;
|
||||
DaliValue::Array modelsValue;
|
||||
modelsValue.reserve(models.size());
|
||||
for (const auto& model : models) {
|
||||
modelsValue.emplace_back(model.toJson());
|
||||
}
|
||||
out["models"] = std::move(modelsValue);
|
||||
if (modbus.has_value()) out["modbus"] = modbusToJson(modbus.value());
|
||||
if (bacnet.has_value()) out["bacnet"] = bacnetToJson(bacnet.value());
|
||||
if (!metadata.empty()) out["meta"] = metadata;
|
||||
return out;
|
||||
}
|
||||
|
||||
esp_err_t BridgeProvisioningStore::save(const BridgeRuntimeConfig& config) const {
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open(nvsNamespace_.c_str(), NVS_READWRITE, &handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(kTag, "nvs_open(save) failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
cJSON* root = toCjson(DaliValue(config.toJson()));
|
||||
char* raw = cJSON_PrintUnformatted(root);
|
||||
if (raw == nullptr) {
|
||||
cJSON_Delete(root);
|
||||
nvs_close(handle);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
err = nvs_set_str(handle, kKeyConfig, raw);
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_commit(handle);
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
cJSON_free(raw);
|
||||
nvs_close(handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(kTag, "save failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t BridgeProvisioningStore::load(BridgeRuntimeConfig* config) const {
|
||||
if (config == nullptr) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open(nvsNamespace_.c_str(), NVS_READONLY, &handle);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
std::string payload;
|
||||
err = readString(handle, kKeyConfig, &payload);
|
||||
nvs_close(handle);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_Parse(payload.c_str());
|
||||
if (root == nullptr) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
const DaliValue value = fromCjson(root);
|
||||
cJSON_Delete(root);
|
||||
const auto* object = value.asObject();
|
||||
if (object == nullptr) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
*config = BridgeRuntimeConfig::fromJson(*object);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t BridgeProvisioningStore::clear() const {
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open(nvsNamespace_.c_str(), NVS_READWRITE, &handle);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
err = nvs_erase_key(handle, kKeyConfig);
|
||||
if (err == ESP_ERR_NVS_NOT_FOUND) {
|
||||
err = ESP_OK;
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_commit(handle);
|
||||
}
|
||||
nvs_close(handle);
|
||||
return err;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
BridgeRuntimeConfig BridgeRuntimeConfig::fromJson(const DaliValue::Object& json) {
|
||||
(void)json;
|
||||
return BridgeRuntimeConfig{};
|
||||
}
|
||||
|
||||
DaliValue::Object BridgeRuntimeConfig::toJson() const { return DaliValue::Object{}; }
|
||||
|
||||
esp_err_t BridgeProvisioningStore::save(const BridgeRuntimeConfig& config) const {
|
||||
(void)config;
|
||||
return -1;
|
||||
}
|
||||
|
||||
esp_err_t BridgeProvisioningStore::load(BridgeRuntimeConfig* config) const {
|
||||
(void)config;
|
||||
return -1;
|
||||
}
|
||||
|
||||
esp_err_t BridgeProvisioningStore::clear() const { return -1; }
|
||||
|
||||
#endif
|
||||
+85
-32
@@ -33,10 +33,43 @@ int toInt(const cJSON* item, int fallback) {
|
||||
return item->valueint;
|
||||
}
|
||||
|
||||
cJSON* toCjson(const DaliValue& value) {
|
||||
if (value.isNull()) {
|
||||
return cJSON_CreateNull();
|
||||
}
|
||||
if (value.isBool()) {
|
||||
return cJSON_CreateBool(value.asBool().value_or(false));
|
||||
}
|
||||
if (value.isInt()) {
|
||||
return cJSON_CreateNumber(value.asInt().value_or(0));
|
||||
}
|
||||
if (value.isDouble()) {
|
||||
return cJSON_CreateNumber(value.asDouble().value_or(0.0));
|
||||
}
|
||||
if (value.isString()) {
|
||||
return cJSON_CreateString(value.asString().value_or("").c_str());
|
||||
}
|
||||
if (const auto* array = value.asArray()) {
|
||||
cJSON* out = cJSON_CreateArray();
|
||||
for (const auto& item : *array) {
|
||||
cJSON_AddItemToArray(out, toCjson(item));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (const auto* object = value.asObject()) {
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
for (const auto& entry : *object) {
|
||||
cJSON_AddItemToObject(out, entry.first.c_str(), toCjson(entry.second));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return cJSON_CreateNull();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
DaliCloudBridge::DaliCloudBridge(DaliComm& comm) : comm_(comm) {}
|
||||
DaliCloudBridge::DaliCloudBridge(DaliComm& comm) : comm_(comm), bridge_(comm) {}
|
||||
|
||||
bool DaliCloudBridge::start(const GatewayCloudConfig& config) {
|
||||
config_ = config;
|
||||
@@ -137,49 +170,69 @@ bool DaliCloudBridge::handleDownlink(const std::string& payload) {
|
||||
}
|
||||
|
||||
const cJSON* seqItem = cJSON_GetObjectItemCaseSensitive(root, "seq");
|
||||
const cJSON* modelItem = cJSON_GetObjectItemCaseSensitive(root, "model");
|
||||
const cJSON* opItem = cJSON_GetObjectItemCaseSensitive(root, "op");
|
||||
const cJSON* addrItem = cJSON_GetObjectItemCaseSensitive(root, "addr");
|
||||
const cJSON* cmdItem = cJSON_GetObjectItemCaseSensitive(root, "cmd");
|
||||
const cJSON* shortAddrItem = cJSON_GetObjectItemCaseSensitive(root, "shortAddress");
|
||||
const cJSON* valueItem = cJSON_GetObjectItemCaseSensitive(root, "value");
|
||||
|
||||
DaliBridgeRequest request;
|
||||
request.sequence = toString(seqItem);
|
||||
request.modelID = toString(modelItem);
|
||||
|
||||
const std::string op = toString(opItem);
|
||||
if (!op.empty()) {
|
||||
request.operation = bridgeOperationFromString(op);
|
||||
}
|
||||
|
||||
const std::string seq = toString(seqItem);
|
||||
const std::string op = toString(opItem).empty() ? "send" : toString(opItem);
|
||||
const int addr = toInt(addrItem, -1);
|
||||
if (addr >= 0) {
|
||||
request.rawAddress = addr;
|
||||
}
|
||||
|
||||
const int cmd = toInt(cmdItem, -1);
|
||||
|
||||
bool ok = false;
|
||||
bool hasData = false;
|
||||
int data = -1;
|
||||
std::string error = "";
|
||||
|
||||
if (addr < 0 || addr > 255 || cmd < 0 || cmd > 255) {
|
||||
error = "invalid addr/cmd";
|
||||
} else if (op == "send") {
|
||||
ok = comm_.sendRaw(static_cast<uint8_t>(addr), static_cast<uint8_t>(cmd));
|
||||
} else if (op == "send_ext") {
|
||||
ok = comm_.sendExtRaw(static_cast<uint8_t>(addr), static_cast<uint8_t>(cmd));
|
||||
} else if (op == "query") {
|
||||
auto response = comm_.queryRaw(static_cast<uint8_t>(addr), static_cast<uint8_t>(cmd));
|
||||
if (response.has_value()) {
|
||||
ok = true;
|
||||
hasData = true;
|
||||
data = static_cast<int>(response.value());
|
||||
} else {
|
||||
error = "no response";
|
||||
if (cmd >= 0) {
|
||||
request.rawCommand = cmd;
|
||||
}
|
||||
} else {
|
||||
error = "unsupported op";
|
||||
|
||||
const int shortAddr = toInt(shortAddrItem, -1);
|
||||
if (shortAddr >= 0) {
|
||||
request.shortAddress = shortAddr;
|
||||
}
|
||||
|
||||
if (valueItem != nullptr) {
|
||||
if (cJSON_IsNumber(valueItem)) {
|
||||
request.value = valueItem->valuedouble;
|
||||
} else if (cJSON_IsString(valueItem) && valueItem->valuestring != nullptr) {
|
||||
request.value = std::string(valueItem->valuestring);
|
||||
} else if (cJSON_IsBool(valueItem)) {
|
||||
request.value = cJSON_IsTrue(valueItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (!request.operation.has_value()) {
|
||||
request.operation = BridgeOperation::send;
|
||||
}
|
||||
|
||||
const DaliBridgeResult result = bridge_.execute(request);
|
||||
|
||||
cJSON* resp = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(resp, "type", "dali_resp");
|
||||
cJSON_AddStringToObject(resp, "seq", seq.c_str());
|
||||
cJSON_AddStringToObject(resp, "op", op.c_str());
|
||||
cJSON_AddBoolToObject(resp, "ok", ok);
|
||||
if (hasData) {
|
||||
cJSON_AddNumberToObject(resp, "data", data);
|
||||
cJSON_AddStringToObject(resp, "seq", result.sequence.c_str());
|
||||
if (!result.modelID.empty()) {
|
||||
cJSON_AddStringToObject(resp, "model", result.modelID.c_str());
|
||||
}
|
||||
if (!ok) {
|
||||
cJSON_AddStringToObject(resp, "error", error.c_str());
|
||||
cJSON_AddStringToObject(resp, "op", bridgeOperationToString(result.operation));
|
||||
cJSON_AddBoolToObject(resp, "ok", result.ok);
|
||||
if (result.data.has_value()) {
|
||||
cJSON_AddNumberToObject(resp, "data", result.data.value());
|
||||
}
|
||||
if (!result.error.empty()) {
|
||||
cJSON_AddStringToObject(resp, "error", result.error.c_str());
|
||||
}
|
||||
if (!result.metadata.empty()) {
|
||||
cJSON_AddItemToObject(resp, "meta", toCjson(DaliValue(result.metadata)));
|
||||
}
|
||||
|
||||
char* raw = cJSON_PrintUnformatted(resp);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#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