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:
@@ -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() &&
|
||||
|
||||
Reference in New Issue
Block a user