diff --git a/README.md b/README.md index 740a73c..3f584ff 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,14 @@ broadcast targets are never queried for refresh. Gateway feature opcode `0x06` keeps the Lua-compatible low-byte feature bits, advertises cache support with bit `0x40`, advertises gateway/channel name support with bit `0x80`, and advertises the native C++ gateway type with bit -`0x0100`. Gateway opcode `0x05` reads and writes user-facing gateway identity: -operation `0x00` reads the channel name, `0x01` writes the channel name, -`0x02` reads the physical gateway device name, and `0x03` writes the physical -gateway device name. Empty writes reset to the default name. Gateway opcode -`0x39` returns cache summary and target snapshots so frontend clients can read -cached state without issuing live DALI queries on supported gateways. +`0x0100`. Bit `0x0200` advertises the generic gateway operation protocol, and +bit `0x0400` advertises explicit raw-report leases. Gateway opcode `0x05` reads +and writes user-facing gateway identity: operation `0x00` reads the channel +name, `0x01` writes the channel name, `0x02` reads the physical gateway device +name, and `0x03` writes the physical gateway device name. Empty writes reset to +the default name. Gateway opcode `0x39` returns cache summary and target +snapshots so frontend clients can read cached state without issuing live DALI +queries on supported gateways. Gateway opcode `0x09` with address/data `0x00/0x00` is a chip-level channel-id report. It returns the enabled DALI channel ids so clients do not need to probe @@ -73,6 +75,52 @@ channel number after the serial matches, so BLE/Wi-Fi configuration and DALI send/query commands can target a channel even when its variable channel id is unknown. +## Gateway operation protocol + +Opcode `0x67` starts, aborts, and polls gateway-executed high-level DALI +operations. Clients send one compact gateway command and the controller runs the +required DTR setup, device-type select, query chain, retry, or address iteration +on the gateway. The command frame is: + +- Start: `28 01 67 01 `. +- Abort: `28 01 67 02 00 00 00 00 `. +- Status: `28 01 67 03 00 00 00 00 `. + +Multi-byte fields are little-endian. TLV payload fields use +`{fieldId:u8, type:u8, len:u8, value...}`. Types are `0x01` u8, `0x02` u16, +`0x03` u32, `0x04` signed i32, `0x05` bool, and `0x06` byte list. Dynamic +snapshot metadata is returned as repeated byte-list entries using compact +`key=value` payloads. + +Operation events are emitted as +`22 67 `. +Event values are accepted `0x00`, progress `0x01`, item result `0x02`, +completed `0x03`, aborted `0x04`, and error `0x05`. Status values are ok +`0x00`, busy `0x01`, invalid `0x02`, unsupported `0x03`, no response `0x04`, +failed `0x05`, and aborted `0x06`. + +Large result payloads are emitted as chunks: +`22 68 `. +Clients assemble chunks by gateway, request id, and operation id before +processing result TLVs. + +The initial C++ executor accepts the shared `BridgeOperation` numeric ids for +raw send/query, brightness/on/off/recall, color-temperature and DT8 RGB/XY/RGBW +setters, DT1/DT4/DT5/DT6/DT8 snapshots, group masks, scene levels/maps, address +settings, short-address range search, and short-address allocation/reset/stop. +Unsupported ids return an `unsupported` operation error so old clients can fall +back to their app-side workflow. Legacy opcodes such as `0x12`, `0x13`, `0x14`, +`0x30`, `0x32`, `0x60`-`0x65`, and `0x39` remain available for compatibility. + +Opcode `0x66` controls passive raw-report leases. Command +`28 01 66 01 ` enables or disables a +volatile per-gateway lease; `ttl=0` disables. The response is +`22 66 `. Passive raw DALI +notifications over UDP, BLE raw characteristics, ESP-NOW setup UART mirroring, +and gateway notification opcodes `0x01` and `0x65` are suppressed unless the +lease is active. Direct command/query responses still work without a raw-report +lease. + ## Current status The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port with raw receive fan-out, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app also includes a `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, W5500 SPI Ethernet with DHCP, Wi-Fi STA lifecycle, ESP-Touch smartconfig credential provisioning, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, setup AP GPIO entry, and optional Wi-Fi credential reset GPIO handling, and an optional `gateway_485_control` bridge that claims UART0 for Lua-compatible framed command ingress plus `0x22` notification egress when the console is moved off UART0. Startup behavior is configured in `main/Kconfig.projbuild`: BLE and wired Ethernet are enabled by default, W5500 initialization and startup probe failures are ignored by default for boards without populated Ethernet hardware by fully disabling Ethernet for that boot, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected, and the UART0 control bridge stays disabled unless the deployment explicitly repurposes UART0 away from the ESP-IDF console. Runtime settings and internal scene/group data are cached in RAM after load, skip unchanged flash writes, and batch Wi-Fi credential commits to reduce flash stalls on ESP32-S3 boards where flash and PSRAM share the SPI bus. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. The checked-in `sdkconfig` is aligned with the app's custom 16 MB partition table so the Wi-Fi/BLE/network-enabled image fits the OTA app slots. diff --git a/apps/gateway/.vscode/c_cpp_properties.json b/apps/gateway/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..22b26a4 --- /dev/null +++ b/apps/gateway/.vscode/c_cpp_properties.json @@ -0,0 +1,15 @@ +{ + "configurations": [ + { + "name": "Mac", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "cStandard": "c17", + "cppStandard": "c++14", + "intelliSenseMode": "${default}" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/apps/gateway/.vscode/settings.json b/apps/gateway/.vscode/settings.json index 1a2b6cc..f4155ba 100644 --- a/apps/gateway/.vscode/settings.json +++ b/apps/gateway/.vscode/settings.json @@ -4,7 +4,7 @@ "OPENOCD_SCRIPTS": "/Users/tonylu/.espressif/tools/openocd-esp32/v0.12.0-esp32-20240318/openocd-esp32/share/openocd/scripts", "ESP_ROM_ELF_DIR": "/Users/tonylu/.espressif/tools/esp-rom-elfs/20240305/" }, - "clangd.path": "/Users/tonylu/.espressif/tools/esp-clang/esp-20.1.1_20250829/esp-clang/bin/clangd", + "clangd.path": "/Users/tonylu/.espressif/tools/esp-clang/esp-19.1.2_20250312/esp-clang/bin/clangd", "clangd.arguments": [ "--background-index", "--query-driver=**", diff --git a/components/gateway_ble/src/gateway_ble.cpp b/components/gateway_ble/src/gateway_ble.cpp index 9957539..e15d7a7 100644 --- a/components/gateway_ble/src/gateway_ble.cpp +++ b/components/gateway_ble/src/gateway_ble.cpp @@ -405,6 +405,9 @@ void GatewayBleBridge::handleDaliRawFrame(const DaliRawFrame& frame) { if (!enabled_ || conn_handle_ == kInvalidConnectionHandle || frame.data.empty()) { return; } + if (!controller_.rawReportingEnabled(frame.gateway_id)) { + return; + } notifyCharacteristic(frame.channel_index, LegacyRawPayload(frame.data)); } diff --git a/components/gateway_controller/include/gateway_controller.hpp b/components/gateway_controller/include/gateway_controller.hpp index 91f8c7c..21e1f3e 100644 --- a/components/gateway_controller/include/gateway_controller.hpp +++ b/components/gateway_controller/include/gateway_controller.hpp @@ -52,6 +52,13 @@ struct GatewayChannelSnapshot { uint8_t group_mask_high{0}; bool allocating{false}; int last_alloc_addr{0}; + bool operation_active{false}; + uint8_t operation_request_id{0}; + uint16_t operation_id{0}; + uint8_t operation_status{0}; + uint8_t operation_progress{0}; + uint8_t operation_target{0}; + uint8_t operation_count{0}; }; struct GatewayControllerSnapshot { @@ -109,8 +116,16 @@ class GatewayController { bool bleEnabled() const; bool wifiEnabled() const; bool ipRouterEnabled() const; + bool rawReportingEnabled(uint8_t gateway_id) const; GatewayControllerSnapshot snapshot(); + struct ParsedTlv { + uint8_t type{0}; + std::vector value; + }; + + using ParsedTlvMap = std::map; + private: struct ReconciliationJob { enum class Phase : uint8_t { @@ -140,6 +155,30 @@ class GatewayController { uint8_t short_address{0}; }; + struct GatewayOperationRuntimeState { + bool active{false}; + bool cancel_requested{false}; + uint8_t request_id{0}; + uint16_t operation_id{0}; + uint8_t status{0}; + uint8_t progress{0}; + uint8_t target{0}; + uint8_t count{0}; + }; + + struct GatewayRawReportLease { + bool enabled{false}; + TickType_t expires_at{0}; + }; + + struct GatewayOperationTaskContext { + GatewayController* controller{nullptr}; + uint8_t gateway_id{0}; + uint8_t request_id{0}; + uint16_t operation_id{0}; + ParsedTlvMap fields; + }; + struct TransactionWaiter { uint8_t gateway_id{0}; uint8_t opcode{0}; @@ -150,7 +189,9 @@ class GatewayController { }; static void TaskEntry(void* arg); + static void OperationTaskEntry(void* arg); void taskLoop(); + void runOperationTask(GatewayOperationTaskContext* context); void dispatchCommand(const std::vector& command); void scheduleReconciliation(uint8_t gateway_id, std::optional target = std::nullopt); @@ -171,6 +212,13 @@ class GatewayController { void refreshRuntimeGatewayNames(); void publishPayload(uint8_t gateway_id, const std::vector& payload); void publishFrame(const std::vector& frame); + void publishRawReportLeaseResponse(uint8_t gateway_id, uint8_t status); + void publishOperationEvent(uint8_t gateway_id, uint8_t request_id, uint16_t operation_id, + uint8_t event, uint8_t status, uint8_t progress, + uint8_t target, uint8_t count); + void publishOperationResultChunks(uint8_t gateway_id, uint8_t request_id, + uint16_t operation_id, + const std::vector& tlv_payload); bool transactionFrameMatches(const TransactionWaiter& waiter, const std::vector& frame) const; void captureTransactionFrame(const std::vector& frame); @@ -219,6 +267,16 @@ class GatewayController { bool gatewaySerialMatches(const std::vector& command) const; void handleGatewayIdentityCommand(uint8_t gateway_id, uint8_t op); void handleAllocationCommand(uint8_t gateway_id, const std::vector& command); + void handleRawReportLeaseCommand(uint8_t gateway_id, const std::vector& command); + void handleGatewayOperationCommand(uint8_t gateway_id, const std::vector& command); + bool operationActive(uint8_t gateway_id) const; + bool operationCancelRequested(uint8_t gateway_id, uint8_t request_id) const; + bool startOperation(uint8_t gateway_id, uint8_t request_id, uint16_t operation_id, + ParsedTlvMap fields); + void abortOperation(uint8_t gateway_id, uint8_t request_id); + void finishOperation(uint8_t gateway_id, uint8_t request_id, uint8_t status, + uint8_t progress, uint8_t target, uint8_t count); + ParsedTlvMap parseOperationTlvs(const uint8_t* data, size_t len, bool* ok) const; void handleInternalSceneCommand(uint8_t gateway_id, const std::vector& command); void handleInternalGroupCommand(uint8_t gateway_id, const std::vector& command); void handleGatewayCacheCommand(uint8_t gateway_id, const std::vector& command); @@ -234,6 +292,7 @@ class GatewayController { TaskHandle_t task_handle_{nullptr}; SemaphoreHandle_t maintenance_lock_{nullptr}; SemaphoreHandle_t transaction_lock_{nullptr}; + SemaphoreHandle_t operation_lock_{nullptr}; std::vector notification_sinks_; std::vector ble_state_sinks_; std::vector wifi_state_sinks_; @@ -242,6 +301,8 @@ class GatewayController { std::vector transaction_waiters_; std::map reconciliation_jobs_; std::map cache_refresh_jobs_; + std::map operation_states_; + std::map raw_report_leases_; std::atomic maintenance_activity_gateway_{-1}; bool setup_mode_{false}; bool wireless_setup_mode_{false}; diff --git a/components/gateway_controller/src/gateway_controller.cpp b/components/gateway_controller/src/gateway_controller.cpp index 406a802..e9b99c5 100644 --- a/components/gateway_controller/src/gateway_controller.cpp +++ b/components/gateway_controller/src/gateway_controller.cpp @@ -9,7 +9,9 @@ #include #include #include +#include #include +#include #include namespace gateway { @@ -43,9 +45,14 @@ constexpr uint8_t kDali103QueryOpcode = 0x62; constexpr uint8_t kDali103QueryResponseOpcode = 0x63; constexpr uint8_t kDali103NoResponseOpcode = 0x64; constexpr uint8_t kDali103RawFrameOpcode = 0x65; +constexpr uint8_t kRawReportLeaseOpcode = 0x66; +constexpr uint8_t kGatewayOperationOpcode = 0x67; +constexpr uint8_t kGatewayOperationResultOpcode = 0x68; constexpr uint16_t kGatewayFeatureCache = 0x0040; constexpr uint16_t kGatewayFeatureNames = 0x0080; constexpr uint16_t kGatewayFeatureNativeCpp = 0x0100; +constexpr uint16_t kGatewayFeatureOperationProtocol = 0x0200; +constexpr uint16_t kGatewayFeatureRawReportLease = 0x0400; constexpr uint8_t kGatewayCacheOpcode = 0x39; constexpr uint8_t kGatewayCacheProtocolVersion = 1; constexpr uint8_t kGatewayCacheOpSummary = 0x00; @@ -66,6 +73,102 @@ constexpr uint16_t kCacheFlagMinKnown = 1U << 7; constexpr uint16_t kCacheFlagMaxKnown = 1U << 8; constexpr uint16_t kCacheFlagFadeTimeKnown = 1U << 9; constexpr uint16_t kCacheFlagFadeRateKnown = 1U << 10; +constexpr uint8_t kOperationVerbStart = 0x01; +constexpr uint8_t kOperationVerbAbort = 0x02; +constexpr uint8_t kOperationVerbStatus = 0x03; +constexpr uint8_t kOperationEventAccepted = 0x00; +constexpr uint8_t kOperationEventProgress = 0x01; +constexpr uint8_t kOperationEventItemResult = 0x02; +constexpr uint8_t kOperationEventCompleted = 0x03; +constexpr uint8_t kOperationEventAborted = 0x04; +constexpr uint8_t kOperationEventError = 0x05; +constexpr uint8_t kOperationStatusOk = 0x00; +constexpr uint8_t kOperationStatusBusy = 0x01; +constexpr uint8_t kOperationStatusInvalid = 0x02; +constexpr uint8_t kOperationStatusUnsupported = 0x03; +constexpr uint8_t kOperationStatusNoResponse = 0x04; +constexpr uint8_t kOperationStatusFailed = 0x05; +constexpr uint8_t kOperationStatusAborted = 0x06; +constexpr uint8_t kTlvTypeU8 = 0x01; +constexpr uint8_t kTlvTypeU16 = 0x02; +constexpr uint8_t kTlvTypeU32 = 0x03; +constexpr uint8_t kTlvTypeI32 = 0x04; +constexpr uint8_t kTlvTypeBool = 0x05; +constexpr uint8_t kTlvTypeBytes = 0x06; +constexpr uint8_t kTlvFieldTarget = 0x01; +constexpr uint8_t kTlvFieldCommand = 0x02; +constexpr uint8_t kTlvFieldValue = 0x03; +constexpr uint8_t kTlvFieldRawAddress = 0x04; +constexpr uint8_t kTlvFieldStartAddress = 0x05; +constexpr uint8_t kTlvFieldX = 0x06; +constexpr uint8_t kTlvFieldY = 0x07; +constexpr uint8_t kTlvFieldRed = 0x08; +constexpr uint8_t kTlvFieldGreen = 0x09; +constexpr uint8_t kTlvFieldBlue = 0x0A; +constexpr uint8_t kTlvFieldWhite = 0x0B; +constexpr uint8_t kTlvFieldCoolWhite = 0x0C; +constexpr uint8_t kTlvFieldWarmWhite = 0x0D; +constexpr uint8_t kTlvFieldAmber = 0x0E; +constexpr uint8_t kTlvFieldFreeColour = 0x0F; +constexpr uint8_t kTlvFieldControl = 0x10; +constexpr uint8_t kTlvFieldScene = 0x11; +constexpr uint8_t kTlvFieldGroupMask = 0x12; +constexpr uint8_t kTlvFieldRemoveAddressFirst = 0x13; +constexpr uint8_t kTlvFieldCloseLight = 0x14; +constexpr uint8_t kTlvFieldEndAddress = 0x15; +constexpr uint8_t kTlvFieldKind = 0x20; +constexpr uint8_t kTlvFieldAddress = 0x21; +constexpr uint8_t kTlvFieldResult = 0x22; +constexpr uint8_t kTlvFieldEntry = 0x30; +constexpr uint8_t kTlvFieldPowerOnLevel = 0x40; +constexpr uint8_t kTlvFieldSystemFailureLevel = 0x41; +constexpr uint8_t kTlvFieldMinLevel = 0x42; +constexpr uint8_t kTlvFieldMaxLevel = 0x43; +constexpr uint8_t kTlvFieldFadeTime = 0x44; +constexpr uint8_t kTlvFieldFadeRate = 0x45; +constexpr size_t kOperationResultMaxChunkBytes = 120; +constexpr uint16_t kBridgeOperationSend = 1; +constexpr uint16_t kBridgeOperationSendExt = 2; +constexpr uint16_t kBridgeOperationQuery = 3; +constexpr uint16_t kBridgeOperationSetBrightness = 4; +constexpr uint16_t kBridgeOperationSetBrightnessPercent = 5; +constexpr uint16_t kBridgeOperationOn = 6; +constexpr uint16_t kBridgeOperationOff = 7; +constexpr uint16_t kBridgeOperationRecallMaxLevel = 8; +constexpr uint16_t kBridgeOperationRecallMinLevel = 9; +constexpr uint16_t kBridgeOperationSetColorTemperature = 10; +constexpr uint16_t kBridgeOperationGetBrightness = 11; +constexpr uint16_t kBridgeOperationGetStatus = 12; +constexpr uint16_t kBridgeOperationGetColorTemperature = 13; +constexpr uint16_t kBridgeOperationGetColorStatus = 14; +constexpr uint16_t kBridgeOperationSetColorTemperatureRaw = 21; +constexpr uint16_t kBridgeOperationSetColourXY = 22; +constexpr uint16_t kBridgeOperationStoreDt8SceneSnapshot = 30; +constexpr uint16_t kBridgeOperationStoreDt8PowerOnLevelSnapshot = 31; +constexpr uint16_t kBridgeOperationStoreDt8SystemFailureLevelSnapshot = 32; +constexpr uint16_t kBridgeOperationGetDt1Snapshot = 33; +constexpr uint16_t kBridgeOperationGetDt4Snapshot = 34; +constexpr uint16_t kBridgeOperationGetDt5Snapshot = 35; +constexpr uint16_t kBridgeOperationGetDt6Snapshot = 36; +constexpr uint16_t kBridgeOperationGetDt8StatusSnapshot = 37; +constexpr uint16_t kBridgeOperationGetGroupMask = 38; +constexpr uint16_t kBridgeOperationSetGroupMask = 39; +constexpr uint16_t kBridgeOperationGetSceneLevel = 40; +constexpr uint16_t kBridgeOperationSetSceneLevel = 41; +constexpr uint16_t kBridgeOperationRemoveSceneLevel = 42; +constexpr uint16_t kBridgeOperationGetSceneMap = 43; +constexpr uint16_t kBridgeOperationGetAddressSettings = 44; +constexpr uint16_t kBridgeOperationSetAddressSettings = 45; +constexpr uint16_t kBridgeOperationSearchAddressRange = 46; +constexpr uint16_t kBridgeOperationAllocateAllShortAddresses = 47; +constexpr uint16_t kBridgeOperationResetAndAllocateShortAddresses = 48; +constexpr uint16_t kBridgeOperationStopAddressAllocation = 49; +constexpr uint16_t kBridgeOperationSetColourRGB = 23; +constexpr uint16_t kBridgeOperationSetColourRGBW = 80; +constexpr uint16_t kBridgeOperationSetColourRGBCW = 81; +constexpr uint16_t kBridgeOperationSetColourRGBWAF = 82; +constexpr uint16_t kDaliCmdQueryStatus = 0x90; +constexpr uint16_t kDaliCmdQueryBallast = 0x91; constexpr const char* kBridgeTransportInvalidFrameResponse = "{\"statusCode\":400,\"error\":\"invalid bridge transport frame\"," "\"message\":\"invalid bridge transport frame\"}"; @@ -195,6 +298,138 @@ void AppendLe32(std::vector& out, uint32_t value) { out.push_back(static_cast((value >> 24) & 0xFF)); } +bool TickIsBefore(TickType_t lhs, TickType_t rhs) { + return static_cast(lhs - rhs) < 0; +} + +uint16_t ReadLe16(const uint8_t* data) { + return static_cast(data[0]) | (static_cast(data[1]) << 8); +} + +std::optional TlvUnsigned(const GatewayController::ParsedTlvMap& fields, + uint8_t field_id) { + const auto it = fields.find(field_id); + if (it == fields.end()) { + return std::nullopt; + } + const auto& field = it->second; + if (field.type == kTlvTypeBool && !field.value.empty()) { + return field.value[0] == 0 ? 0U : 1U; + } + if (field.type == kTlvTypeU8 && field.value.size() == 1) { + return field.value[0]; + } + if (field.type == kTlvTypeU16 && field.value.size() == 2) { + return ReadLe16(field.value.data()); + } + if ((field.type == kTlvTypeU32 || field.type == kTlvTypeI32) && field.value.size() == 4) { + return static_cast(field.value[0]) | + (static_cast(field.value[1]) << 8) | + (static_cast(field.value[2]) << 16) | + (static_cast(field.value[3]) << 24); + } + return std::nullopt; +} + +std::optional TlvSigned(const GatewayController::ParsedTlvMap& fields, + uint8_t field_id) { + const auto value = TlvUnsigned(fields, field_id); + if (!value.has_value()) { + return std::nullopt; + } + return static_cast(value.value()); +} + +int TlvIntOr(const GatewayController::ParsedTlvMap& fields, uint8_t field_id, int fallback) { + const auto value = TlvSigned(fields, field_id); + return value.has_value() ? static_cast(value.value()) : fallback; +} + +std::optional TlvInt(const GatewayController::ParsedTlvMap& fields, uint8_t field_id) { + const auto value = TlvSigned(fields, field_id); + if (!value.has_value()) { + return std::nullopt; + } + return static_cast(value.value()); +} + +bool TlvBoolOr(const GatewayController::ParsedTlvMap& fields, uint8_t field_id, bool fallback) { + const auto value = TlvUnsigned(fields, field_id); + return value.has_value() ? value.value() != 0 : fallback; +} + +void AppendTlvBytes(std::vector& out, uint8_t field_id, uint8_t type, + const uint8_t* data, size_t len) { + if (len > 255) { + len = 255; + } + out.push_back(field_id); + out.push_back(type); + out.push_back(static_cast(len)); + out.insert(out.end(), data, data + len); +} + +void AppendTlvU8(std::vector& out, uint8_t field_id, uint8_t value) { + AppendTlvBytes(out, field_id, kTlvTypeU8, &value, 1); +} + +void AppendTlvU16(std::vector& out, uint8_t field_id, uint16_t value) { + const uint8_t data[] = { + static_cast(value & 0xFF), + static_cast((value >> 8) & 0xFF), + }; + AppendTlvBytes(out, field_id, kTlvTypeU16, data, sizeof(data)); +} + +void AppendTlvI32(std::vector& out, uint8_t field_id, int32_t value) { + const auto raw = static_cast(value); + const uint8_t data[] = { + static_cast(raw & 0xFF), + static_cast((raw >> 8) & 0xFF), + static_cast((raw >> 16) & 0xFF), + static_cast((raw >> 24) & 0xFF), + }; + AppendTlvBytes(out, field_id, kTlvTypeI32, data, sizeof(data)); +} + +void AppendTlvBool(std::vector& out, uint8_t field_id, bool value) { + const uint8_t data = value ? 1 : 0; + AppendTlvBytes(out, field_id, kTlvTypeBool, &data, 1); +} + +void AppendTlvString(std::vector& out, uint8_t field_id, std::string_view value) { + AppendTlvBytes(out, field_id, kTlvTypeBytes, + reinterpret_cast(value.data()), value.size()); +} + +std::string IntVectorToString(const std::vector& values) { + std::ostringstream stream; + for (size_t index = 0; index < values.size(); ++index) { + if (index > 0) { + stream << ','; + } + stream << values[index]; + } + return stream.str(); +} + +void AppendSnapshotTlvs(std::vector& out, const DaliDomainSnapshot& snapshot) { + AppendTlvString(out, kTlvFieldKind, snapshot.kind); + AppendTlvI32(out, kTlvFieldAddress, snapshot.address); + for (const auto& [key, value] : snapshot.bools) { + AppendTlvString(out, kTlvFieldEntry, key + "=" + (value ? "true" : "false")); + } + for (const auto& [key, value] : snapshot.ints) { + AppendTlvString(out, kTlvFieldEntry, key + "=" + std::to_string(value)); + } + for (const auto& [key, value] : snapshot.numbers) { + AppendTlvString(out, kTlvFieldEntry, key + "=" + std::to_string(value)); + } + for (const auto& [key, value] : snapshot.int_arrays) { + AppendTlvString(out, kTlvFieldEntry, key + "=[" + IntVectorToString(value) + "]"); + } +} + void AppendFeatureBits(std::vector& out, uint16_t feature) { if (feature > 0xFF) { out.push_back(static_cast((feature >> 8) & 0xFF)); @@ -302,9 +537,14 @@ GatewayController::GatewayController(GatewayRuntime& runtime, DaliDomainService& cache_(cache), config_(config), maintenance_lock_(xSemaphoreCreateMutex()), - transaction_lock_(xSemaphoreCreateMutex()) {} + transaction_lock_(xSemaphoreCreateMutex()), + operation_lock_(xSemaphoreCreateMutex()) {} GatewayController::~GatewayController() { + if (operation_lock_ != nullptr) { + vSemaphoreDelete(operation_lock_); + operation_lock_ = nullptr; + } if (transaction_lock_ != nullptr) { vSemaphoreDelete(transaction_lock_); transaction_lock_ = nullptr; @@ -485,6 +725,15 @@ bool GatewayController::ipRouterEnabled() const { return ip_router_enabled_; } +bool GatewayController::rawReportingEnabled(uint8_t gateway_id) const { + LockGuard guard(operation_lock_); + const auto it = raw_report_leases_.find(gateway_id); + if (it == raw_report_leases_.end() || !it->second.enabled) { + return false; + } + return !TickIsBefore(it->second.expires_at, xTaskGetTickCount()); +} + GatewayControllerSnapshot GatewayController::snapshot() { GatewayControllerSnapshot out; out.setup_mode = setup_mode_; @@ -511,6 +760,20 @@ GatewayControllerSnapshot GatewayController::snapshot() { channel_snapshot.group_mask_high = group_high; channel_snapshot.allocating = dali_domain_.isAllocAddr(channel.gateway_id); channel_snapshot.last_alloc_addr = dali_domain_.lastAllocAddr(channel.gateway_id); + { + LockGuard guard(operation_lock_); + const auto operation_it = operation_states_.find(channel.gateway_id); + if (operation_it != operation_states_.end()) { + const auto& state = operation_it->second; + channel_snapshot.operation_active = state.active; + channel_snapshot.operation_request_id = state.request_id; + channel_snapshot.operation_id = state.operation_id; + channel_snapshot.operation_status = state.status; + channel_snapshot.operation_progress = state.progress; + channel_snapshot.operation_target = state.target; + channel_snapshot.operation_count = state.count; + } + } out.channels.push_back(std::move(channel_snapshot)); } return out; @@ -520,6 +783,18 @@ void GatewayController::TaskEntry(void* arg) { static_cast(arg)->taskLoop(); } +void GatewayController::OperationTaskEntry(void* arg) { + auto* context = static_cast(arg); + if (context == nullptr || context->controller == nullptr) { + delete context; + vTaskDelete(nullptr); + return; + } + context->controller->runOperationTask(context); + delete context; + vTaskDelete(nullptr); +} + void GatewayController::taskLoop() { while (true) { bool worked = false; @@ -605,7 +880,7 @@ bool GatewayController::runMaintenanceStep() { } if (has_job) { - if (runtime_.shouldYieldMaintenance(gateway_id)) { + if (operationActive(gateway_id) || runtime_.shouldYieldMaintenance(gateway_id)) { return false; } if (cacheMaintenanceSnoozed(gateway_id)) { @@ -742,7 +1017,8 @@ bool GatewayController::runCacheRefreshStep() { if (now < job.next_due_tick) { continue; } - if (runtime_.shouldYieldMaintenance(channel.gateway_id) || + if (operationActive(channel.gateway_id) || + runtime_.shouldYieldMaintenance(channel.gateway_id) || dali_domain_.isAllocAddr(channel.gateway_id) || cacheMaintenanceSnoozed(channel.gateway_id) || !dali_domain_.isBusIdle(channel.gateway_id, config_.cache_refresh_idle_ms)) { @@ -882,6 +1158,21 @@ void GatewayController::dispatchCommand(const std::vector& command) { if (!chip_level_command && IsDaliHostCommandOpcode(opcode)) { dali_domain_.markHostActivity(gateway_id); } + if (!chip_level_command && operationActive(gateway_id) && + opcode != kRawReportLeaseOpcode && opcode != kGatewayOperationOpcode && + !(opcode == 0x30 && (addr == 0 || addr == 3))) { + ESP_LOGW(kTag, "gateway=%u busy with operation, opcode=0x%02x rejected", gateway_id, + opcode); + if (opcode == 0x14) { + publishPayload(gateway_id, {0x04, gateway_id, 0x00}); + } else if (opcode == kDali103QueryOpcode) { + publishPayload(gateway_id, {kDali103NoResponseOpcode, gateway_id, 0x00}); + } else if (opcode == kGatewayCacheOpcode) { + publishPayload(gateway_id, {kGatewayCacheOpcode, gateway_id, command[4], + kGatewayCacheStatusInvalidArgument}); + } + return; + } switch (opcode) { case 0x00: @@ -941,7 +1232,8 @@ void GatewayController::dispatchCommand(const std::vector& command) { handleGatewayNameCommand(gateway_id, command); break; case 0x06: { - uint16_t feature = kGatewayFeatureNativeCpp; + uint16_t feature = kGatewayFeatureNativeCpp | kGatewayFeatureOperationProtocol | + kGatewayFeatureRawReportLease; if (setup_mode_ && config_.setup_supported) { feature |= 0x01; } @@ -1039,9 +1331,15 @@ void GatewayController::dispatchCommand(const std::vector& command) { break; case 0x31: break; - case 0x32: - dali_domain_.resetAndAllocAddr(gateway_id); + case 0x32: { + ParsedTlvMap fields; + fields[kTlvFieldStartAddress] = ParsedTlv{kTlvTypeU8, {0}}; + if (!startOperation(gateway_id, 0, kBridgeOperationResetAndAllocateShortAddresses, + std::move(fields))) { + ESP_LOGW(kTag, "legacy reset allocation opcode 0x32 rejected gateway=%u", gateway_id); + } break; + } case 0x37: if (command.size() >= 8) { int kelvin = command[5] * 256 + command[6]; @@ -1084,6 +1382,12 @@ void GatewayController::dispatchCommand(const std::vector& command) { } } break; + case kRawReportLeaseOpcode: + handleRawReportLeaseCommand(gateway_id, command); + break; + case kGatewayOperationOpcode: + handleGatewayOperationCommand(gateway_id, command); + break; case kGatewayCacheOpcode: handleGatewayCacheCommand(gateway_id, command); break; @@ -1198,6 +1502,79 @@ void GatewayController::publishBridgeTransportResponse(uint8_t gateway_id, uint8 } } +void GatewayController::publishRawReportLeaseResponse(uint8_t gateway_id, uint8_t status) { + bool enabled = false; + uint16_t ttl_seconds = 0; + { + LockGuard guard(operation_lock_); + const auto it = raw_report_leases_.find(gateway_id); + if (it != raw_report_leases_.end() && it->second.enabled && + !TickIsBefore(it->second.expires_at, xTaskGetTickCount())) { + enabled = true; + const TickType_t remaining_ticks = it->second.expires_at - xTaskGetTickCount(); + ttl_seconds = static_cast( + std::min(65535, remaining_ticks * portTICK_PERIOD_MS / 1000)); + } + } + publishPayload(gateway_id, {kRawReportLeaseOpcode, + gateway_id, + status, + static_cast(enabled ? 1 : 0), + static_cast(ttl_seconds & 0xFF), + static_cast((ttl_seconds >> 8) & 0xFF)}); +} + +void GatewayController::publishOperationEvent(uint8_t gateway_id, uint8_t request_id, + uint16_t operation_id, uint8_t event, + uint8_t status, uint8_t progress, + uint8_t target, uint8_t count) { + { + LockGuard guard(operation_lock_); + auto& state = operation_states_[gateway_id]; + state.request_id = request_id; + state.operation_id = operation_id; + state.status = status; + state.progress = progress; + state.target = target; + state.count = count; + } + publishPayload(gateway_id, {kGatewayOperationOpcode, + gateway_id, + request_id, + static_cast(operation_id & 0xFF), + static_cast((operation_id >> 8) & 0xFF), + event, + status, + progress, + target, + count}); +} + +void GatewayController::publishOperationResultChunks(uint8_t gateway_id, uint8_t request_id, + uint16_t operation_id, + const std::vector& tlv_payload) { + const size_t total = std::max( + 1, (tlv_payload.size() + kOperationResultMaxChunkBytes - 1) / + kOperationResultMaxChunkBytes); + const size_t capped_total = std::min(255, total); + for (size_t index = 0; index < capped_total; ++index) { + const size_t start = index * kOperationResultMaxChunkBytes; + const size_t chunk_len = + std::min(kOperationResultMaxChunkBytes, tlv_payload.size() - start); + std::vector payload{kGatewayOperationResultOpcode, + gateway_id, + request_id, + static_cast(operation_id & 0xFF), + static_cast((operation_id >> 8) & 0xFF), + static_cast(capped_total), + static_cast(index), + static_cast(chunk_len)}; + payload.insert(payload.end(), tlv_payload.begin() + start, + tlv_payload.begin() + start + chunk_len); + publishPayload(gateway_id, payload); + } +} + void GatewayController::publishFrame(const std::vector& frame) { captureTransactionFrame(frame); for (const auto& sink : notification_sinks_) { @@ -1230,10 +1607,15 @@ bool GatewayController::transactionFrameMatches(const TransactionWaiter& waiter, case 0x05: case 0x0A: case 0x30: + case kRawReportLeaseOpcode: case kGatewayCacheOpcode: case 0xA0: case 0xA2: return response_opcode == waiter.opcode && gateway_matches; + case kGatewayOperationOpcode: + return (response_opcode == kGatewayOperationOpcode || + response_opcode == kGatewayOperationResultOpcode) && + gateway_matches; case kBridgeTransportRequestOpcode: return response_opcode == kBridgeTransportResponseOpcode && gateway_matches; default: @@ -1500,7 +1882,8 @@ void GatewayController::handleDaliRawFrame(const DaliRawFrame& frame) { handleApplicationControllerFrame(frame); const bool maintenance_activity = maintenance_activity_gateway_.load() == frame.gateway_id; if (setup_mode_ || dali_domain_.isAllocAddr(frame.gateway_id) || maintenance_activity || - runtime_.hasActiveQueryCommand(frame.gateway_id)) { + runtime_.hasActiveQueryCommand(frame.gateway_id) || + !rawReportingEnabled(frame.gateway_id)) { return; } publishPayload(frame.gateway_id, @@ -1538,7 +1921,8 @@ void GatewayController::handleDaliRawFrame(const DaliRawFrame& frame) { } if (setup_mode_ || dali_domain_.isAllocAddr(frame.gateway_id) || maintenance_activity || - runtime_.hasActiveQueryCommand(frame.gateway_id)) { + runtime_.hasActiveQueryCommand(frame.gateway_id) || + !rawReportingEnabled(frame.gateway_id)) { return; } @@ -1955,11 +2339,21 @@ void GatewayController::handleAllocationCommand(uint8_t gateway_id, const uint8_t mode = command[4]; const uint8_t start_addr = command[5]; if (mode == 0) { - dali_domain_.stopAllocAddr(gateway_id); + abortOperation(gateway_id, 0); } else if (mode == 1) { - dali_domain_.allocateAllAddr(gateway_id, start_addr); + ParsedTlvMap fields; + fields[kTlvFieldStartAddress] = ParsedTlv{kTlvTypeU8, {start_addr}}; + if (!startOperation(gateway_id, 0, kBridgeOperationAllocateAllShortAddresses, + std::move(fields))) { + ESP_LOGW(kTag, "legacy allocation start rejected gateway=%u", gateway_id); + } } else if (mode == 2) { - dali_domain_.resetAndAllocAddr(gateway_id); + ParsedTlvMap fields; + fields[kTlvFieldStartAddress] = ParsedTlv{kTlvTypeU8, {start_addr}}; + if (!startOperation(gateway_id, 0, kBridgeOperationResetAndAllocateShortAddresses, + std::move(fields))) { + ESP_LOGW(kTag, "legacy reset allocation start rejected gateway=%u", gateway_id); + } } else if (mode == 3) { publishPayload(gateway_id, {0x30, gateway_id, static_cast(dali_domain_.isAllocAddr(gateway_id) ? 1 : 0), @@ -1967,6 +2361,723 @@ void GatewayController::handleAllocationCommand(uint8_t gateway_id, } } +bool GatewayController::operationActive(uint8_t gateway_id) const { + LockGuard guard(operation_lock_); + const auto it = operation_states_.find(gateway_id); + return it != operation_states_.end() && it->second.active; +} + +bool GatewayController::operationCancelRequested(uint8_t gateway_id, uint8_t request_id) const { + LockGuard guard(operation_lock_); + const auto it = operation_states_.find(gateway_id); + return it != operation_states_.end() && it->second.active && + it->second.request_id == request_id && it->second.cancel_requested; +} + +bool GatewayController::startOperation(uint8_t gateway_id, uint8_t request_id, + uint16_t operation_id, ParsedTlvMap fields) { + { + LockGuard guard(operation_lock_); + auto& state = operation_states_[gateway_id]; + if (state.active) { + return false; + } + state.active = true; + state.cancel_requested = false; + state.request_id = request_id; + state.operation_id = operation_id; + state.status = kOperationStatusOk; + state.progress = 0; + state.target = 0; + state.count = 0; + } + + auto* context = new GatewayOperationTaskContext(); + context->controller = this; + context->gateway_id = gateway_id; + context->request_id = request_id; + context->operation_id = operation_id; + context->fields = std::move(fields); + const BaseType_t created = xTaskCreate(&GatewayController::OperationTaskEntry, "gateway_op", + config_.task_stack_size, context, + config_.task_priority, nullptr); + if (created != pdPASS) { + delete context; + { + LockGuard guard(operation_lock_); + auto& state = operation_states_[gateway_id]; + state.active = false; + state.status = kOperationStatusFailed; + } + return false; + } + return true; +} + +void GatewayController::abortOperation(uint8_t gateway_id, uint8_t request_id) { + uint16_t operation_id = 0; + bool had_active_operation = false; + { + LockGuard guard(operation_lock_); + auto it = operation_states_.find(gateway_id); + if (it != operation_states_.end() && it->second.active && + (request_id == 0 || request_id == it->second.request_id)) { + it->second.cancel_requested = true; + it->second.status = kOperationStatusAborted; + operation_id = it->second.operation_id; + request_id = it->second.request_id; + had_active_operation = true; + } + } + dali_domain_.stopAllocAddr(gateway_id); + if (!had_active_operation) { + publishOperationEvent(gateway_id, request_id, 0, kOperationEventAborted, + kOperationStatusOk, 100, 0, 0); + } else { + publishOperationEvent(gateway_id, request_id, operation_id, kOperationEventAborted, + kOperationStatusAborted, 100, 0, 0); + } +} + +void GatewayController::finishOperation(uint8_t gateway_id, uint8_t request_id, uint8_t status, + uint8_t progress, uint8_t target, uint8_t count) { + LockGuard guard(operation_lock_); + auto it = operation_states_.find(gateway_id); + if (it == operation_states_.end() || it->second.request_id != request_id) { + return; + } + it->second.active = false; + it->second.cancel_requested = false; + it->second.status = status; + it->second.progress = progress; + it->second.target = target; + it->second.count = count; +} + +GatewayController::ParsedTlvMap GatewayController::parseOperationTlvs(const uint8_t* data, + size_t len, + bool* ok) const { + ParsedTlvMap fields; + size_t offset = 0; + bool valid = true; + while (offset < len) { + if (offset + 3 > len) { + valid = false; + break; + } + const uint8_t field_id = data[offset++]; + const uint8_t type = data[offset++]; + const uint8_t field_len = data[offset++]; + if (offset + field_len > len) { + valid = false; + break; + } + ParsedTlv parsed; + parsed.type = type; + parsed.value.assign(data + offset, data + offset + field_len); + fields[field_id] = std::move(parsed); + offset += field_len; + } + if (ok != nullptr) { + *ok = valid; + } + return fields; +} + +void GatewayController::handleRawReportLeaseCommand(uint8_t gateway_id, + const std::vector& command) { + if (command.size() < 7) { + publishRawReportLeaseResponse(gateway_id, kOperationStatusInvalid); + return; + } + const uint8_t op = command[4]; + if (op == 0x00) { + publishRawReportLeaseResponse(gateway_id, kOperationStatusOk); + return; + } + if (op != 0x01 || command.size() < 9) { + publishRawReportLeaseResponse(gateway_id, kOperationStatusInvalid); + return; + } + + const bool enabled = command[5] != 0; + const uint16_t ttl_seconds = ReadLe16(&command[6]); + { + LockGuard guard(operation_lock_); + auto& lease = raw_report_leases_[gateway_id]; + lease.enabled = enabled && ttl_seconds > 0; + lease.expires_at = xTaskGetTickCount() + pdMS_TO_TICKS(static_cast(ttl_seconds) * 1000U); + } + publishRawReportLeaseResponse(gateway_id, kOperationStatusOk); +} + +void GatewayController::handleGatewayOperationCommand(uint8_t gateway_id, + const std::vector& command) { + if (command.size() < 11) { + const uint8_t request_id = command.size() > 5 ? command[5] : 0; + publishOperationEvent(gateway_id, request_id, 0, kOperationEventError, + kOperationStatusInvalid, 0, 0, 0); + return; + } + + const uint8_t verb = command[4]; + const uint8_t request_id = command[5]; + const uint16_t operation_id = ReadLe16(&command[6]); + if (verb == kOperationVerbAbort) { + abortOperation(gateway_id, request_id); + return; + } + if (verb == kOperationVerbStatus) { + GatewayOperationRuntimeState state; + { + LockGuard guard(operation_lock_); + const auto it = operation_states_.find(gateway_id); + if (it != operation_states_.end()) { + state = it->second; + } else { + state.request_id = request_id; + state.operation_id = operation_id; + state.progress = 100; + } + } + publishOperationEvent(gateway_id, state.request_id, state.operation_id, + state.active ? kOperationEventProgress : kOperationEventCompleted, + state.status, state.progress, state.target, state.count); + return; + } + if (verb != kOperationVerbStart) { + publishOperationEvent(gateway_id, request_id, operation_id, kOperationEventError, + kOperationStatusInvalid, 0, 0, 0); + return; + } + + const uint16_t payload_len = ReadLe16(&command[8]); + const size_t available_len = command.size() - 11; + if (payload_len != available_len) { + publishOperationEvent(gateway_id, request_id, operation_id, kOperationEventError, + kOperationStatusInvalid, 0, 0, 0); + return; + } + + bool ok = false; + auto fields = parseOperationTlvs(command.data() + 10, payload_len, &ok); + if (!ok) { + publishOperationEvent(gateway_id, request_id, operation_id, kOperationEventError, + kOperationStatusInvalid, 0, 0, 0); + return; + } + + if (!startOperation(gateway_id, request_id, operation_id, std::move(fields))) { + publishOperationEvent(gateway_id, request_id, operation_id, kOperationEventError, + kOperationStatusBusy, 0, 0, 0); + } +} + +void GatewayController::runOperationTask(GatewayOperationTaskContext* context) { + const uint8_t gateway_id = context->gateway_id; + const uint8_t request_id = context->request_id; + const uint16_t operation_id = context->operation_id; + const auto& fields = context->fields; + std::vector result_tlvs; + uint8_t status = kOperationStatusOk; + uint8_t progress = 0; + uint8_t target_byte = 0; + uint8_t count = 0; + + auto fail = [&](uint8_t fail_status) { + status = fail_status; + progress = 100; + }; + auto canceled = [&]() { + return operationCancelRequested(gateway_id, request_id); + }; + auto update_progress = [&](uint8_t event, uint8_t next_progress, uint8_t target, + uint8_t next_count, uint8_t next_status = kOperationStatusOk) { + progress = next_progress; + target_byte = target; + count = next_count; + publishOperationEvent(gateway_id, request_id, operation_id, event, next_status, + next_progress, target, next_count); + }; + + publishOperationEvent(gateway_id, request_id, operation_id, kOperationEventAccepted, + kOperationStatusOk, 0, 0, 0); + dali_domain_.markHostActivity(gateway_id); + + const int target = TlvIntOr(fields, kTlvFieldTarget, 0); + target_byte = static_cast(std::clamp(target, 0, 255)); + + if (canceled()) { + fail(kOperationStatusAborted); + } else { + switch (operation_id) { + case kBridgeOperationSend: + case kBridgeOperationSendExt: { + const auto command = TlvUnsigned(fields, kTlvFieldCommand); + const auto raw_addr = TlvUnsigned(fields, kTlvFieldRawAddress) + .value_or(rawCommandAddressFromDec(target)); + if (!command.has_value() || raw_addr > 255) { + fail(kOperationStatusInvalid); + break; + } + const bool ok = operation_id == kBridgeOperationSend + ? sendRawAndMirror(gateway_id, static_cast(raw_addr), + static_cast(command.value())) + : sendExtRawAndMirror(gateway_id, static_cast(raw_addr), + static_cast(command.value())); + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvBool(result_tlvs, kTlvFieldResult, ok); + progress = 100; + break; + } + case kBridgeOperationQuery: { + const auto command = TlvUnsigned(fields, kTlvFieldCommand); + const auto raw_addr = TlvUnsigned(fields, kTlvFieldRawAddress) + .value_or(rawCommandAddressFromDec(target)); + if (!command.has_value() || raw_addr > 255) { + fail(kOperationStatusInvalid); + break; + } + const auto value = dali_domain_.queryRaw(gateway_id, static_cast(raw_addr), + static_cast(command.value())); + if (!value.has_value()) { + fail(kOperationStatusNoResponse); + break; + } + AppendTlvU8(result_tlvs, kTlvFieldResult, value.value()); + progress = 100; + break; + } + case kBridgeOperationSetBrightness: + case kBridgeOperationSetBrightnessPercent: { + auto value = TlvInt(fields, kTlvFieldValue); + if (!value.has_value()) { + fail(kOperationStatusInvalid); + break; + } + int level = value.value(); + if (operation_id == kBridgeOperationSetBrightnessPercent) { + level = std::clamp(level, 0, 100) * 254 / 100; + } + const bool ok = setBrightAndMirror(gateway_id, target, static_cast(level)); + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvI32(result_tlvs, kTlvFieldResult, level); + progress = 100; + break; + } + case kBridgeOperationOn: + case kBridgeOperationRecallMaxLevel: { + const bool ok = onAndMirror(gateway_id, target); + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvBool(result_tlvs, kTlvFieldResult, ok); + progress = 100; + break; + } + case kBridgeOperationOff: { + const bool ok = offAndMirror(gateway_id, target); + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvBool(result_tlvs, kTlvFieldResult, ok); + progress = 100; + break; + } + case kBridgeOperationRecallMinLevel: { + const bool ok = sendRawAndMirror(gateway_id, rawCommandAddressFromDec(target), 0x06); + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvBool(result_tlvs, kTlvFieldResult, ok); + progress = 100; + break; + } + case kBridgeOperationSetColorTemperature: + case kBridgeOperationSetColorTemperatureRaw: { + const auto value = TlvInt(fields, kTlvFieldValue); + if (!value.has_value()) { + fail(kOperationStatusInvalid); + break; + } + int kelvin_or_mirek = value.value(); + if (operation_id == kBridgeOperationSetColorTemperature && + config_.color_temperature_max < config_.color_temperature_min) { + kelvin_or_mirek = reverseInRange(kelvin_or_mirek, config_.color_temperature_min, + config_.color_temperature_max); + } + const bool ok = operation_id == kBridgeOperationSetColorTemperature + ? dali_domain_.setColTemp(gateway_id, target, kelvin_or_mirek) + : dali_domain_.setColTempRaw(gateway_id, target, kelvin_or_mirek); + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvI32(result_tlvs, kTlvFieldResult, kelvin_or_mirek); + progress = 100; + break; + } + case kBridgeOperationSetColourXY: { + const auto x = TlvInt(fields, kTlvFieldX); + const auto y = TlvInt(fields, kTlvFieldY); + if (!x.has_value() || !y.has_value()) { + fail(kOperationStatusInvalid); + break; + } + const int raw_addr = TlvIntOr(fields, kTlvFieldRawAddress, rawCommandAddressFromDec(target)); + const bool ok = dali_domain_.setColourRaw(gateway_id, raw_addr, x.value(), y.value()); + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvBool(result_tlvs, kTlvFieldResult, ok); + progress = 100; + break; + } + case kBridgeOperationSetColourRGB: + case kBridgeOperationSetColourRGBW: + case kBridgeOperationSetColourRGBCW: + case kBridgeOperationSetColourRGBWAF: { + const int r = TlvIntOr(fields, kTlvFieldRed, -1); + const int g = TlvIntOr(fields, kTlvFieldGreen, -1); + const int b = TlvIntOr(fields, kTlvFieldBlue, -1); + if (r < 0 || g < 0 || b < 0) { + fail(kOperationStatusInvalid); + break; + } + bool ok = false; + if (operation_id == kBridgeOperationSetColourRGB) { + ok = dali_domain_.setColourRGB(gateway_id, target, r, g, b); + } else if (operation_id == kBridgeOperationSetColourRGBW) { + const int w = TlvIntOr(fields, kTlvFieldWhite, -1); + ok = w >= 0 && dali_domain_.setColourRGBW(gateway_id, target, r, g, b, w); + } else if (operation_id == kBridgeOperationSetColourRGBCW) { + const int cw = TlvIntOr(fields, kTlvFieldCoolWhite, -1); + const int ww = TlvIntOr(fields, kTlvFieldWarmWhite, -1); + ok = cw >= 0 && ww >= 0 && + dali_domain_.setColourRGBCW(gateway_id, target, r, g, b, cw, ww); + } else { + const int w = TlvIntOr(fields, kTlvFieldWhite, -1); + ok = w >= 0 && + dali_domain_.setColourRGBWAF( + gateway_id, target, r, g, b, w, TlvIntOr(fields, kTlvFieldAmber, 255), + TlvIntOr(fields, kTlvFieldFreeColour, 255), + TlvIntOr(fields, kTlvFieldControl, -1)); + } + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvBool(result_tlvs, kTlvFieldResult, ok); + progress = 100; + break; + } + case kBridgeOperationGetBrightness: { + const auto value = dali_domain_.queryActualLevel(gateway_id, target); + if (!value.has_value()) { + fail(kOperationStatusNoResponse); + break; + } + AppendTlvU8(result_tlvs, kTlvFieldResult, value.value()); + progress = 100; + break; + } + case kBridgeOperationGetStatus: { + const auto value = dali_domain_.queryRaw(gateway_id, rawCommandAddressFromDec(target), + kDaliCmdQueryStatus); + if (!value.has_value()) { + fail(kOperationStatusNoResponse); + break; + } + AppendTlvU8(result_tlvs, kTlvFieldResult, value.value()); + progress = 100; + break; + } + case kBridgeOperationGetColorStatus: + case kBridgeOperationGetDt8StatusSnapshot: { + const auto snapshot = dali_domain_.dt8StatusSnapshot(gateway_id, target); + if (!snapshot.has_value()) { + fail(kOperationStatusNoResponse); + break; + } + AppendSnapshotTlvs(result_tlvs, snapshot.value()); + progress = 100; + break; + } + case kBridgeOperationGetDt1Snapshot: + case kBridgeOperationGetDt4Snapshot: + case kBridgeOperationGetDt5Snapshot: + case kBridgeOperationGetDt6Snapshot: { + std::optional snapshot; + if (operation_id == kBridgeOperationGetDt1Snapshot) { + snapshot = dali_domain_.dt1Snapshot(gateway_id, target); + } else if (operation_id == kBridgeOperationGetDt4Snapshot) { + snapshot = dali_domain_.dt4Snapshot(gateway_id, target); + } else if (operation_id == kBridgeOperationGetDt5Snapshot) { + snapshot = dali_domain_.dt5Snapshot(gateway_id, target); + } else { + snapshot = dali_domain_.dt6Snapshot(gateway_id, target); + } + if (!snapshot.has_value()) { + fail(kOperationStatusNoResponse); + break; + } + AppendSnapshotTlvs(result_tlvs, snapshot.value()); + progress = 100; + break; + } + case kBridgeOperationGetGroupMask: { + const auto value = dali_domain_.queryGroupMask(gateway_id, target); + if (!value.has_value()) { + fail(kOperationStatusNoResponse); + break; + } + AppendTlvU16(result_tlvs, kTlvFieldGroupMask, value.value()); + progress = 100; + break; + } + case kBridgeOperationSetGroupMask: { + const auto value = TlvUnsigned(fields, kTlvFieldGroupMask) + .value_or(TlvUnsigned(fields, kTlvFieldValue).value_or(0)); + const bool ok = dali_domain_.applyGroupMask(gateway_id, target, + static_cast(value & 0xFFFF)); + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvU16(result_tlvs, kTlvFieldGroupMask, static_cast(value & 0xFFFF)); + progress = 100; + break; + } + case kBridgeOperationGetSceneLevel: { + const int scene = TlvIntOr(fields, kTlvFieldScene, -1); + if (scene < 0 || scene >= kDaliSceneCount) { + fail(kOperationStatusInvalid); + break; + } + const auto value = dali_domain_.querySceneLevel(gateway_id, target, scene); + if (!value.has_value()) { + fail(kOperationStatusNoResponse); + break; + } + AppendTlvU8(result_tlvs, kTlvFieldScene, static_cast(scene)); + AppendTlvU8(result_tlvs, kTlvFieldResult, value.value()); + progress = 100; + break; + } + case kBridgeOperationSetSceneLevel: + case kBridgeOperationRemoveSceneLevel: { + const int scene = TlvIntOr(fields, kTlvFieldScene, -1); + if (scene < 0 || scene >= kDaliSceneCount) { + fail(kOperationStatusInvalid); + break; + } + std::optional level; + if (operation_id == kBridgeOperationSetSceneLevel) { + const auto value = TlvUnsigned(fields, kTlvFieldValue); + if (!value.has_value()) { + fail(kOperationStatusInvalid); + break; + } + level = static_cast(value.value() & 0xFF); + } + const bool ok = dali_domain_.applySceneLevel(gateway_id, target, scene, level); + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvBool(result_tlvs, kTlvFieldResult, ok); + progress = 100; + break; + } + case kBridgeOperationGetSceneMap: { + for (uint8_t scene = 0; scene < kDaliSceneCount; ++scene) { + if (canceled()) { + fail(kOperationStatusAborted); + break; + } + const auto value = dali_domain_.querySceneLevel(gateway_id, target, scene); + if (value.has_value()) { + AppendTlvString(result_tlvs, kTlvFieldEntry, + std::to_string(scene) + "=" + std::to_string(value.value())); + ++count; + } + update_progress(kOperationEventItemResult, + static_cast((scene + 1) * 100 / kDaliSceneCount), scene, + count, value.has_value() ? kOperationStatusOk + : kOperationStatusNoResponse); + } + progress = 100; + break; + } + case kBridgeOperationGetAddressSettings: { + const auto settings = dali_domain_.queryAddressSettings(gateway_id, target); + if (!settings.has_value()) { + fail(kOperationStatusNoResponse); + break; + } + if (settings->power_on_level.has_value()) { + AppendTlvU8(result_tlvs, kTlvFieldPowerOnLevel, settings->power_on_level.value()); + } + if (settings->system_failure_level.has_value()) { + AppendTlvU8(result_tlvs, kTlvFieldSystemFailureLevel, + settings->system_failure_level.value()); + } + if (settings->min_level.has_value()) { + AppendTlvU8(result_tlvs, kTlvFieldMinLevel, settings->min_level.value()); + } + if (settings->max_level.has_value()) { + AppendTlvU8(result_tlvs, kTlvFieldMaxLevel, settings->max_level.value()); + } + if (settings->fade_time.has_value()) { + AppendTlvU8(result_tlvs, kTlvFieldFadeTime, settings->fade_time.value()); + } + if (settings->fade_rate.has_value()) { + AppendTlvU8(result_tlvs, kTlvFieldFadeRate, settings->fade_rate.value()); + } + progress = 100; + break; + } + case kBridgeOperationSetAddressSettings: { + DaliAddressSettingsSnapshot settings; + if (auto value = TlvUnsigned(fields, kTlvFieldPowerOnLevel)) { + settings.power_on_level = static_cast(value.value() & 0xFF); + } + if (auto value = TlvUnsigned(fields, kTlvFieldSystemFailureLevel)) { + settings.system_failure_level = static_cast(value.value() & 0xFF); + } + if (auto value = TlvUnsigned(fields, kTlvFieldMinLevel)) { + settings.min_level = static_cast(value.value() & 0xFF); + } + if (auto value = TlvUnsigned(fields, kTlvFieldMaxLevel)) { + settings.max_level = static_cast(value.value() & 0xFF); + } + if (auto value = TlvUnsigned(fields, kTlvFieldFadeTime)) { + settings.fade_time = static_cast(value.value() & 0xFF); + } + if (auto value = TlvUnsigned(fields, kTlvFieldFadeRate)) { + settings.fade_rate = static_cast(value.value() & 0xFF); + } + if (!settings.anyKnown()) { + fail(kOperationStatusInvalid); + break; + } + const bool ok = dali_domain_.applyAddressSettings(gateway_id, target, settings); + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvBool(result_tlvs, kTlvFieldResult, ok); + progress = 100; + break; + } + case kBridgeOperationSearchAddressRange: { + const int start = std::clamp(TlvIntOr(fields, kTlvFieldStartAddress, 0), 0, 63); + const int end = std::clamp(TlvIntOr(fields, kTlvFieldEndAddress, 63), start, 63); + const int total = std::max(1, end - start + 1); + for (int address = start; address <= end; ++address) { + if (canceled()) { + fail(kOperationStatusAborted); + break; + } + const auto value = dali_domain_.queryRaw(gateway_id, rawCommandAddressFromDec(address), + kDaliCmdQueryBallast); + const bool online = value.has_value() && value.value() == 0xFF; + if (online) { + AppendTlvU8(result_tlvs, kTlvFieldEntry, static_cast(address)); + ++count; + } + update_progress(kOperationEventItemResult, + static_cast((address - start + 1) * 100 / total), + static_cast(address), count, + online ? kOperationStatusOk : kOperationStatusNoResponse); + } + progress = 100; + break; + } + case kBridgeOperationAllocateAllShortAddresses: + case kBridgeOperationResetAndAllocateShortAddresses: + case kBridgeOperationStopAddressAllocation: { + if (operation_id == kBridgeOperationStopAddressAllocation) { + dali_domain_.stopAllocAddr(gateway_id); + AppendTlvBool(result_tlvs, kTlvFieldResult, true); + progress = 100; + break; + } + update_progress(kOperationEventProgress, 1, 0, 0); + const int start = TlvIntOr(fields, kTlvFieldStartAddress, 0); + const bool ok = operation_id == kBridgeOperationAllocateAllShortAddresses + ? dali_domain_.allocateAllAddr(gateway_id, start) + : dali_domain_.resetAndAllocAddr( + gateway_id, start, + TlvBoolOr(fields, kTlvFieldRemoveAddressFirst, false), + TlvBoolOr(fields, kTlvFieldCloseLight, false)); + if (canceled()) { + fail(kOperationStatusAborted); + } else if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvI32(result_tlvs, kTlvFieldResult, dali_domain_.lastAllocAddr(gateway_id)); + progress = 100; + target_byte = static_cast(dali_domain_.lastAllocAddr(gateway_id) & 0xFF); + break; + } + case kBridgeOperationStoreDt8PowerOnLevelSnapshot: + case kBridgeOperationStoreDt8SystemFailureLevelSnapshot: { + const auto level = TlvInt(fields, kTlvFieldValue); + if (!level.has_value()) { + fail(kOperationStatusInvalid); + break; + } + const bool ok = operation_id == kBridgeOperationStoreDt8PowerOnLevelSnapshot + ? dali_domain_.storeDt8PowerOnLevelSnapshot(gateway_id, target, + level.value()) + : dali_domain_.storeDt8SystemFailureLevelSnapshot(gateway_id, target, + level.value()); + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvBool(result_tlvs, kTlvFieldResult, ok); + progress = 100; + break; + } + case kBridgeOperationStoreDt8SceneSnapshot: { + const int scene = TlvIntOr(fields, kTlvFieldScene, -1); + const int brightness = TlvIntOr(fields, kTlvFieldValue, -1); + if (scene < 0 || scene >= kDaliSceneCount || brightness < 0) { + fail(kOperationStatusInvalid); + break; + } + const bool ok = dali_domain_.storeDt8SceneSnapshot( + gateway_id, target, scene, brightness, DaliDt8SceneColorMode::kDisabled); + if (!ok) { + fail(kOperationStatusFailed); + } + AppendTlvBool(result_tlvs, kTlvFieldResult, ok); + progress = 100; + break; + } + case kBridgeOperationGetColorTemperature: + default: + fail(kOperationStatusUnsupported); + break; + } + } + + if (status == kOperationStatusOk && canceled()) { + status = kOperationStatusAborted; + } + + const uint8_t final_event = status == kOperationStatusOk + ? kOperationEventCompleted + : status == kOperationStatusAborted ? kOperationEventAborted + : kOperationEventError; + if (!result_tlvs.empty()) { + publishOperationResultChunks(gateway_id, request_id, operation_id, result_tlvs); + } + finishOperation(gateway_id, request_id, status, 100, target_byte, count); + publishOperationEvent(gateway_id, request_id, operation_id, final_event, status, 100, + target_byte, count); +} + void GatewayController::handleInternalSceneCommand(uint8_t gateway_id, const std::vector& command) { if (command.size() < 7) { diff --git a/components/gateway_network/src/gateway_network.cpp b/components/gateway_network/src/gateway_network.cpp index 6e76e35..bc8e1a5 100644 --- a/components/gateway_network/src/gateway_network.cpp +++ b/components/gateway_network/src/gateway_network.cpp @@ -1298,6 +1298,9 @@ void GatewayNetworkService::handleDaliRawFrame(const DaliRawFrame& frame) { if (!espnow_started_ || !espnow_connected_ || frame.data.empty()) { return; } + if (!controller_.rawReportingEnabled(frame.gateway_id)) { + return; + } cJSON* payload = cJSON_CreateObject(); if (payload == nullptr) { @@ -1606,6 +1609,13 @@ std::string GatewayNetworkService::gatewaySnapshotJson() { cJSON_AddNumberToObject(item, "groupMaskHigh", channel.group_mask_high); cJSON_AddBoolToObject(item, "isAllocAddr", channel.allocating); cJSON_AddNumberToObject(item, "lastAllocAddr", channel.last_alloc_addr); + cJSON_AddBoolToObject(item, "operationActive", channel.operation_active); + cJSON_AddNumberToObject(item, "operationRequestId", channel.operation_request_id); + cJSON_AddNumberToObject(item, "operationId", channel.operation_id); + cJSON_AddNumberToObject(item, "operationStatus", channel.operation_status); + cJSON_AddNumberToObject(item, "operationProgress", channel.operation_progress); + cJSON_AddNumberToObject(item, "operationTarget", channel.operation_target); + cJSON_AddNumberToObject(item, "operationCount", channel.operation_count); cJSON_AddItemToArray(channels, item); } cJSON_AddItemToObject(root, "channels", channels); @@ -1757,4 +1767,4 @@ esp_err_t GatewayNetworkService::HandleJqJsGet(httpd_req_t* req) { return httpd_resp_send_chunk(req, nullptr, 0); } -} // namespace gateway \ No newline at end of file +} // namespace gateway diff --git a/components/gateway_runtime/src/gateway_runtime.cpp b/components/gateway_runtime/src/gateway_runtime.cpp index 74565d0..494da48 100644 --- a/components/gateway_runtime/src/gateway_runtime.cpp +++ b/components/gateway_runtime/src/gateway_runtime.cpp @@ -370,7 +370,8 @@ GatewayRuntime::CommandPriority GatewayRuntime::classifyCommandPriority( if (opcode == 0x00 || opcode == 0x01 || opcode == 0x03 || opcode == 0x04 || opcode == 0x07 || opcode == 0x08 || opcode == 0x10 || opcode == 0x11 || opcode == 0x12 || opcode == 0x13 || opcode == 0x0B || opcode == 0x17 || opcode == 0x18 || opcode == 0x37 || opcode == 0x38 || - opcode == 0x60 || opcode == 0x61 || opcode == 0x62 || (opcode == 0x30 && addr == 0)) { + opcode == 0x60 || opcode == 0x61 || opcode == 0x62 || opcode == 0x66 || opcode == 0x67 || + (opcode == 0x30 && addr == 0)) { return CommandPriority::kControl; } return CommandPriority::kNormal;