Add diagnostic bit support to Gateway Modbus
- Introduced new enum value `kShortDiagnosticBit` to `GatewayModbusGeneratedKind`. - Enhanced `GatewayModbusPoint` and `GatewayModbusPointBinding` structures to include diagnostic snapshot, boolean key, and device type. - Added new diagnostic bit specifications and updated the corresponding arrays for generated discrete inputs and holding registers. - Implemented `addGeneratedDiagnosticPoint` function to handle the creation of diagnostic points. - Updated `rebuildMap` method to include generated diagnostic points during the map rebuilding process. Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -13,7 +13,7 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
|
||||
- `gateway_cache/`: DALI scene/group/settings/runtime cache used by controller reconciliation and protocol bridges.
|
||||
- `gateway_bridge/`: per-channel bridge provisioning, command execution, protocol startup, and HTTP bridge actions.
|
||||
- `gateway_modbus/`: gateway-owned Modbus TCP config, generated DALI point tables, and provisioned Modbus model override dispatch.
|
||||
- `gateway_bacnet/`: BACnet/IP server adapter backed by bacnet-stack.
|
||||
- `gateway_bacnet/`: BACnet/IP server adapter backed by bacnet-stack, including the gateway-owned BACnet bridge model adapter.
|
||||
- `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`, including raw DALI notifications.
|
||||
- `gateway_controller/`: Lua-compatible gateway command dispatcher, internal scene/group state, and notification fan-out.
|
||||
- `gateway_network/`: HTTP `/info`, `/dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP port `2020` command/notify routing, Wi-Fi STA lifecycle, ESP-Touch smartconfig, setup AP mode, ESP-NOW setup ingress, and BOOT-button Wi-Fi reset for the native gateway.
|
||||
@@ -36,3 +36,23 @@ The first generated map slice creates stable points for every DALI short address
|
||||
- Input registers: read-only inventory state, primary type, type mask, cached actual level, scene id, raw status placeholder, group mask, and cached settings.
|
||||
|
||||
Unknown numeric values read as `0xFFFF`; booleans read as false unless inventory or cache state proves otherwise. Provisioned Modbus models still work as overrides at their configured Modbus point, and normal generated reads prefer gateway cache state to avoid DALI bus polling.
|
||||
|
||||
An extension discrete-input range starts immediately after the legacy `0-63` short-address block. It publishes decoded DALI status, failure, and feature bits as individual booleans for base status, DT1 emergency, DT4/5/6 control-gear feature/failure status, and DT8 color status/features. This keeps existing Modbus addresses stable while making bit-level diagnostics readable without consumers masking packed status registers.
|
||||
|
||||
## BACnet/IP
|
||||
|
||||
BACnet/IP is owned by `gateway/components/gateway_bacnet` and is started through the per-channel bridge service. Runtime BACnet server settings live under top-level `bacnetServer` in bridge config:
|
||||
|
||||
```json
|
||||
{
|
||||
"bacnetServer": {
|
||||
"deviceInstance": 4194303,
|
||||
"localAddress": "",
|
||||
"udpPort": 47808
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Provisioned BACnet models still use generic `BridgeModel` fields such as object type, object instance, property, and optional `bitIndex`. Query-style models refresh BACnet `Present_Value` from live DALI reads, and binary models with `bitIndex` expose a single packed status bit.
|
||||
|
||||
For discovered DALI short addresses, the gateway also mirrors the generated Modbus discrete diagnostics as BACnet binary-input objects. Object instances are allocated in a gateway-owned generated range using the channel index plus the generated Modbus discrete-input offset, so generated objects stay deterministic while avoiding the provisioned-object address space in normal deployments.
|
||||
@@ -134,9 +134,14 @@ class DaliDomainService {
|
||||
std::optional<DaliDomainSnapshot> discoverDeviceTypes(
|
||||
uint8_t gateway_id, int short_address, const std::vector<int>& fallback_types = {},
|
||||
int max_next_types = 16) const;
|
||||
std::optional<DaliDomainSnapshot> baseStatusSnapshot(uint8_t gateway_id,
|
||||
int short_address) const;
|
||||
std::optional<DaliDomainSnapshot> dt1Snapshot(uint8_t gateway_id, int short_address) const;
|
||||
std::optional<DaliDomainSnapshot> dt4Snapshot(uint8_t gateway_id, int short_address) const;
|
||||
std::optional<DaliDomainSnapshot> dt5Snapshot(uint8_t gateway_id, int short_address) const;
|
||||
std::optional<DaliDomainSnapshot> dt6Snapshot(uint8_t gateway_id, int short_address) const;
|
||||
std::optional<DaliDomainSnapshot> dt8StatusSnapshot(uint8_t gateway_id,
|
||||
int short_address) const;
|
||||
std::optional<DaliDomainSnapshot> dt8SceneColorReport(uint8_t gateway_id, int short_address,
|
||||
int scene) const;
|
||||
std::optional<DaliDomainSnapshot> dt8PowerOnLevelColorReport(uint8_t gateway_id,
|
||||
|
||||
@@ -466,6 +466,113 @@ std::optional<DaliDomainSnapshot> DaliDomainService::discoverDeviceTypes(
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
std::optional<DaliDomainSnapshot> DaliDomainService::baseStatusSnapshot(
|
||||
uint8_t gateway_id, int short_address) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto raw_status = channel->dali->base.getStatus(short_address);
|
||||
if (!raw_status.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const DaliStatus status = DaliStatus::fromByte(static_cast<uint8_t>(raw_status.value()));
|
||||
auto snapshot = MakeSnapshot(gateway_id, short_address, "base_status");
|
||||
snapshot.ints["rawStatus"] = raw_status.value() & 0xFF;
|
||||
snapshot.bools["controlGearPresent"] = status.controlGearPresent;
|
||||
snapshot.bools["lampFailure"] = status.lampFailure;
|
||||
snapshot.bools["lampPowerOn"] = status.lampPowerOn;
|
||||
snapshot.bools["limitError"] = status.limitError;
|
||||
snapshot.bools["fadingCompleted"] = status.fadingCompleted;
|
||||
snapshot.bools["resetState"] = status.resetState;
|
||||
snapshot.bools["missingShortAddress"] = status.missingShortAddress;
|
||||
snapshot.bools["powerSupplyFault"] = status.psFault;
|
||||
snapshot.bools["psFault"] = status.psFault;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
std::optional<DaliDomainSnapshot> DaliDomainService::dt1Snapshot(uint8_t gateway_id,
|
||||
int short_address) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto detailed = channel->dali->dt1.getDT1TestStatusDetailed(short_address);
|
||||
if (!detailed.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt1");
|
||||
PutOptionalInt(snapshot, "failureStatusRaw", detailed->failureStatus);
|
||||
PutOptionalInt(snapshot, "emergencyStatusRaw", detailed->emergencyStatus);
|
||||
PutOptionalInt(snapshot, "emergencyModeRaw", detailed->emergencyMode);
|
||||
PutOptionalInt(snapshot, "featuresRaw", detailed->feature);
|
||||
PutOptionalInt(snapshot, "deviceStatusRaw", detailed->deviceStatus);
|
||||
|
||||
snapshot.bools["testInProgress"] = detailed->testInProgress;
|
||||
snapshot.bools["lampFailure"] = detailed->lampFailure;
|
||||
snapshot.bools["batteryFailure"] = detailed->batteryFailure;
|
||||
snapshot.bools["functionTestActive"] = detailed->functionTestActive;
|
||||
snapshot.bools["durationTestActive"] = detailed->durationTestActive;
|
||||
snapshot.bools["testDone"] = detailed->testDone;
|
||||
snapshot.bools["identifyActive"] = detailed->identifyActive;
|
||||
snapshot.bools["physicalSelectionActive"] = detailed->physicalSelectionActive;
|
||||
snapshot.bools["circuitFailure"] = detailed->circuitFailure;
|
||||
snapshot.bools["batteryDurationFailure"] = detailed->batteryDurationFailure;
|
||||
snapshot.bools["emergencyLampFailure"] = detailed->emergencyLampFailure;
|
||||
snapshot.bools["functionTestMaxDelayExceeded"] = detailed->functionTestMaxDelayExceeded;
|
||||
snapshot.bools["durationTestMaxDelayExceeded"] = detailed->durationTestMaxDelayExceeded;
|
||||
snapshot.bools["functionTestFailed"] = detailed->functionTestFailed;
|
||||
snapshot.bools["durationTestFailed"] = detailed->durationTestFailed;
|
||||
snapshot.bools["functionTestResultValid"] = detailed->functionTestResultValid;
|
||||
snapshot.bools["durationTestResultValid"] = detailed->durationTestResultValid;
|
||||
snapshot.bools["batteryFullyCharged"] = detailed->batteryFullyCharged;
|
||||
snapshot.bools["functionTestPending"] = detailed->functionTestPending;
|
||||
snapshot.bools["durationTestPending"] = detailed->durationTestPending;
|
||||
snapshot.bools["restModeActive"] = detailed->restModeActive;
|
||||
snapshot.bools["normalModeActive"] = detailed->normalModeActive;
|
||||
snapshot.bools["emergencyModeActive"] = detailed->emergencyModeActive;
|
||||
snapshot.bools["extendedEmergencyModeActive"] = detailed->extendedEmergencyModeActive;
|
||||
snapshot.bools["hardwiredInhibitActive"] = detailed->hardwiredInhibitActive;
|
||||
snapshot.bools["hardwiredSwitchOn"] = detailed->hardwiredSwitchOn;
|
||||
snapshot.bools["supportsAutoTest"] = detailed->supportsAutoTest;
|
||||
snapshot.bools["supportsAdjustableEmergencyLevel"] = detailed->supportsAdjustableEmergencyLevel;
|
||||
|
||||
if (detailed->emergencyStatus.has_value()) {
|
||||
const DaliDT1EmergencyStatus status(detailed->emergencyStatus.value());
|
||||
snapshot.bools["inhibitMode"] = status.inhibitMode();
|
||||
snapshot.bools["functionTestRequestPending"] = status.functionTestRequestPending();
|
||||
snapshot.bools["durationTestRequestPending"] = status.durationTestRequestPending();
|
||||
snapshot.bools["identificationActive"] = status.identificationActive();
|
||||
snapshot.bools["physicallySelected"] = status.physicallySelected();
|
||||
}
|
||||
if (detailed->emergencyMode.has_value()) {
|
||||
const DaliDT1EmergencyMode mode(detailed->emergencyMode.value());
|
||||
snapshot.bools["functionTestInProgress"] = mode.functionTestInProgress();
|
||||
snapshot.bools["durationTestInProgress"] = mode.durationTestInProgress();
|
||||
}
|
||||
if (detailed->feature.has_value()) {
|
||||
const DaliDT1Features features(detailed->feature.value());
|
||||
snapshot.bools["integralEmergencyControlGear"] = features.integralEmergencyControlGear();
|
||||
snapshot.bools["maintainedControlGear"] = features.maintainedControlGear();
|
||||
snapshot.bools["switchedMaintainedControlGear"] = features.switchedMaintainedControlGear();
|
||||
snapshot.bools["autoTestCapability"] = features.autoTestCapability();
|
||||
snapshot.bools["adjustableEmergencyLevel"] = features.adjustableEmergencyLevel();
|
||||
snapshot.bools["hardwiredInhibitSupported"] = features.hardwiredInhibitSupported();
|
||||
snapshot.bools["physicalSelectionSupported"] = features.physicalSelectionSupported();
|
||||
snapshot.bools["relightInRestModeSupported"] = features.relightInRestModeSupported();
|
||||
}
|
||||
if (detailed->deviceStatus.has_value()) {
|
||||
const DaliDT1DeviceStatus status(detailed->deviceStatus.value());
|
||||
snapshot.bools["controlGearFailure"] = status.controlGearFailure();
|
||||
snapshot.bools["controlGearOk"] = status.controlGearOk();
|
||||
snapshot.bools["lampPoweredByEmergencyGear"] = status.lampPoweredByEmergencyGear();
|
||||
snapshot.bools["deviceResetState"] = status.resetState();
|
||||
snapshot.bools["deviceMissingShortAddress"] = status.missingShortAddress();
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
std::optional<DaliDomainSnapshot> DaliDomainService::dt4Snapshot(uint8_t gateway_id,
|
||||
int short_address) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
@@ -506,15 +613,27 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt4Snapshot(uint8_t gateway
|
||||
snapshot.ints["featuresRaw2"] = features->raw2();
|
||||
snapshot.ints["featuresRaw3"] = features->raw3();
|
||||
snapshot.ints["dimmingMethodCode"] = features->dimmingMethodCode();
|
||||
snapshot.bools["canQueryLoadOverCurrentShutdown"] =
|
||||
features->canQueryLoadOverCurrentShutdown();
|
||||
snapshot.bools["canQueryOpenCircuitDetection"] = features->canQueryOpenCircuitDetection();
|
||||
snapshot.bools["canQueryLoadDecrease"] = features->canQueryLoadDecrease();
|
||||
snapshot.bools["canQueryLoadIncrease"] = features->canQueryLoadIncrease();
|
||||
snapshot.bools["canQueryThermalShutdown"] = features->canQueryThermalShutdown();
|
||||
snapshot.bools["canQueryThermalOverloadReduction"] =
|
||||
features->canQueryThermalOverloadReduction();
|
||||
snapshot.bools["canQueryTemperature"] = features->canQueryTemperature();
|
||||
snapshot.bools["canQuerySupplyVoltage"] = features->canQuerySupplyVoltage();
|
||||
snapshot.bools["canQuerySupplyFrequency"] = features->canQuerySupplyFrequency();
|
||||
snapshot.bools["canQueryLoadVoltage"] = features->canQueryLoadVoltage();
|
||||
snapshot.bools["canQueryLoadCurrent"] = features->canQueryLoadCurrent();
|
||||
snapshot.bools["canQueryRealLoadPower"] = features->canQueryRealLoadPower();
|
||||
snapshot.bools["canQueryLoadRating"] = features->canQueryLoadRating();
|
||||
snapshot.bools["canQueryCurrentOverloadReduction"] =
|
||||
features->canQueryCurrentOverloadReduction();
|
||||
snapshot.bools["physicalSelectionSupported"] = features->physicalSelectionSupported();
|
||||
snapshot.bools["canSelectNonLogarithmicDimmingCurve"] =
|
||||
features->canSelectNonLogarithmicDimmingCurve();
|
||||
snapshot.bools["canQueryUnsuitableLoad"] = features->canQueryUnsuitableLoad();
|
||||
}
|
||||
if (const auto failure = dt4.getFailureStatus(short_address)) {
|
||||
snapshot.ints["failureRaw1"] = failure->raw1();
|
||||
@@ -526,7 +645,10 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt4Snapshot(uint8_t gateway
|
||||
snapshot.bools["thermalShutdown"] = failure->thermalShutdown();
|
||||
snapshot.bools["thermalOverloadReduction"] = failure->thermalOverloadReduction();
|
||||
snapshot.bools["referenceMeasurementFailedStatus"] = failure->referenceMeasurementFailed();
|
||||
snapshot.bools["loadUnsuitableForSelectedMethod"] =
|
||||
failure->loadUnsuitableForSelectedMethod();
|
||||
snapshot.bools["supplyVoltageOutOfLimits"] = failure->supplyVoltageOutOfLimits();
|
||||
snapshot.bools["supplyFrequencyOutOfLimits"] = failure->supplyFrequencyOutOfLimits();
|
||||
snapshot.bools["loadVoltageOutOfLimits"] = failure->loadVoltageOutOfLimits();
|
||||
snapshot.bools["loadCurrentOverloadReduction"] = failure->loadCurrentOverloadReduction();
|
||||
}
|
||||
@@ -637,6 +759,40 @@ std::optional<DaliDomainSnapshot> DaliDomainService::dt6Snapshot(uint8_t gateway
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
std::optional<DaliDomainSnapshot> DaliDomainService::dt8StatusSnapshot(
|
||||
uint8_t gateway_id, int short_address) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
if (channel == nullptr || channel->dali == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt8_status");
|
||||
bool has_data = false;
|
||||
if (const auto status = channel->dali->dt8.getColorStatus(short_address)) {
|
||||
has_data = true;
|
||||
snapshot.ints["colorStatusRaw"] = status->raw();
|
||||
snapshot.bools["xyOutOfRange"] = status->xyOutOfRange();
|
||||
snapshot.bools["ctOutOfRange"] = status->ctOutOfRange();
|
||||
snapshot.bools["autoCalibrationActive"] = status->autoCalibrationActive();
|
||||
snapshot.bools["autoCalibrationSuccess"] = status->autoCalibrationSuccess();
|
||||
snapshot.bools["xyActive"] = status->xyActive();
|
||||
snapshot.bools["ctActive"] = status->ctActive();
|
||||
snapshot.bools["primaryNActive"] = status->primaryNActive();
|
||||
snapshot.bools["rgbwafActive"] = status->rgbwafActive();
|
||||
}
|
||||
if (const auto features = channel->dali->dt8.getColorTypeFeature(short_address)) {
|
||||
has_data = true;
|
||||
snapshot.ints["colorTypeFeaturesRaw"] = features->features();
|
||||
snapshot.ints["primaryCount"] = features->primaryCount();
|
||||
snapshot.ints["rgbwafChannels"] = features->rgbwafChannels();
|
||||
snapshot.bools["xyCapable"] = features->xyCapable();
|
||||
snapshot.bools["ctCapable"] = features->ctCapable();
|
||||
snapshot.bools["primaryNCapable"] = features->primaryNCapable();
|
||||
snapshot.bools["rgbwafCapable"] = features->rgbwafCapable();
|
||||
}
|
||||
return has_data ? std::optional<DaliDomainSnapshot>(std::move(snapshot)) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<DaliDomainSnapshot> DaliDomainService::dt8SceneColorReport(
|
||||
uint8_t gateway_id, int short_address, int scene) const {
|
||||
const auto* channel = findChannelByGateway(gateway_id);
|
||||
|
||||
@@ -92,6 +92,7 @@ set(BACNET_PORT_SRCS
|
||||
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"src/gateway_bacnet_bridge.cpp"
|
||||
"src/gateway_bacnet.cpp"
|
||||
"src/gateway_bacnet_stack_port.c"
|
||||
"src/bip_socket_lwip.cpp"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -32,6 +33,7 @@ struct GatewayBacnetObjectBinding {
|
||||
std::string property{"presentValue"};
|
||||
bool out_of_service{false};
|
||||
uint32_t reliability{0};
|
||||
bool readable{false};
|
||||
};
|
||||
|
||||
struct GatewayBacnetServerStatus {
|
||||
@@ -46,13 +48,19 @@ using GatewayBacnetWriteCallback =
|
||||
std::function<bool(BridgeObjectType object_type, uint32_t object_instance,
|
||||
const std::string& property, const DaliValue& value)>;
|
||||
|
||||
using GatewayBacnetReadCallback =
|
||||
std::function<std::optional<DaliValue>(BridgeObjectType object_type,
|
||||
uint32_t object_instance,
|
||||
const std::string& property)>;
|
||||
|
||||
class GatewayBacnetServer {
|
||||
public:
|
||||
static GatewayBacnetServer& instance();
|
||||
|
||||
esp_err_t registerChannel(uint8_t gateway_id, const GatewayBacnetServerConfig& config,
|
||||
std::vector<GatewayBacnetObjectBinding> bindings,
|
||||
GatewayBacnetWriteCallback write_callback);
|
||||
GatewayBacnetWriteCallback write_callback,
|
||||
GatewayBacnetReadCallback read_callback = nullptr);
|
||||
GatewayBacnetServerStatus status() const;
|
||||
bool configCompatible(const GatewayBacnetServerConfig& config) const;
|
||||
bool handleWrite(BridgeObjectType object_type, uint32_t object_instance,
|
||||
@@ -70,6 +78,7 @@ class GatewayBacnetServer {
|
||||
|
||||
esp_err_t startStackLocked(const GatewayBacnetServerConfig& config);
|
||||
esp_err_t rebuildObjectsLocked();
|
||||
void refreshPresentValues();
|
||||
|
||||
static void TaskEntry(void* arg);
|
||||
void taskLoop();
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "bridge.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace gateway {
|
||||
|
||||
struct GatewayBacnetBridgeConfig {
|
||||
uint32_t deviceInstance{4194303};
|
||||
std::string localAddress;
|
||||
uint16_t udpPort{47808};
|
||||
};
|
||||
|
||||
struct GatewayBacnetModelBinding {
|
||||
std::string modelID;
|
||||
BridgeObjectType objectType{BridgeObjectType::unknown};
|
||||
int objectInstance{-1};
|
||||
std::string property;
|
||||
BridgeOperation operation{BridgeOperation::unknown};
|
||||
BridgeDaliTarget target;
|
||||
std::optional<int> bitIndex;
|
||||
};
|
||||
|
||||
class GatewayBacnetBridgeAdapter {
|
||||
public:
|
||||
explicit GatewayBacnetBridgeAdapter(DaliBridgeEngine& engine);
|
||||
|
||||
void setConfig(const GatewayBacnetBridgeConfig& config);
|
||||
const GatewayBacnetBridgeConfig& config() const;
|
||||
|
||||
DaliBridgeResult handlePropertyWrite(BridgeObjectType object_type,
|
||||
int object_instance,
|
||||
const std::string& property,
|
||||
const DaliValue& value) const;
|
||||
DaliBridgeResult readProperty(BridgeObjectType object_type,
|
||||
int object_instance,
|
||||
const std::string& property) const;
|
||||
std::optional<GatewayBacnetModelBinding> findObject(BridgeObjectType object_type,
|
||||
int object_instance,
|
||||
const std::string& property) const;
|
||||
std::vector<GatewayBacnetModelBinding> describeObjects() const;
|
||||
|
||||
private:
|
||||
DaliBridgeResult executeBinding(const GatewayBacnetModelBinding& binding,
|
||||
const std::string& sequence,
|
||||
const DaliValue* value) const;
|
||||
|
||||
DaliBridgeEngine& engine_;
|
||||
GatewayBacnetBridgeConfig config_;
|
||||
};
|
||||
|
||||
} // namespace gateway
|
||||
@@ -56,6 +56,17 @@ bool gateway_bacnet_stack_upsert_object(
|
||||
bool out_of_service,
|
||||
uint32_t reliability);
|
||||
|
||||
bool gateway_bacnet_stack_set_object_state(
|
||||
gateway_bacnet_object_kind_t object_kind,
|
||||
uint32_t object_instance,
|
||||
bool out_of_service,
|
||||
uint32_t reliability);
|
||||
|
||||
bool gateway_bacnet_stack_set_present_value(
|
||||
gateway_bacnet_object_kind_t object_kind,
|
||||
uint32_t object_instance,
|
||||
const gateway_bacnet_write_value_t* value);
|
||||
|
||||
bool gateway_bacnet_stack_clear_objects(void);
|
||||
|
||||
void gateway_bacnet_stack_send_i_am(void);
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace {
|
||||
|
||||
constexpr const char* kTag = "gateway_bacnet";
|
||||
constexpr TickType_t kPollDelayTicks = pdMS_TO_TICKS(10);
|
||||
constexpr TickType_t kValueRefreshTicks = pdMS_TO_TICKS(2000);
|
||||
constexpr uint32_t kReliabilityCommunicationFailure = 12;
|
||||
|
||||
class LockGuard {
|
||||
public:
|
||||
@@ -125,6 +127,59 @@ DaliValue StackWriteValueToDali(const gateway_bacnet_write_value_t& value) {
|
||||
}
|
||||
}
|
||||
|
||||
bool DaliValueToStackPresentValue(BridgeObjectType object_type, const DaliValue& value,
|
||||
gateway_bacnet_write_value_t* out) {
|
||||
if (out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
switch (ToBacnetKind(object_type)) {
|
||||
case GW_BACNET_OBJECT_ANALOG_INPUT:
|
||||
case GW_BACNET_OBJECT_ANALOG_VALUE:
|
||||
case GW_BACNET_OBJECT_ANALOG_OUTPUT: {
|
||||
const auto parsed = value.asDouble();
|
||||
if (!parsed.has_value()) {
|
||||
return false;
|
||||
}
|
||||
*out = gateway_bacnet_write_value_t{GW_BACNET_WRITE_VALUE_REAL,
|
||||
parsed.value(),
|
||||
false,
|
||||
0};
|
||||
return true;
|
||||
}
|
||||
case GW_BACNET_OBJECT_BINARY_INPUT:
|
||||
case GW_BACNET_OBJECT_BINARY_VALUE:
|
||||
case GW_BACNET_OBJECT_BINARY_OUTPUT: {
|
||||
const auto parsed = value.asBool();
|
||||
if (!parsed.has_value()) {
|
||||
return false;
|
||||
}
|
||||
*out = gateway_bacnet_write_value_t{GW_BACNET_WRITE_VALUE_BOOLEAN,
|
||||
0.0,
|
||||
parsed.value(),
|
||||
0};
|
||||
return true;
|
||||
}
|
||||
case GW_BACNET_OBJECT_MULTI_STATE_INPUT:
|
||||
case GW_BACNET_OBJECT_MULTI_STATE_VALUE:
|
||||
case GW_BACNET_OBJECT_MULTI_STATE_OUTPUT: {
|
||||
const auto parsed = value.asInt();
|
||||
if (!parsed.has_value()) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t unsigned_value = parsed.value() <= 0
|
||||
? 1
|
||||
: static_cast<uint32_t>(parsed.value());
|
||||
*out = gateway_bacnet_write_value_t{GW_BACNET_WRITE_VALUE_UNSIGNED,
|
||||
0.0,
|
||||
false,
|
||||
unsigned_value};
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void HandleStackWrite(gateway_bacnet_object_kind_t object_kind, uint32_t object_instance,
|
||||
const gateway_bacnet_write_value_t* value, void*) {
|
||||
if (g_server == nullptr || value == nullptr) {
|
||||
@@ -140,6 +195,7 @@ struct GatewayBacnetServer::ChannelRegistration {
|
||||
GatewayBacnetServerConfig config;
|
||||
std::vector<GatewayBacnetObjectBinding> bindings;
|
||||
GatewayBacnetWriteCallback write_callback;
|
||||
GatewayBacnetReadCallback read_callback;
|
||||
};
|
||||
|
||||
struct GatewayBacnetServer::RuntimeBinding {
|
||||
@@ -150,7 +206,9 @@ struct GatewayBacnetServer::RuntimeBinding {
|
||||
std::string property{"presentValue"};
|
||||
bool out_of_service{false};
|
||||
uint32_t reliability{0};
|
||||
bool readable{false};
|
||||
GatewayBacnetWriteCallback write_callback;
|
||||
GatewayBacnetReadCallback read_callback;
|
||||
};
|
||||
|
||||
GatewayBacnetServer& GatewayBacnetServer::instance() {
|
||||
@@ -187,7 +245,8 @@ GatewayBacnetServerStatus GatewayBacnetServer::status() const {
|
||||
esp_err_t GatewayBacnetServer::registerChannel(
|
||||
uint8_t gateway_id, const GatewayBacnetServerConfig& config,
|
||||
std::vector<GatewayBacnetObjectBinding> bindings,
|
||||
GatewayBacnetWriteCallback write_callback) {
|
||||
GatewayBacnetWriteCallback write_callback,
|
||||
GatewayBacnetReadCallback read_callback) {
|
||||
if (write_callback == nullptr) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
@@ -210,7 +269,7 @@ esp_err_t GatewayBacnetServer::registerChannel(
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
ChannelRegistration registration{gateway_id, config, std::move(bindings),
|
||||
std::move(write_callback)};
|
||||
std::move(write_callback), std::move(read_callback)};
|
||||
if (channel == channels_.end()) {
|
||||
channels_.push_back(std::move(registration));
|
||||
} else {
|
||||
@@ -297,7 +356,9 @@ esp_err_t GatewayBacnetServer::rebuildObjectsLocked() {
|
||||
: binding.property,
|
||||
binding.out_of_service,
|
||||
binding.reliability,
|
||||
channel.write_callback});
|
||||
binding.readable,
|
||||
channel.write_callback,
|
||||
channel.read_callback});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,12 +399,47 @@ bool GatewayBacnetServer::handleWrite(BridgeObjectType object_type, uint32_t obj
|
||||
return ok;
|
||||
}
|
||||
|
||||
void GatewayBacnetServer::refreshPresentValues() {
|
||||
std::vector<RuntimeBinding> bindings;
|
||||
{
|
||||
LockGuard guard(lock_);
|
||||
bindings = runtime_bindings_;
|
||||
}
|
||||
|
||||
for (const auto& binding : bindings) {
|
||||
const auto object_kind = ToBacnetKind(binding.object_type);
|
||||
if (object_kind == GW_BACNET_OBJECT_UNKNOWN) {
|
||||
continue;
|
||||
}
|
||||
if (!binding.readable || binding.read_callback == nullptr) {
|
||||
LockGuard guard(lock_);
|
||||
gateway_bacnet_stack_set_object_state(object_kind, binding.object_instance,
|
||||
binding.out_of_service, binding.reliability);
|
||||
continue;
|
||||
}
|
||||
|
||||
gateway_bacnet_write_value_t stack_value = {};
|
||||
const auto value = binding.read_callback(binding.object_type, binding.object_instance,
|
||||
binding.property);
|
||||
const bool converted = value.has_value() &&
|
||||
DaliValueToStackPresentValue(binding.object_type, value.value(),
|
||||
&stack_value);
|
||||
LockGuard guard(lock_);
|
||||
const bool ok = converted && gateway_bacnet_stack_set_present_value(
|
||||
object_kind, binding.object_instance, &stack_value);
|
||||
gateway_bacnet_stack_set_object_state(
|
||||
object_kind, binding.object_instance, binding.out_of_service || !ok,
|
||||
ok ? binding.reliability : kReliabilityCommunicationFailure);
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayBacnetServer::TaskEntry(void* arg) {
|
||||
static_cast<GatewayBacnetServer*>(arg)->taskLoop();
|
||||
}
|
||||
|
||||
void GatewayBacnetServer::taskLoop() {
|
||||
TickType_t last_timer = xTaskGetTickCount();
|
||||
TickType_t last_refresh = last_timer;
|
||||
|
||||
while (true) {
|
||||
const TickType_t now = xTaskGetTickCount();
|
||||
@@ -353,9 +449,17 @@ void GatewayBacnetServer::taskLoop() {
|
||||
elapsed_ms = static_cast<uint16_t>(elapsed * portTICK_PERIOD_MS);
|
||||
last_timer = now;
|
||||
}
|
||||
bool refresh_due = false;
|
||||
{
|
||||
LockGuard guard(lock_);
|
||||
gateway_bacnet_stack_poll(elapsed_ms);
|
||||
if ((now - last_refresh) >= kValueRefreshTicks) {
|
||||
refresh_due = true;
|
||||
}
|
||||
}
|
||||
if (refresh_due) {
|
||||
refreshPresentValues();
|
||||
last_refresh = now;
|
||||
}
|
||||
vTaskDelay(kPollDelayTicks);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#include "gateway_bacnet_bridge.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace gateway {
|
||||
|
||||
GatewayBacnetBridgeAdapter::GatewayBacnetBridgeAdapter(DaliBridgeEngine& engine)
|
||||
: engine_(engine) {}
|
||||
|
||||
void GatewayBacnetBridgeAdapter::setConfig(const GatewayBacnetBridgeConfig& config) {
|
||||
config_ = config;
|
||||
}
|
||||
|
||||
const GatewayBacnetBridgeConfig& GatewayBacnetBridgeAdapter::config() const { return config_; }
|
||||
|
||||
DaliBridgeResult GatewayBacnetBridgeAdapter::handlePropertyWrite(
|
||||
BridgeObjectType object_type, int object_instance, const std::string& property,
|
||||
const DaliValue& value) const {
|
||||
const auto binding = findObject(object_type, object_instance, property);
|
||||
if (!binding.has_value()) {
|
||||
DaliBridgeResult result;
|
||||
result.sequence = "bacnet-" + std::to_string(object_instance);
|
||||
result.error = "unmapped bacnet object";
|
||||
return result;
|
||||
}
|
||||
const std::string sequence = "bacnet-" + std::to_string(object_instance);
|
||||
return executeBinding(binding.value(), sequence, &value);
|
||||
}
|
||||
|
||||
DaliBridgeResult GatewayBacnetBridgeAdapter::readProperty(BridgeObjectType object_type,
|
||||
int object_instance,
|
||||
const std::string& property) const {
|
||||
const auto binding = findObject(object_type, object_instance, property);
|
||||
if (!binding.has_value()) {
|
||||
DaliBridgeResult result;
|
||||
result.sequence = "bacnet-read-" + std::to_string(object_instance);
|
||||
result.error = "unmapped bacnet object";
|
||||
return result;
|
||||
}
|
||||
const std::string sequence = "bacnet-read-" + std::to_string(object_instance);
|
||||
return executeBinding(binding.value(), sequence, nullptr);
|
||||
}
|
||||
|
||||
std::optional<GatewayBacnetModelBinding> GatewayBacnetBridgeAdapter::findObject(
|
||||
BridgeObjectType object_type, int object_instance, const std::string& property) const {
|
||||
for (const auto& model : engine_.listModels()) {
|
||||
if (model.protocol != BridgeProtocolKind::bacnet) {
|
||||
continue;
|
||||
}
|
||||
if (model.external.objectType != object_type) {
|
||||
continue;
|
||||
}
|
||||
if (model.external.objectInstance.value_or(-1) != object_instance) {
|
||||
continue;
|
||||
}
|
||||
if (!model.external.property.empty() && model.external.property != property) {
|
||||
continue;
|
||||
}
|
||||
const std::string binding_property = model.external.property.empty() ? property
|
||||
: model.external.property;
|
||||
return GatewayBacnetModelBinding{model.id,
|
||||
object_type,
|
||||
object_instance,
|
||||
binding_property,
|
||||
model.operation,
|
||||
model.dali,
|
||||
model.external.bitIndex};
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<GatewayBacnetModelBinding> GatewayBacnetBridgeAdapter::describeObjects() const {
|
||||
std::vector<GatewayBacnetModelBinding> bindings;
|
||||
for (const auto& model : engine_.listModels()) {
|
||||
if (model.protocol != BridgeProtocolKind::bacnet || !model.external.objectInstance.has_value()) {
|
||||
continue;
|
||||
}
|
||||
bindings.push_back(GatewayBacnetModelBinding{model.id,
|
||||
model.external.objectType,
|
||||
model.external.objectInstance.value(),
|
||||
model.external.property,
|
||||
model.operation,
|
||||
model.dali,
|
||||
model.external.bitIndex});
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
DaliBridgeResult GatewayBacnetBridgeAdapter::executeBinding(
|
||||
const GatewayBacnetModelBinding& binding, const std::string& sequence,
|
||||
const DaliValue* value) const {
|
||||
DaliBridgeRequest request;
|
||||
request.sequence = sequence;
|
||||
request.modelID = binding.modelID;
|
||||
if (value != nullptr) {
|
||||
request.value = *value;
|
||||
}
|
||||
return engine_.execute(request);
|
||||
}
|
||||
|
||||
} // namespace gateway
|
||||
@@ -522,6 +522,91 @@ bool gateway_bacnet_stack_upsert_object(
|
||||
}
|
||||
}
|
||||
|
||||
bool gateway_bacnet_stack_set_object_state(
|
||||
gateway_bacnet_object_kind_t object_kind,
|
||||
uint32_t object_instance,
|
||||
bool out_of_service,
|
||||
uint32_t reliability)
|
||||
{
|
||||
BACNET_RELIABILITY object_reliability = (BACNET_RELIABILITY)reliability;
|
||||
|
||||
switch (object_kind) {
|
||||
case GW_BACNET_OBJECT_ANALOG_VALUE:
|
||||
set_analog_value_state(object_instance, out_of_service, object_reliability);
|
||||
return true;
|
||||
case GW_BACNET_OBJECT_ANALOG_OUTPUT:
|
||||
set_analog_output_state(object_instance, out_of_service, object_reliability);
|
||||
return true;
|
||||
case GW_BACNET_OBJECT_BINARY_VALUE:
|
||||
set_binary_value_state(object_instance, out_of_service, object_reliability);
|
||||
return true;
|
||||
case GW_BACNET_OBJECT_BINARY_OUTPUT:
|
||||
set_binary_output_state(object_instance, out_of_service, object_reliability);
|
||||
return true;
|
||||
case GW_BACNET_OBJECT_MULTI_STATE_VALUE:
|
||||
set_multistate_value_state(object_instance, out_of_service, object_reliability);
|
||||
return true;
|
||||
case GW_BACNET_OBJECT_ANALOG_INPUT:
|
||||
set_analog_input_state(object_instance, out_of_service, object_reliability);
|
||||
return true;
|
||||
case GW_BACNET_OBJECT_BINARY_INPUT:
|
||||
set_binary_input_state(object_instance, out_of_service, object_reliability);
|
||||
return true;
|
||||
case GW_BACNET_OBJECT_MULTI_STATE_INPUT:
|
||||
set_multistate_input_state(object_instance, out_of_service, object_reliability);
|
||||
return true;
|
||||
case GW_BACNET_OBJECT_MULTI_STATE_OUTPUT:
|
||||
set_multistate_output_state(object_instance, out_of_service, object_reliability);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool gateway_bacnet_stack_set_present_value(
|
||||
gateway_bacnet_object_kind_t object_kind,
|
||||
uint32_t object_instance,
|
||||
const gateway_bacnet_write_value_t* value)
|
||||
{
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (object_kind) {
|
||||
case GW_BACNET_OBJECT_ANALOG_VALUE:
|
||||
return Analog_Value_Present_Value_Set(
|
||||
object_instance, (float)value->real_value, BACNET_NO_PRIORITY);
|
||||
case GW_BACNET_OBJECT_ANALOG_OUTPUT:
|
||||
return Analog_Output_Present_Value_Set(
|
||||
object_instance, (float)value->real_value, BACNET_MAX_PRIORITY);
|
||||
case GW_BACNET_OBJECT_ANALOG_INPUT:
|
||||
Analog_Input_Present_Value_Set(object_instance, (float)value->real_value);
|
||||
return true;
|
||||
case GW_BACNET_OBJECT_BINARY_VALUE:
|
||||
return Binary_Value_Present_Value_Set(
|
||||
object_instance, value->boolean_value ? BINARY_ACTIVE : BINARY_INACTIVE);
|
||||
case GW_BACNET_OBJECT_BINARY_OUTPUT:
|
||||
return Binary_Output_Present_Value_Set(
|
||||
object_instance, value->boolean_value ? BINARY_ACTIVE : BINARY_INACTIVE,
|
||||
BACNET_MAX_PRIORITY);
|
||||
case GW_BACNET_OBJECT_BINARY_INPUT:
|
||||
return Binary_Input_Present_Value_Set(
|
||||
object_instance, value->boolean_value ? BINARY_ACTIVE : BINARY_INACTIVE);
|
||||
case GW_BACNET_OBJECT_MULTI_STATE_VALUE:
|
||||
return Multistate_Value_Present_Value_Set(
|
||||
object_instance, value->unsigned_value == 0 ? 1 : value->unsigned_value);
|
||||
case GW_BACNET_OBJECT_MULTI_STATE_OUTPUT:
|
||||
return Multistate_Output_Present_Value_Set(
|
||||
object_instance, value->unsigned_value == 0 ? 1 : value->unsigned_value,
|
||||
BACNET_MAX_PRIORITY);
|
||||
case GW_BACNET_OBJECT_MULTI_STATE_INPUT:
|
||||
return Multistate_Input_Present_Value_Set(
|
||||
object_instance, value->unsigned_value == 0 ? 1 : value->unsigned_value);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool gateway_bacnet_stack_clear_objects(void)
|
||||
{
|
||||
return clear_analog_value_objects() &&
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "gateway_bridge.hpp"
|
||||
|
||||
#include "gateway_bacnet_bridge.hpp"
|
||||
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
||||
#include "bacnet_bridge.hpp"
|
||||
#include "gateway_bacnet.hpp"
|
||||
#endif
|
||||
#include "bridge.hpp"
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "lwip/sockets.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
@@ -40,12 +41,18 @@ constexpr const char* kBridgeConfigKey = "bridge_cfg";
|
||||
constexpr const char* kDiscoveryInventoryKey = "bridge_disc";
|
||||
constexpr int kMaxDaliShortAddress = 63;
|
||||
constexpr uint16_t kModbusUnknownRegister = 0xFFFF;
|
||||
constexpr uint16_t kModbusDiscreteInputBase = 10001;
|
||||
constexpr uint32_t kDiagnosticSnapshotCacheTtlMs = 500;
|
||||
constexpr uint32_t kBacnetGeneratedBinaryInputBase = 1000000;
|
||||
constexpr uint32_t kBacnetGeneratedBinaryInputChannelStride = 32768;
|
||||
constexpr uint32_t kBacnetMaxObjectInstance = 4194303;
|
||||
constexpr uint32_t kBacnetReliabilityNoFaultDetected = 0;
|
||||
constexpr uint32_t kBacnetReliabilityCommunicationFailure = 12;
|
||||
|
||||
struct GatewayBridgeStoredConfig {
|
||||
BridgeRuntimeConfig bridge;
|
||||
std::optional<GatewayModbusConfig> modbus;
|
||||
std::optional<GatewayBacnetBridgeConfig> bacnet_server;
|
||||
};
|
||||
|
||||
struct BridgeDiscoveryEntry {
|
||||
@@ -243,6 +250,18 @@ bool SnapshotHasDeviceType(const DaliDomainSnapshot& snapshot, int device_type)
|
||||
types->second.end();
|
||||
}
|
||||
|
||||
std::optional<bool> SnapshotBoolValue(const DaliDomainSnapshot& snapshot,
|
||||
const std::string& key) {
|
||||
const auto found = snapshot.bools.find(key);
|
||||
return found == snapshot.bools.end() ? std::nullopt : std::optional<bool>(found->second);
|
||||
}
|
||||
|
||||
std::optional<int> SnapshotIntValue(const DaliDomainSnapshot& snapshot,
|
||||
const std::string& key) {
|
||||
const auto found = snapshot.ints.find(key);
|
||||
return found == snapshot.ints.end() ? std::nullopt : std::optional<int>(found->second);
|
||||
}
|
||||
|
||||
bool OperationRequiresDt1(BridgeOperation operation) {
|
||||
switch (operation) {
|
||||
case BridgeOperation::getEmergencyLevel:
|
||||
@@ -268,6 +287,22 @@ bool OperationRequiresDt8(BridgeOperation operation) {
|
||||
}
|
||||
}
|
||||
|
||||
bool BridgeOperationReadable(BridgeOperation operation) {
|
||||
switch (operation) {
|
||||
case BridgeOperation::query:
|
||||
case BridgeOperation::getBrightness:
|
||||
case BridgeOperation::getStatus:
|
||||
case BridgeOperation::getColorTemperature:
|
||||
case BridgeOperation::getColorStatus:
|
||||
case BridgeOperation::getEmergencyLevel:
|
||||
case BridgeOperation::getEmergencyStatus:
|
||||
case BridgeOperation::getEmergencyFailureStatus:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<int> BridgeTargetValue(const BridgeDaliTarget& target) {
|
||||
switch (target.kind) {
|
||||
case BridgeDaliTargetKind::shortAddress:
|
||||
@@ -753,27 +788,53 @@ cJSON* ToCjson(const DaliValue& value) {
|
||||
|
||||
DaliValue::Object GatewayBridgeStoredConfigToValue(
|
||||
const BridgeRuntimeConfig& bridge_config,
|
||||
const std::optional<GatewayModbusConfig>& modbus_config) {
|
||||
const std::optional<GatewayModbusConfig>& modbus_config,
|
||||
const std::optional<GatewayBacnetBridgeConfig>& bacnet_server_config) {
|
||||
DaliValue::Object out = bridge_config.toJson();
|
||||
if (modbus_config.has_value()) {
|
||||
out["modbus"] = GatewayModbusConfigToValue(modbus_config.value());
|
||||
}
|
||||
if (bacnet_server_config.has_value()) {
|
||||
DaliValue::Object bacnet;
|
||||
bacnet["deviceInstance"] = static_cast<int64_t>(bacnet_server_config->deviceInstance);
|
||||
bacnet["localAddress"] = bacnet_server_config->localAddress;
|
||||
bacnet["udpPort"] = static_cast<int>(bacnet_server_config->udpPort);
|
||||
out["bacnetServer"] = std::move(bacnet);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string GatewayBridgeStoredConfigToJson(
|
||||
const BridgeRuntimeConfig& bridge_config,
|
||||
const std::optional<GatewayModbusConfig>& modbus_config) {
|
||||
cJSON* root = ToCjson(DaliValue(GatewayBridgeStoredConfigToValue(bridge_config, modbus_config)));
|
||||
const std::optional<GatewayModbusConfig>& modbus_config,
|
||||
const std::optional<GatewayBacnetBridgeConfig>& bacnet_server_config) {
|
||||
cJSON* root = ToCjson(DaliValue(GatewayBridgeStoredConfigToValue(
|
||||
bridge_config, modbus_config, bacnet_server_config)));
|
||||
const std::string body = PrintJson(root);
|
||||
cJSON_Delete(root);
|
||||
return body;
|
||||
}
|
||||
|
||||
std::optional<GatewayBacnetBridgeConfig> GatewayBacnetBridgeConfigFromValue(
|
||||
const DaliValue* value) {
|
||||
if (value == nullptr || value->asObject() == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto& json = *value->asObject();
|
||||
GatewayBacnetBridgeConfig 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;
|
||||
}
|
||||
|
||||
GatewayBridgeStoredConfig GatewayBridgeStoredConfigFromValue(const DaliValue::Object& object) {
|
||||
GatewayBridgeStoredConfig config;
|
||||
config.bridge = BridgeRuntimeConfig::fromJson(object);
|
||||
config.modbus = GatewayModbusConfigFromValue(getObjectValue(object, "modbus"));
|
||||
config.bacnet_server = GatewayBacnetBridgeConfigFromValue(
|
||||
getObjectValue(object, "bacnetServer"));
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -938,11 +999,12 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
std::unique_ptr<DaliBridgeEngine> engine;
|
||||
std::unique_ptr<GatewayModbusBridge> modbus;
|
||||
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
||||
std::unique_ptr<DaliBacnetBridge> bacnet;
|
||||
std::unique_ptr<GatewayBacnetBridgeAdapter> bacnet;
|
||||
#endif
|
||||
std::unique_ptr<DaliCloudBridge> cloud;
|
||||
BridgeRuntimeConfig bridge_config;
|
||||
std::optional<GatewayModbusConfig> modbus_config;
|
||||
std::optional<GatewayBacnetBridgeConfig> bacnet_server_config;
|
||||
BridgeDiscoveryInventory discovery_inventory;
|
||||
std::optional<GatewayCloudConfig> cloud_config;
|
||||
bool bridge_config_loaded{false};
|
||||
@@ -953,6 +1015,12 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
bool bacnet_started{false};
|
||||
TaskHandle_t modbus_task_handle{nullptr};
|
||||
|
||||
struct DiagnosticSnapshotCacheEntry {
|
||||
DaliDomainSnapshot snapshot;
|
||||
TickType_t captured_ticks{0};
|
||||
};
|
||||
std::map<std::string, DiagnosticSnapshotCacheEntry> diagnostic_snapshot_cache;
|
||||
|
||||
static void ModbusTaskEntry(void* arg) {
|
||||
static_cast<ChannelRuntime*>(arg)->modbusTaskLoop();
|
||||
}
|
||||
@@ -982,6 +1050,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
const auto stored_config = GatewayBridgeStoredConfigFromValue(bridge_object);
|
||||
bridge_config = stored_config.bridge;
|
||||
modbus_config = stored_config.modbus;
|
||||
bacnet_server_config = stored_config.bacnet_server;
|
||||
bridge_config_loaded = true;
|
||||
}
|
||||
DaliValue::Object discovery_object;
|
||||
@@ -1018,9 +1087,9 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
|
||||
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
||||
if (service_config.bacnet_enabled) {
|
||||
bacnet = std::make_unique<DaliBacnetBridge>(*engine);
|
||||
if (bridge_config.bacnet.has_value()) {
|
||||
bacnet->setConfig(bridge_config.bacnet.value());
|
||||
bacnet = std::make_unique<GatewayBacnetBridgeAdapter>(*engine);
|
||||
if (bacnet_server_config.has_value()) {
|
||||
bacnet->setConfig(bacnet_server_config.value());
|
||||
}
|
||||
} else {
|
||||
bacnet.reset();
|
||||
@@ -1029,6 +1098,62 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
|
||||
applyCloudModelsLocked();
|
||||
bacnet_started = false;
|
||||
diagnostic_snapshot_cache.clear();
|
||||
}
|
||||
|
||||
std::optional<DaliDomainSnapshot> diagnosticSnapshotLocked(int short_address,
|
||||
const std::string& kind) {
|
||||
if (!ValidShortAddress(short_address) || kind.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::string key = kind + ":" + std::to_string(short_address);
|
||||
const TickType_t now = xTaskGetTickCount();
|
||||
const auto cached = diagnostic_snapshot_cache.find(key);
|
||||
if (cached != diagnostic_snapshot_cache.end() &&
|
||||
(now - cached->second.captured_ticks) <= pdMS_TO_TICKS(kDiagnosticSnapshotCacheTtlMs)) {
|
||||
return cached->second.snapshot;
|
||||
}
|
||||
|
||||
std::optional<DaliDomainSnapshot> snapshot;
|
||||
if (kind == "base_status") {
|
||||
snapshot = domain.baseStatusSnapshot(channel.gateway_id, short_address);
|
||||
} else if (kind == "dt1") {
|
||||
snapshot = domain.dt1Snapshot(channel.gateway_id, short_address);
|
||||
} else if (kind == "dt4") {
|
||||
snapshot = domain.dt4Snapshot(channel.gateway_id, short_address);
|
||||
} else if (kind == "dt5") {
|
||||
snapshot = domain.dt5Snapshot(channel.gateway_id, short_address);
|
||||
} else if (kind == "dt6") {
|
||||
snapshot = domain.dt6Snapshot(channel.gateway_id, short_address);
|
||||
} else if (kind == "dt8_status") {
|
||||
snapshot = domain.dt8StatusSnapshot(channel.gateway_id, short_address);
|
||||
}
|
||||
|
||||
if (snapshot.has_value()) {
|
||||
diagnostic_snapshot_cache[key] = DiagnosticSnapshotCacheEntry{snapshot.value(), now};
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
std::optional<bool> readSnapshotBoolLocked(int short_address, const std::string& kind,
|
||||
const std::string& bool_key) {
|
||||
const auto snapshot = diagnosticSnapshotLocked(short_address, kind);
|
||||
if (!snapshot.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return SnapshotBoolValue(snapshot.value(), bool_key);
|
||||
}
|
||||
|
||||
std::optional<bool> readDiagnosticBoolPointLocked(const GatewayModbusPoint& point) {
|
||||
if (point.diagnostic_device_type > 0) {
|
||||
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
|
||||
if (discovery == nullptr ||
|
||||
!SnapshotHasDeviceType(discovery->discovery, point.diagnostic_device_type)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return readSnapshotBoolLocked(point.short_address, point.diagnostic_snapshot,
|
||||
point.diagnostic_bool);
|
||||
}
|
||||
|
||||
esp_err_t saveDiscoveryInventoryLocked() const {
|
||||
@@ -1117,6 +1242,10 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
[this](BridgeObjectType object_type, uint32_t object_instance,
|
||||
const std::string& property, const DaliValue& value) {
|
||||
return handleBacnetWrite(object_type, object_instance, property, value);
|
||||
},
|
||||
[this](BridgeObjectType object_type, uint32_t object_instance,
|
||||
const std::string& property) -> std::optional<DaliValue> {
|
||||
return readBacnetValue(object_type, object_instance, property);
|
||||
});
|
||||
#else
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
@@ -1142,13 +1271,15 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
BridgeProvisioningStore store(bridgeNamespace());
|
||||
const esp_err_t err = store.saveObject(
|
||||
kBridgeConfigKey,
|
||||
GatewayBridgeStoredConfigToValue(parsed->bridge, parsed->modbus));
|
||||
GatewayBridgeStoredConfigToValue(parsed->bridge, parsed->modbus,
|
||||
parsed->bacnet_server));
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
LockGuard guard(lock);
|
||||
bridge_config = parsed->bridge;
|
||||
modbus_config = parsed->modbus;
|
||||
bacnet_server_config = parsed->bacnet_server;
|
||||
bridge_config_loaded = true;
|
||||
applyBridgeConfigLocked();
|
||||
return ESP_OK;
|
||||
@@ -1163,6 +1294,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
LockGuard guard(lock);
|
||||
bridge_config = BridgeRuntimeConfig{};
|
||||
modbus_config.reset();
|
||||
bacnet_server_config.reset();
|
||||
bridge_config_loaded = false;
|
||||
applyBridgeConfigLocked();
|
||||
return ESP_OK;
|
||||
@@ -1244,7 +1376,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
return model == bridge_config.models.end() ? model_id : model->displayName();
|
||||
}
|
||||
|
||||
bool shouldPublishBacnetBindingLocked(const BacnetObjectBinding& binding) {
|
||||
bool shouldPublishBacnetBindingLocked(const GatewayBacnetModelBinding& binding) {
|
||||
if (binding.objectInstance < 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -1270,8 +1402,8 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<BacnetObjectBinding> effectiveBacnetObjectsLocked() {
|
||||
std::vector<BacnetObjectBinding> bindings;
|
||||
std::vector<GatewayBacnetModelBinding> effectiveBacnetObjectsLocked() {
|
||||
std::vector<GatewayBacnetModelBinding> bindings;
|
||||
if (bacnet == nullptr) {
|
||||
return bindings;
|
||||
}
|
||||
@@ -1290,15 +1422,104 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
: channel.name;
|
||||
config.task_stack_size = service_config.bacnet_task_stack_size;
|
||||
config.task_priority = service_config.bacnet_task_priority;
|
||||
if (bacnet != nullptr) {
|
||||
const auto& bridge_config = bacnet->config();
|
||||
config.device_instance = bridge_config.deviceInstance;
|
||||
config.local_address = bridge_config.localAddress;
|
||||
config.udp_port = bridge_config.udpPort;
|
||||
if (bacnet_server_config.has_value()) {
|
||||
config.device_instance = bacnet_server_config->deviceInstance;
|
||||
config.local_address = bacnet_server_config->localAddress;
|
||||
config.udp_port = bacnet_server_config->udpPort;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
std::optional<uint32_t> generatedBacnetBinaryInputInstance(uint16_t modbus_address) const {
|
||||
if (modbus_address < kModbusDiscreteInputBase) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const uint32_t relative = static_cast<uint32_t>(modbus_address - kModbusDiscreteInputBase);
|
||||
if (relative >= kBacnetGeneratedBinaryInputChannelStride) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const uint32_t instance = kBacnetGeneratedBinaryInputBase +
|
||||
static_cast<uint32_t>(channel.channel_index) *
|
||||
kBacnetGeneratedBinaryInputChannelStride +
|
||||
relative;
|
||||
if (instance > kBacnetMaxObjectInstance) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
std::optional<GatewayModbusPoint> generatedBacnetPointForObjectLocked(
|
||||
BridgeObjectType object_type, uint32_t object_instance) const {
|
||||
if (object_type != BridgeObjectType::binaryInput || modbus == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const uint32_t channel_base = kBacnetGeneratedBinaryInputBase +
|
||||
static_cast<uint32_t>(channel.channel_index) *
|
||||
kBacnetGeneratedBinaryInputChannelStride;
|
||||
if (object_instance < channel_base ||
|
||||
object_instance >= channel_base + kBacnetGeneratedBinaryInputChannelStride) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const uint32_t relative = object_instance - channel_base;
|
||||
if (relative > UINT16_MAX - kModbusDiscreteInputBase) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto point = modbus->findPoint(
|
||||
GatewayModbusSpace::kDiscreteInput,
|
||||
static_cast<uint16_t>(kModbusDiscreteInputBase + relative));
|
||||
if (!point.has_value() || !point->generated) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return point;
|
||||
}
|
||||
|
||||
bool shouldPublishGeneratedBacnetPointLocked(const GatewayModbusPointBinding& point) {
|
||||
if (!point.generated || point.space != GatewayModbusSpace::kDiscreteInput ||
|
||||
point.access != GatewayModbusAccess::kReadOnly ||
|
||||
!ValidShortAddress(point.short_address)) {
|
||||
return false;
|
||||
}
|
||||
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
|
||||
if (discovery == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (point.diagnostic_device_type > 0 &&
|
||||
!SnapshotHasDeviceType(discovery->discovery, point.diagnostic_device_type)) {
|
||||
return false;
|
||||
}
|
||||
return generatedBacnetBinaryInputInstance(point.address).has_value();
|
||||
}
|
||||
|
||||
std::vector<GatewayBacnetObjectBinding> generatedBacnetObjectBindingsLocked() {
|
||||
std::vector<GatewayBacnetObjectBinding> bindings;
|
||||
if (modbus == nullptr) {
|
||||
return bindings;
|
||||
}
|
||||
for (const auto& point : modbus->describePoints()) {
|
||||
if (!shouldPublishGeneratedBacnetPointLocked(point)) {
|
||||
continue;
|
||||
}
|
||||
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
|
||||
const auto object_instance = generatedBacnetBinaryInputInstance(point.address);
|
||||
if (discovery == nullptr || !object_instance.has_value()) {
|
||||
continue;
|
||||
}
|
||||
const bool out_of_service = !discovery->online;
|
||||
bindings.push_back(GatewayBacnetObjectBinding{channel.gateway_id,
|
||||
point.id,
|
||||
point.name,
|
||||
BridgeObjectType::binaryInput,
|
||||
object_instance.value(),
|
||||
"presentValue",
|
||||
out_of_service,
|
||||
out_of_service
|
||||
? kBacnetReliabilityCommunicationFailure
|
||||
: kBacnetReliabilityNoFaultDetected,
|
||||
true});
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
std::vector<GatewayBacnetObjectBinding> bacnetObjectBindingsLocked() {
|
||||
std::vector<GatewayBacnetObjectBinding> bindings;
|
||||
for (const auto& binding : effectiveBacnetObjectsLocked()) {
|
||||
@@ -1324,8 +1545,11 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
binding.property.empty() ? "presentValue"
|
||||
: binding.property,
|
||||
out_of_service,
|
||||
reliability});
|
||||
reliability,
|
||||
BridgeOperationReadable(binding.operation)});
|
||||
}
|
||||
auto generated = generatedBacnetObjectBindingsLocked();
|
||||
bindings.insert(bindings.end(), generated.begin(), generated.end());
|
||||
return bindings;
|
||||
}
|
||||
|
||||
@@ -1344,6 +1568,47 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
return result.ok;
|
||||
}
|
||||
|
||||
std::optional<DaliValue> readBacnetValue(BridgeObjectType object_type,
|
||||
uint32_t object_instance,
|
||||
const std::string& property) {
|
||||
LockGuard guard(lock);
|
||||
if (const auto point = generatedBacnetPointForObjectLocked(object_type, object_instance)) {
|
||||
const auto value = readGeneratedBoolPointLocked(point.value());
|
||||
return value.has_value() ? std::optional<DaliValue>(DaliValue(value.value()))
|
||||
: std::nullopt;
|
||||
}
|
||||
if (bacnet == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto binding = bacnet->findObject(object_type, static_cast<int>(object_instance),
|
||||
property.empty() ? "presentValue" : property);
|
||||
if (!binding.has_value() || !BridgeOperationReadable(binding->operation)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const DaliBridgeResult result = bacnet->readProperty(
|
||||
object_type, static_cast<int>(object_instance),
|
||||
property.empty() ? "presentValue" : property);
|
||||
if (!result.ok || !result.data.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if ((object_type == BridgeObjectType::binaryInput ||
|
||||
object_type == BridgeObjectType::binaryValue ||
|
||||
object_type == BridgeObjectType::binaryOutput) &&
|
||||
binding->bitIndex.has_value()) {
|
||||
const int bit = binding->bitIndex.value();
|
||||
if (bit < 0 || bit >= 32) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return DaliValue(((result.data.value() >> bit) & 0x1) != 0);
|
||||
}
|
||||
if (object_type == BridgeObjectType::binaryInput ||
|
||||
object_type == BridgeObjectType::binaryValue ||
|
||||
object_type == BridgeObjectType::binaryOutput) {
|
||||
return DaliValue(result.data.value() != 0);
|
||||
}
|
||||
return DaliValue(result.data.value());
|
||||
}
|
||||
|
||||
esp_err_t startBacnet() {
|
||||
LockGuard guard(lock);
|
||||
if (!service_config.bacnet_enabled) {
|
||||
@@ -1420,10 +1685,12 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
cJSON_AddBoolToObject(bacnet_json, "serverStarted", false);
|
||||
cJSON_AddNumberToObject(bacnet_json, "serverObjectCount", 0);
|
||||
#endif
|
||||
if (bridge_config.bacnet.has_value()) {
|
||||
cJSON_AddNumberToObject(bacnet_json, "deviceInstance", bridge_config.bacnet->deviceInstance);
|
||||
cJSON_AddStringToObject(bacnet_json, "localAddress", bridge_config.bacnet->localAddress.c_str());
|
||||
cJSON_AddNumberToObject(bacnet_json, "udpPort", bridge_config.bacnet->udpPort);
|
||||
if (bacnet_server_config.has_value()) {
|
||||
cJSON_AddNumberToObject(bacnet_json, "deviceInstance",
|
||||
bacnet_server_config->deviceInstance);
|
||||
cJSON_AddStringToObject(bacnet_json, "localAddress",
|
||||
bacnet_server_config->localAddress.c_str());
|
||||
cJSON_AddNumberToObject(bacnet_json, "udpPort", bacnet_server_config->udpPort);
|
||||
}
|
||||
cJSON_AddItemToObject(root, "bacnet", bacnet_json);
|
||||
}
|
||||
@@ -1446,7 +1713,8 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
|
||||
GatewayBridgeHttpResponse configJson() const {
|
||||
return GatewayBridgeHttpResponse{ESP_OK,
|
||||
GatewayBridgeStoredConfigToJson(bridge_config, modbus_config)};
|
||||
GatewayBridgeStoredConfigToJson(bridge_config, modbus_config,
|
||||
bacnet_server_config)};
|
||||
}
|
||||
|
||||
GatewayBridgeHttpResponse inventoryJson() const {
|
||||
@@ -1623,6 +1891,20 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
if (binding.short_address >= 0) {
|
||||
cJSON_AddNumberToObject(item, "shortAddress", binding.short_address);
|
||||
}
|
||||
if (binding.bit_index.has_value()) {
|
||||
cJSON_AddNumberToObject(item, "bitIndex", binding.bit_index.value());
|
||||
}
|
||||
if (!binding.diagnostic_snapshot.empty()) {
|
||||
cJSON_AddStringToObject(item, "diagnosticSnapshot",
|
||||
binding.diagnostic_snapshot.c_str());
|
||||
}
|
||||
if (!binding.diagnostic_bool.empty()) {
|
||||
cJSON_AddStringToObject(item, "diagnosticBool", binding.diagnostic_bool.c_str());
|
||||
}
|
||||
if (binding.diagnostic_device_type >= 0) {
|
||||
cJSON_AddNumberToObject(item, "diagnosticDeviceType",
|
||||
binding.diagnostic_device_type);
|
||||
}
|
||||
cJSON_AddItemToArray(bindings, item);
|
||||
}
|
||||
}
|
||||
@@ -1677,6 +1959,50 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
}
|
||||
cJSON_AddItemToArray(bindings, item);
|
||||
}
|
||||
if (modbus != nullptr) {
|
||||
for (const auto& point : modbus->describePoints()) {
|
||||
if (!shouldPublishGeneratedBacnetPointLocked(point)) {
|
||||
continue;
|
||||
}
|
||||
const auto object_instance = generatedBacnetBinaryInputInstance(point.address);
|
||||
const auto* discovery = findDiscoveryEntryLocked(point.short_address);
|
||||
if (!object_instance.has_value() || discovery == nullptr) {
|
||||
continue;
|
||||
}
|
||||
cJSON* item = cJSON_CreateObject();
|
||||
if (item == nullptr) {
|
||||
continue;
|
||||
}
|
||||
cJSON_AddStringToObject(item, "model", point.id.c_str());
|
||||
cJSON_AddStringToObject(item, "name", point.name.c_str());
|
||||
cJSON_AddStringToObject(item, "objectType", "binaryInput");
|
||||
cJSON_AddNumberToObject(item, "objectInstance", object_instance.value());
|
||||
cJSON_AddStringToObject(item, "property", "presentValue");
|
||||
cJSON_AddBoolToObject(item, "generated", true);
|
||||
cJSON_AddStringToObject(item, "generatedKind",
|
||||
GatewayModbusGeneratedKindToString(point.generated_kind));
|
||||
cJSON_AddNumberToObject(item, "shortAddress", point.short_address);
|
||||
if (!point.diagnostic_snapshot.empty()) {
|
||||
cJSON_AddStringToObject(item, "diagnosticSnapshot",
|
||||
point.diagnostic_snapshot.c_str());
|
||||
}
|
||||
if (!point.diagnostic_bool.empty()) {
|
||||
cJSON_AddStringToObject(item, "diagnosticBool", point.diagnostic_bool.c_str());
|
||||
}
|
||||
if (point.diagnostic_device_type >= 0) {
|
||||
cJSON_AddNumberToObject(item, "diagnosticDeviceType",
|
||||
point.diagnostic_device_type);
|
||||
}
|
||||
const bool out_of_service = !discovery->online;
|
||||
cJSON_AddBoolToObject(item, "outOfService", out_of_service);
|
||||
cJSON_AddStringToObject(item, "reliability",
|
||||
BacnetReliabilityToString(out_of_service
|
||||
? kBacnetReliabilityCommunicationFailure
|
||||
: kBacnetReliabilityNoFaultDetected));
|
||||
cJSON_AddStringToObject(item, "inventoryState", DiscoveryStateString(discovery->online));
|
||||
cJSON_AddItemToArray(bindings, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
cJSON_AddItemToObject(root, "bindings", bindings);
|
||||
@@ -1747,14 +2073,23 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
case GatewayModbusGeneratedKind::kShortSettingsKnown:
|
||||
return state.settings.anyKnown();
|
||||
case GatewayModbusGeneratedKind::kShortControlGearPresent:
|
||||
return readSnapshotBoolLocked(point.short_address, "base_status", "controlGearPresent");
|
||||
case GatewayModbusGeneratedKind::kShortLampFailure:
|
||||
return readSnapshotBoolLocked(point.short_address, "base_status", "lampFailure");
|
||||
case GatewayModbusGeneratedKind::kShortLampPowerOn:
|
||||
return readSnapshotBoolLocked(point.short_address, "base_status", "lampPowerOn");
|
||||
case GatewayModbusGeneratedKind::kShortLimitError:
|
||||
return readSnapshotBoolLocked(point.short_address, "base_status", "limitError");
|
||||
case GatewayModbusGeneratedKind::kShortFadingCompleted:
|
||||
return readSnapshotBoolLocked(point.short_address, "base_status", "fadingCompleted");
|
||||
case GatewayModbusGeneratedKind::kShortResetState:
|
||||
return readSnapshotBoolLocked(point.short_address, "base_status", "resetState");
|
||||
case GatewayModbusGeneratedKind::kShortMissingShortAddress:
|
||||
return readSnapshotBoolLocked(point.short_address, "base_status", "missingShortAddress");
|
||||
case GatewayModbusGeneratedKind::kShortPowerSupplyFault:
|
||||
return false;
|
||||
return readSnapshotBoolLocked(point.short_address, "base_status", "powerSupplyFault");
|
||||
case GatewayModbusGeneratedKind::kShortDiagnosticBit:
|
||||
return readDiagnosticBoolPointLocked(point);
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -1794,8 +2129,15 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
return state.status.scene_id.has_value()
|
||||
? static_cast<uint16_t>(state.status.scene_id.value())
|
||||
: kModbusUnknownRegister;
|
||||
case GatewayModbusGeneratedKind::kShortRawStatus:
|
||||
case GatewayModbusGeneratedKind::kShortRawStatus: {
|
||||
const auto snapshot = diagnosticSnapshotLocked(point.short_address, "base_status");
|
||||
if (snapshot.has_value()) {
|
||||
const auto raw_status = SnapshotIntValue(snapshot.value(), "rawStatus");
|
||||
return raw_status.has_value() ? static_cast<uint16_t>(raw_status.value() & 0xFF)
|
||||
: kModbusUnknownRegister;
|
||||
}
|
||||
return kModbusUnknownRegister;
|
||||
}
|
||||
case GatewayModbusGeneratedKind::kShortGroupMask:
|
||||
return state.group_mask_known ? state.group_mask : kModbusUnknownRegister;
|
||||
case GatewayModbusGeneratedKind::kShortPowerOnLevel:
|
||||
@@ -2427,11 +2769,24 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
|
||||
}
|
||||
return ErrorResponse(ESP_ERR_NOT_FOUND, "device did not respond to type discovery");
|
||||
}
|
||||
if (action == "dt4" || action == "dt5" || action == "dt6") {
|
||||
if (action == "base_status" || action == "status_bits") {
|
||||
const auto address = QueryInt(query, "addr", "address");
|
||||
if (!address.has_value() || !ValidDaliAddress(address.value())) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
|
||||
}
|
||||
return SnapshotResponse(dali_domain_.baseStatusSnapshot(gateway_id.value(), address.value()),
|
||||
"base status snapshot is unavailable");
|
||||
}
|
||||
if (action == "dt1" || action == "dt4" || action == "dt5" || action == "dt6" ||
|
||||
action == "dt8_status") {
|
||||
const auto address = QueryInt(query, "addr", "address");
|
||||
if (!address.has_value() || !ValidDaliAddress(address.value())) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
|
||||
}
|
||||
if (action == "dt1") {
|
||||
return SnapshotResponse(dali_domain_.dt1Snapshot(gateway_id.value(), address.value()),
|
||||
"DT1 snapshot is unavailable");
|
||||
}
|
||||
if (action == "dt4") {
|
||||
return SnapshotResponse(dali_domain_.dt4Snapshot(gateway_id.value(), address.value()),
|
||||
"DT4 snapshot is unavailable");
|
||||
@@ -2440,9 +2795,13 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
|
||||
return SnapshotResponse(dali_domain_.dt5Snapshot(gateway_id.value(), address.value()),
|
||||
"DT5 snapshot is unavailable");
|
||||
}
|
||||
if (action == "dt6") {
|
||||
return SnapshotResponse(dali_domain_.dt6Snapshot(gateway_id.value(), address.value()),
|
||||
"DT6 snapshot is unavailable");
|
||||
}
|
||||
return SnapshotResponse(dali_domain_.dt8StatusSnapshot(gateway_id.value(), address.value()),
|
||||
"DT8 status snapshot is unavailable");
|
||||
}
|
||||
if (action == "dt8_scene") {
|
||||
const auto address = QueryInt(query, "addr", "address");
|
||||
const auto scene = QueryInt(query, "scene");
|
||||
|
||||
@@ -79,6 +79,7 @@ enum class GatewayModbusGeneratedKind : uint8_t {
|
||||
kShortActualLevel,
|
||||
kShortSceneId,
|
||||
kShortRawStatus,
|
||||
kShortDiagnosticBit,
|
||||
};
|
||||
|
||||
struct GatewayModbusPoint {
|
||||
@@ -93,6 +94,9 @@ struct GatewayModbusPoint {
|
||||
std::string model_id;
|
||||
BridgeOperation operation{BridgeOperation::unknown};
|
||||
std::optional<int> bit_index;
|
||||
std::string diagnostic_snapshot;
|
||||
std::string diagnostic_bool;
|
||||
int diagnostic_device_type{-1};
|
||||
};
|
||||
|
||||
struct GatewayModbusPointBinding {
|
||||
@@ -105,6 +109,10 @@ struct GatewayModbusPointBinding {
|
||||
GatewayModbusGeneratedKind generated_kind{GatewayModbusGeneratedKind::kNone};
|
||||
int short_address{-1};
|
||||
GatewayModbusAccess access{GatewayModbusAccess::kReadWrite};
|
||||
std::optional<int> bit_index;
|
||||
std::string diagnostic_snapshot;
|
||||
std::string diagnostic_bool;
|
||||
int diagnostic_device_type{-1};
|
||||
};
|
||||
|
||||
std::optional<GatewayModbusConfig> GatewayModbusConfigFromValue(const DaliValue* value);
|
||||
|
||||
@@ -15,6 +15,9 @@ constexpr uint16_t kInputRegisterBase = 30001;
|
||||
constexpr uint16_t kHoldingRegisterBase = 40001;
|
||||
constexpr uint16_t kShortAddressCount = 64;
|
||||
constexpr uint16_t kShortStride = 32;
|
||||
constexpr uint16_t kDiagnosticDiscreteInputBase = kDiscreteInputBase +
|
||||
kShortAddressCount * kShortStride;
|
||||
constexpr uint16_t kDiagnosticStride = 128;
|
||||
|
||||
struct PointKey {
|
||||
GatewayModbusSpace space{GatewayModbusSpace::kHoldingRegister};
|
||||
@@ -37,6 +40,15 @@ struct GeneratedPointSpec {
|
||||
const char* name;
|
||||
};
|
||||
|
||||
struct GeneratedDiagnosticBitSpec {
|
||||
uint16_t offset;
|
||||
int device_type;
|
||||
const char* snapshot;
|
||||
const char* bool_key;
|
||||
const char* suffix;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
constexpr std::array<GeneratedPointSpec, 4> kGeneratedCoils{{
|
||||
{0, GatewayModbusSpace::kCoil, GatewayModbusAccess::kWriteOnly,
|
||||
GatewayModbusGeneratedKind::kShortOn, "on", "recall max"},
|
||||
@@ -48,7 +60,7 @@ constexpr std::array<GeneratedPointSpec, 4> kGeneratedCoils{{
|
||||
GatewayModbusGeneratedKind::kShortRecallMin, "recall_min", "recall min"},
|
||||
}};
|
||||
|
||||
constexpr std::array<GeneratedPointSpec, 18> kGeneratedDiscreteInputs{{
|
||||
constexpr std::array<GeneratedPointSpec, 19> kGeneratedDiscreteInputs{{
|
||||
{0, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
|
||||
GatewayModbusGeneratedKind::kShortDiscovered, "discovered", "discovered"},
|
||||
{1, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
|
||||
@@ -87,9 +99,12 @@ constexpr std::array<GeneratedPointSpec, 18> kGeneratedDiscreteInputs{{
|
||||
{22, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
|
||||
GatewayModbusGeneratedKind::kShortMissingShortAddress, "missing_short_address",
|
||||
"missing short address"},
|
||||
{23, GatewayModbusSpace::kDiscreteInput, GatewayModbusAccess::kReadOnly,
|
||||
GatewayModbusGeneratedKind::kShortPowerSupplyFault, "power_supply_fault",
|
||||
"power supply fault"},
|
||||
}};
|
||||
|
||||
constexpr std::array<GeneratedPointSpec, 8> kGeneratedHoldingRegisters{{
|
||||
constexpr std::array<GeneratedPointSpec, 9> kGeneratedHoldingRegisters{{
|
||||
{0, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
|
||||
GatewayModbusGeneratedKind::kShortBrightness, "brightness", "brightness"},
|
||||
{1, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
|
||||
@@ -108,6 +123,8 @@ constexpr std::array<GeneratedPointSpec, 8> kGeneratedHoldingRegisters{{
|
||||
GatewayModbusGeneratedKind::kShortMaxLevel, "max_level", "maximum level"},
|
||||
{7, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
|
||||
GatewayModbusGeneratedKind::kShortFadeTime, "fade_time", "fade time"},
|
||||
{8, GatewayModbusSpace::kHoldingRegister, GatewayModbusAccess::kReadWrite,
|
||||
GatewayModbusGeneratedKind::kShortFadeRate, "fade_rate", "fade rate"},
|
||||
}};
|
||||
|
||||
constexpr std::array<GeneratedPointSpec, 13> kGeneratedInputRegisters{{
|
||||
@@ -140,6 +157,132 @@ constexpr std::array<GeneratedPointSpec, 13> kGeneratedInputRegisters{{
|
||||
GatewayModbusGeneratedKind::kShortFadeRate, "fade_rate", "fade rate"},
|
||||
}};
|
||||
|
||||
constexpr GeneratedDiagnosticBitSpec kGeneratedDiagnosticBits[] = {
|
||||
{0, 1, "dt1", "circuitFailure", "dt1_circuit_failure", "DT1 circuit failure"},
|
||||
{1, 1, "dt1", "batteryDurationFailure", "dt1_battery_duration_failure", "DT1 battery duration failure"},
|
||||
{2, 1, "dt1", "batteryFailure", "dt1_battery_failure", "DT1 battery failure"},
|
||||
{3, 1, "dt1", "emergencyLampFailure", "dt1_emergency_lamp_failure", "DT1 emergency lamp failure"},
|
||||
{4, 1, "dt1", "functionTestMaxDelayExceeded", "dt1_function_test_delay_exceeded", "DT1 function test delay exceeded"},
|
||||
{5, 1, "dt1", "durationTestMaxDelayExceeded", "dt1_duration_test_delay_exceeded", "DT1 duration test delay exceeded"},
|
||||
{6, 1, "dt1", "functionTestFailed", "dt1_function_test_failed", "DT1 function test failed"},
|
||||
{7, 1, "dt1", "durationTestFailed", "dt1_duration_test_failed", "DT1 duration test failed"},
|
||||
{8, 1, "dt1", "inhibitMode", "dt1_inhibit_mode", "DT1 inhibit mode"},
|
||||
{9, 1, "dt1", "functionTestResultValid", "dt1_function_result_valid", "DT1 function result valid"},
|
||||
{10, 1, "dt1", "durationTestResultValid", "dt1_duration_result_valid", "DT1 duration result valid"},
|
||||
{11, 1, "dt1", "batteryFullyCharged", "dt1_battery_fully_charged", "DT1 battery fully charged"},
|
||||
{12, 1, "dt1", "functionTestRequestPending", "dt1_function_request_pending", "DT1 function request pending"},
|
||||
{13, 1, "dt1", "durationTestRequestPending", "dt1_duration_request_pending", "DT1 duration request pending"},
|
||||
{14, 1, "dt1", "identificationActive", "dt1_identification_active", "DT1 identification active"},
|
||||
{15, 1, "dt1", "physicallySelected", "dt1_physically_selected", "DT1 physically selected"},
|
||||
{16, 1, "dt1", "restModeActive", "dt1_rest_mode_active", "DT1 rest mode active"},
|
||||
{17, 1, "dt1", "normalModeActive", "dt1_normal_mode_active", "DT1 normal mode active"},
|
||||
{18, 1, "dt1", "emergencyModeActive", "dt1_emergency_mode_active", "DT1 emergency mode active"},
|
||||
{19, 1, "dt1", "extendedEmergencyModeActive", "dt1_extended_mode_active", "DT1 extended emergency mode active"},
|
||||
{20, 1, "dt1", "functionTestInProgress", "dt1_function_test_in_progress", "DT1 function test in progress"},
|
||||
{21, 1, "dt1", "durationTestInProgress", "dt1_duration_test_in_progress", "DT1 duration test in progress"},
|
||||
{22, 1, "dt1", "hardwiredInhibitActive", "dt1_hardwired_inhibit_active", "DT1 hardwired inhibit active"},
|
||||
{23, 1, "dt1", "hardwiredSwitchOn", "dt1_hardwired_switch_on", "DT1 hardwired switch on"},
|
||||
{24, 1, "dt1", "integralEmergencyControlGear", "dt1_integral_emergency", "DT1 integral emergency gear"},
|
||||
{25, 1, "dt1", "maintainedControlGear", "dt1_maintained", "DT1 maintained gear"},
|
||||
{26, 1, "dt1", "switchedMaintainedControlGear", "dt1_switched_maintained", "DT1 switched maintained gear"},
|
||||
{27, 1, "dt1", "autoTestCapability", "dt1_auto_test_capability", "DT1 auto test capability"},
|
||||
{28, 1, "dt1", "adjustableEmergencyLevel", "dt1_adjustable_level", "DT1 adjustable emergency level"},
|
||||
{29, 1, "dt1", "hardwiredInhibitSupported", "dt1_hardwired_inhibit_supported", "DT1 hardwired inhibit supported"},
|
||||
{30, 1, "dt1", "physicalSelectionSupported", "dt1_physical_selection_supported", "DT1 physical selection supported"},
|
||||
{31, 1, "dt1", "relightInRestModeSupported", "dt1_relight_rest_supported", "DT1 relight in rest mode supported"},
|
||||
{32, 4, "dt4", "leadingEdgeModeRunning", "dt4_leading_edge_running", "DT4 leading edge running"},
|
||||
{33, 4, "dt4", "trailingEdgeModeRunning", "dt4_trailing_edge_running", "DT4 trailing edge running"},
|
||||
{34, 4, "dt4", "referenceMeasurementRunning", "dt4_reference_running", "DT4 reference measurement running"},
|
||||
{35, 4, "dt4", "nonLogarithmicDimmingCurveActive", "dt4_non_log_curve_active", "DT4 non-log curve active"},
|
||||
{36, 4, "dt4", "canQueryLoadOverCurrentShutdown", "dt4_can_load_over_current_shutdown", "DT4 can query over-current shutdown"},
|
||||
{37, 4, "dt4", "canQueryOpenCircuitDetection", "dt4_can_open_circuit", "DT4 can query open circuit"},
|
||||
{38, 4, "dt4", "canQueryLoadDecrease", "dt4_can_load_decrease", "DT4 can query load decrease"},
|
||||
{39, 4, "dt4", "canQueryLoadIncrease", "dt4_can_load_increase", "DT4 can query load increase"},
|
||||
{40, 4, "dt4", "canQueryThermalShutdown", "dt4_can_thermal_shutdown", "DT4 can query thermal shutdown"},
|
||||
{41, 4, "dt4", "canQueryThermalOverloadReduction", "dt4_can_thermal_overload", "DT4 can query thermal overload"},
|
||||
{42, 4, "dt4", "physicalSelectionSupported", "dt4_physical_selection_supported", "DT4 physical selection supported"},
|
||||
{43, 4, "dt4", "canQueryTemperature", "dt4_can_temperature", "DT4 can query temperature"},
|
||||
{44, 4, "dt4", "canQuerySupplyVoltage", "dt4_can_supply_voltage", "DT4 can query supply voltage"},
|
||||
{45, 4, "dt4", "canQuerySupplyFrequency", "dt4_can_supply_frequency", "DT4 can query supply frequency"},
|
||||
{46, 4, "dt4", "canQueryLoadVoltage", "dt4_can_load_voltage", "DT4 can query load voltage"},
|
||||
{47, 4, "dt4", "canQueryLoadCurrent", "dt4_can_load_current", "DT4 can query load current"},
|
||||
{48, 4, "dt4", "canQueryRealLoadPower", "dt4_can_load_power", "DT4 can query load power"},
|
||||
{49, 4, "dt4", "canQueryLoadRating", "dt4_can_load_rating", "DT4 can query load rating"},
|
||||
{50, 4, "dt4", "canQueryCurrentOverloadReduction", "dt4_can_current_overload", "DT4 can query current overload"},
|
||||
{51, 4, "dt4", "canSelectNonLogarithmicDimmingCurve", "dt4_can_non_log_curve", "DT4 can select non-log curve"},
|
||||
{52, 4, "dt4", "canQueryUnsuitableLoad", "dt4_can_unsuitable_load", "DT4 can query unsuitable load"},
|
||||
{53, 4, "dt4", "loadOverCurrentShutdown", "dt4_load_over_current_shutdown", "DT4 load over-current shutdown"},
|
||||
{54, 4, "dt4", "openCircuitDetected", "dt4_open_circuit_detected", "DT4 open circuit detected"},
|
||||
{55, 4, "dt4", "loadDecreaseDetected", "dt4_load_decrease_detected", "DT4 load decrease detected"},
|
||||
{56, 4, "dt4", "loadIncreaseDetected", "dt4_load_increase_detected", "DT4 load increase detected"},
|
||||
{57, 4, "dt4", "thermalShutdown", "dt4_thermal_shutdown", "DT4 thermal shutdown"},
|
||||
{58, 4, "dt4", "thermalOverloadReduction", "dt4_thermal_overload", "DT4 thermal overload reduction"},
|
||||
{59, 4, "dt4", "referenceMeasurementFailedStatus", "dt4_reference_failed", "DT4 reference failed"},
|
||||
{60, 4, "dt4", "loadUnsuitableForSelectedMethod", "dt4_unsuitable_load", "DT4 unsuitable load"},
|
||||
{61, 4, "dt4", "supplyVoltageOutOfLimits", "dt4_supply_voltage_limits", "DT4 supply voltage out of limits"},
|
||||
{62, 4, "dt4", "supplyFrequencyOutOfLimits", "dt4_supply_frequency_limits", "DT4 supply frequency out of limits"},
|
||||
{63, 4, "dt4", "loadVoltageOutOfLimits", "dt4_load_voltage_limits", "DT4 load voltage out of limits"},
|
||||
{64, 4, "dt4", "loadCurrentOverloadReduction", "dt4_load_current_overload", "DT4 load current overload"},
|
||||
{65, 5, "dt5", "outputRange0To10VSelectable", "dt5_output_range_selectable", "DT5 output range selectable"},
|
||||
{66, 5, "dt5", "internalPullUpSelectable", "dt5_pullup_selectable", "DT5 pull-up selectable"},
|
||||
{67, 5, "dt5", "outputFaultDetectionSelectable", "dt5_fault_detection_selectable", "DT5 fault detection selectable"},
|
||||
{68, 5, "dt5", "mainsRelay", "dt5_mains_relay", "DT5 mains relay"},
|
||||
{69, 5, "dt5", "outputLevelQueryable", "dt5_output_level_queryable", "DT5 output level queryable"},
|
||||
{70, 5, "dt5", "nonLogarithmicDimmingCurveSupported", "dt5_non_log_supported", "DT5 non-log curve supported"},
|
||||
{71, 5, "dt5", "physicalSelectionByOutputLossSupported", "dt5_output_loss_selection", "DT5 output-loss selection supported"},
|
||||
{72, 5, "dt5", "physicalSelectionSwitchSupported", "dt5_selection_switch", "DT5 selection switch supported"},
|
||||
{73, 5, "dt5", "outputFaultDetected", "dt5_output_fault", "DT5 output fault detected"},
|
||||
{74, 5, "dt5", "zeroToTenVoltOperation", "dt5_zero_to_ten", "DT5 0-10V operation"},
|
||||
{75, 5, "dt5", "internalPullUpOn", "dt5_pullup_on", "DT5 pull-up on"},
|
||||
{76, 5, "dt5", "nonLogarithmicDimmingCurveActive", "dt5_non_log_active", "DT5 non-log curve active"},
|
||||
{77, 6, "dt6", "ledPowerSupplyIntegrated", "dt6_power_supply_integrated", "DT6 power supply integrated"},
|
||||
{78, 6, "dt6", "ledModuleIntegrated", "dt6_module_integrated", "DT6 LED module integrated"},
|
||||
{79, 6, "dt6", "acSupplyPossible", "dt6_ac_supply_possible", "DT6 AC supply possible"},
|
||||
{80, 6, "dt6", "dcSupplyPossible", "dt6_dc_supply_possible", "DT6 DC supply possible"},
|
||||
{81, 6, "dt6", "pwmModePossible", "dt6_pwm_possible", "DT6 PWM possible"},
|
||||
{82, 6, "dt6", "amModePossible", "dt6_am_possible", "DT6 AM possible"},
|
||||
{83, 6, "dt6", "currentControlledOutputPossible", "dt6_current_control_possible", "DT6 current control possible"},
|
||||
{84, 6, "dt6", "highCurrentPulseModePossible", "dt6_high_current_possible", "DT6 high current possible"},
|
||||
{85, 6, "dt6", "canQueryShortCircuit", "dt6_can_short_circuit", "DT6 can query short circuit"},
|
||||
{86, 6, "dt6", "canQueryOpenCircuit", "dt6_can_open_circuit", "DT6 can query open circuit"},
|
||||
{87, 6, "dt6", "canQueryLoadDecrease", "dt6_can_load_decrease", "DT6 can query load decrease"},
|
||||
{88, 6, "dt6", "canQueryLoadIncrease", "dt6_can_load_increase", "DT6 can query load increase"},
|
||||
{89, 6, "dt6", "canQueryCurrentProtector", "dt6_can_current_protector", "DT6 can query current protector"},
|
||||
{90, 6, "dt6", "canQueryThermalShutdown", "dt6_can_thermal_shutdown", "DT6 can query thermal shutdown"},
|
||||
{91, 6, "dt6", "canQueryThermalOverloadReduction", "dt6_can_thermal_overload", "DT6 can query thermal overload"},
|
||||
{92, 6, "dt6", "shortCircuit", "dt6_short_circuit", "DT6 short circuit"},
|
||||
{93, 6, "dt6", "openCircuit", "dt6_open_circuit", "DT6 open circuit"},
|
||||
{94, 6, "dt6", "loadDecrease", "dt6_load_decrease", "DT6 load decrease"},
|
||||
{95, 6, "dt6", "loadIncrease", "dt6_load_increase", "DT6 load increase"},
|
||||
{96, 6, "dt6", "currentProtectorActive", "dt6_current_protector_active", "DT6 current protector active"},
|
||||
{97, 6, "dt6", "thermalShutdown", "dt6_thermal_shutdown", "DT6 thermal shutdown"},
|
||||
};
|
||||
|
||||
constexpr GeneratedDiagnosticBitSpec kGeneratedDiagnosticBitsTail[] = {
|
||||
{98, 6, "dt6", "thermalOverloadReduction", "dt6_thermal_overload", "DT6 thermal overload"},
|
||||
{99, 6, "dt6", "referenceMeasurementFailed", "dt6_reference_failed", "DT6 reference failed"},
|
||||
{100, 6, "dt6", "pwmModeActive", "dt6_pwm_active", "DT6 PWM active"},
|
||||
{101, 6, "dt6", "amModeActive", "dt6_am_active", "DT6 AM active"},
|
||||
{102, 6, "dt6", "currentControlledOutput", "dt6_current_controlled_output", "DT6 current controlled output"},
|
||||
{103, 6, "dt6", "highCurrentPulseModeActive", "dt6_high_current_active", "DT6 high current active"},
|
||||
{104, 6, "dt6", "nonLogarithmicDimmingCurveActive", "dt6_non_log_active", "DT6 non-log curve active"},
|
||||
{105, 8, "dt8_status", "xyOutOfRange", "dt8_xy_out_of_range", "DT8 xy out of range"},
|
||||
{106, 8, "dt8_status", "ctOutOfRange", "dt8_ct_out_of_range", "DT8 CT out of range"},
|
||||
{107, 8, "dt8_status", "autoCalibrationActive", "dt8_auto_cal_active", "DT8 auto calibration active"},
|
||||
{108, 8, "dt8_status", "autoCalibrationSuccess", "dt8_auto_cal_success", "DT8 auto calibration success"},
|
||||
{109, 8, "dt8_status", "xyActive", "dt8_xy_active", "DT8 xy active"},
|
||||
{110, 8, "dt8_status", "ctActive", "dt8_ct_active", "DT8 CT active"},
|
||||
{111, 8, "dt8_status", "primaryNActive", "dt8_primary_active", "DT8 primary-N active"},
|
||||
{112, 8, "dt8_status", "rgbwafActive", "dt8_rgbwaf_active", "DT8 RGBWAF active"},
|
||||
{113, 8, "dt8_status", "xyCapable", "dt8_xy_capable", "DT8 xy capable"},
|
||||
{114, 8, "dt8_status", "ctCapable", "dt8_ct_capable", "DT8 CT capable"},
|
||||
{115, 8, "dt8_status", "primaryNCapable", "dt8_primary_capable", "DT8 primary-N capable"},
|
||||
{116, 8, "dt8_status", "rgbwafCapable", "dt8_rgbwaf_capable", "DT8 RGBWAF capable"},
|
||||
{117, 6, "dt6", "physicalSelectionSupported", "dt6_physical_selection_supported", "DT6 physical selection supported"},
|
||||
{118, 6, "dt6", "currentProtectorEnabled", "dt6_current_protector_enabled", "DT6 current protector enabled"},
|
||||
{119, 1, "dt1", "controlGearFailure", "dt1_control_gear_failure", "DT1 control gear failure"},
|
||||
};
|
||||
|
||||
uint16_t baseForSpace(GatewayModbusSpace space) {
|
||||
switch (space) {
|
||||
case GatewayModbusSpace::kCoil:
|
||||
@@ -214,6 +357,29 @@ void addGeneratedPoint(std::map<PointKey, GatewayModbusPoint>* points, uint8_t s
|
||||
(*points)[PointKey{spec.space, address}] = std::move(point);
|
||||
}
|
||||
|
||||
void addGeneratedDiagnosticPoint(std::map<PointKey, GatewayModbusPoint>* points,
|
||||
uint8_t short_address,
|
||||
const GeneratedDiagnosticBitSpec& spec) {
|
||||
if (points == nullptr) {
|
||||
return;
|
||||
}
|
||||
const uint16_t address = static_cast<uint16_t>(kDiagnosticDiscreteInputBase +
|
||||
short_address * kDiagnosticStride + spec.offset);
|
||||
GatewayModbusPoint point;
|
||||
point.space = GatewayModbusSpace::kDiscreteInput;
|
||||
point.access = GatewayModbusAccess::kReadOnly;
|
||||
point.address = address;
|
||||
point.id = generatedId(short_address, spec.suffix);
|
||||
point.name = generatedName(short_address, spec.name);
|
||||
point.generated = true;
|
||||
point.generated_kind = GatewayModbusGeneratedKind::kShortDiagnosticBit;
|
||||
point.short_address = short_address;
|
||||
point.diagnostic_snapshot = spec.snapshot == nullptr ? "" : spec.snapshot;
|
||||
point.diagnostic_bool = spec.bool_key == nullptr ? "" : spec.bool_key;
|
||||
point.diagnostic_device_type = spec.device_type;
|
||||
(*points)[PointKey{point.space, address}] = std::move(point);
|
||||
}
|
||||
|
||||
GatewayModbusPointBinding toBinding(const GatewayModbusPoint& point) {
|
||||
return GatewayModbusPointBinding{point.model_id,
|
||||
point.space,
|
||||
@@ -223,7 +389,11 @@ GatewayModbusPointBinding toBinding(const GatewayModbusPoint& point) {
|
||||
point.generated,
|
||||
point.generated_kind,
|
||||
point.short_address,
|
||||
point.access};
|
||||
point.access,
|
||||
point.bit_index,
|
||||
point.diagnostic_snapshot,
|
||||
point.diagnostic_bool,
|
||||
point.diagnostic_device_type};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -356,6 +526,8 @@ const char* GatewayModbusGeneratedKindToString(GatewayModbusGeneratedKind kind)
|
||||
return "short_scene_id";
|
||||
case GatewayModbusGeneratedKind::kShortRawStatus:
|
||||
return "short_raw_status";
|
||||
case GatewayModbusGeneratedKind::kShortDiagnosticBit:
|
||||
return "short_diagnostic_bit";
|
||||
case GatewayModbusGeneratedKind::kNone:
|
||||
default:
|
||||
return "none";
|
||||
@@ -417,6 +589,12 @@ void GatewayModbusBridge::rebuildMap() {
|
||||
for (const auto& spec : kGeneratedInputRegisters) {
|
||||
addGeneratedPoint(&next, short_address, spec);
|
||||
}
|
||||
for (const auto& spec : kGeneratedDiagnosticBits) {
|
||||
addGeneratedDiagnosticPoint(&next, short_address, spec);
|
||||
}
|
||||
for (const auto& spec : kGeneratedDiagnosticBitsTail) {
|
||||
addGeneratedDiagnosticPoint(&next, short_address, spec);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& model : engine_.listModels()) {
|
||||
|
||||
Reference in New Issue
Block a user