#include "bacnet_bridge.hpp" #include "bridge.hpp" #include "bridge_model.hpp" #include "bridge_provisioning.hpp" #include "dali_comm.hpp" #include "modbus_bridge.hpp" #include #include 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(len), len > 0 ? data[0] : 0U); (void)data; return false; } std::vector readGateway(size_t len, uint32_t timeoutMs) { ESP_LOGI(kTag, "placeholder DALI gateway read len=%u timeout=%u", static_cast(len), static_cast(timeoutMs)); return {}; } std::vector transactGateway(const uint8_t* data, size_t len) { ESP_LOGI(kTag, "placeholder DALI gateway transact len=%u first=0x%02X", static_cast(len), len > 0 ? data[0] : 0U); (void)data; return {}; } uint16_t readBe16(const uint8_t* data) { return static_cast((static_cast(data[0]) << 8) | data[1]); } void writeBe16(uint8_t* data, uint16_t value) { data[0] = static_cast((value >> 8) & 0xFF); data[1] = static_cast(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(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(ret); } return true; } int normalizeHoldingRegister(uint16_t zeroBasedAddress) { return 40001 + static_cast(zeroBasedAddress); } bool sendModbusFrame(int sock, const uint8_t* mbap, const std::vector& pdu) { std::vector frame(7 + pdu.size()); std::memcpy(frame.data(), mbap, 7); writeBe16(&frame[4], static_cast(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 pdu{static_cast(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() ? "" : 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 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(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(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 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(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(&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(&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); }