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
+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() &&