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:
Tony
2026-05-04 02:26:09 +08:00
parent 694217eb2c
commit 7424b43bdd
13 changed files with 1133 additions and 40 deletions
@@ -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,
+156
View File
@@ -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);
+1
View File
@@ -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
@@ -52,9 +52,20 @@ bool gateway_bacnet_stack_upsert_object(
gateway_bacnet_object_kind_t object_kind,
uint32_t object_instance,
const char* object_name,
const char* description,
bool out_of_service,
uint32_t reliability);
const char* description,
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);
@@ -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() &&
+387 -28
View File
@@ -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,8 +2795,12 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
return SnapshotResponse(dali_domain_.dt5Snapshot(gateway_id.value(), address.value()),
"DT5 snapshot is unavailable");
}
return SnapshotResponse(dali_domain_.dt6Snapshot(gateway_id.value(), address.value()),
"DT6 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");
@@ -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()) {