|
|
|
@@ -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);
|
|
|
|
|
}
|