feat(gateway_network): integrate GatewayBridgeService and add bridge handling

- Updated CMakeLists.txt to require gateway_bridge component.
- Modified GatewayNetworkService to include a pointer to GatewayBridgeService.
- Added new HTTP handlers for bridge GET and POST requests.
- Implemented query utility functions for handling request parameters.
- Enhanced response handling for bridge actions with JSON responses.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Tony
2026-05-01 03:54:02 +08:00
parent 2c1aa28d4f
commit d16c289626
19 changed files with 3186 additions and 10 deletions
@@ -3,6 +3,7 @@
#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
@@ -75,6 +76,23 @@ struct DaliRawFrame {
std::vector<uint8_t> data;
};
enum class DaliDt8SceneColorMode {
kDisabled,
kColorTemperature,
kRgb,
};
struct DaliDomainSnapshot {
uint8_t gateway_id{0};
int address{0};
std::string kind;
std::map<std::string, bool> bools;
std::map<std::string, int> ints;
std::map<std::string, double> numbers;
std::map<std::string, std::vector<int>> int_arrays;
std::map<std::string, std::vector<double>> number_arrays;
};
class DaliDomainService {
public:
DaliDomainService();
@@ -97,6 +115,24 @@ class DaliDomainService {
bool sendRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const;
bool sendExtRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const;
std::optional<uint8_t> queryRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const;
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> 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> dt8SceneColorReport(uint8_t gateway_id, int short_address,
int scene) const;
std::optional<DaliDomainSnapshot> dt8PowerOnLevelColorReport(uint8_t gateway_id,
int short_address) const;
std::optional<DaliDomainSnapshot> dt8SystemFailureLevelColorReport(uint8_t gateway_id,
int short_address) const;
bool storeDt8SceneSnapshot(uint8_t gateway_id, int short_address, int scene, int brightness,
DaliDt8SceneColorMode color_mode = DaliDt8SceneColorMode::kDisabled,
int color_temperature = 0, int red = 0, int green = 0,
int blue = 0) const;
bool storeDt8PowerOnLevelSnapshot(uint8_t gateway_id, int short_address, int level) const;
bool storeDt8SystemFailureLevelSnapshot(uint8_t gateway_id, int short_address, int level) const;
bool setBright(uint8_t gateway_id, int short_address, int brightness) const;
bool setColTempRaw(uint8_t gateway_id, int short_address, int mirek) const;
bool setColTemp(uint8_t gateway_id, int short_address, int kelvin) const;
+327
View File
@@ -18,6 +18,47 @@ constexpr const char* kTag = "dali_domain";
constexpr size_t kSerialRxPacketMaxBytes = 8;
constexpr UBaseType_t kSerialRxQueueDepth = 8;
DaliDomainSnapshot MakeSnapshot(uint8_t gateway_id, int address, const char* kind) {
DaliDomainSnapshot snapshot;
snapshot.gateway_id = gateway_id;
snapshot.address = address;
snapshot.kind = kind == nullptr ? "" : kind;
return snapshot;
}
template <typename T>
void PutOptionalInt(DaliDomainSnapshot& snapshot, const char* name, const std::optional<T>& value) {
if (value.has_value()) {
snapshot.ints[name] = static_cast<int>(value.value());
}
}
void PutOptionalBool(DaliDomainSnapshot& snapshot, const char* name,
const std::optional<bool>& value) {
if (value.has_value()) {
snapshot.bools[name] = value.value();
}
}
void PutOptionalNumber(DaliDomainSnapshot& snapshot, const char* name,
const std::optional<double>& value) {
if (value.has_value()) {
snapshot.numbers[name] = value.value();
}
}
Dt8SceneStoreColorMode ToDaliCppColorMode(DaliDt8SceneColorMode color_mode) {
switch (color_mode) {
case DaliDt8SceneColorMode::kColorTemperature:
return Dt8SceneStoreColorMode::colorTemperature;
case DaliDt8SceneColorMode::kRgb:
return Dt8SceneStoreColorMode::rgb;
case DaliDt8SceneColorMode::kDisabled:
default:
return Dt8SceneStoreColorMode::disabled;
}
}
struct SerialRxPacket {
size_t len{0};
uint8_t data[kSerialRxPacketMaxBytes]{};
@@ -403,6 +444,292 @@ std::optional<uint8_t> DaliDomainService::queryRaw(uint8_t gateway_id, uint8_t r
return channel->comm->queryRawNew(raw_addr, command);
}
std::optional<DaliDomainSnapshot> DaliDomainService::discoverDeviceTypes(
uint8_t gateway_id, int short_address, const std::vector<int>& fallback_types,
int max_next_types) const {
const auto* channel = findChannelByGateway(gateway_id);
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
const std::vector<int> fallback = fallback_types.empty() ? std::vector<int>{4, 5, 6, 8}
: fallback_types;
auto discovery = channel->dali->base.discoverDeviceTypes(short_address, fallback,
max_next_types);
if (!discovery.has_value()) {
return std::nullopt;
}
auto snapshot = MakeSnapshot(gateway_id, short_address, "device");
PutOptionalInt(snapshot, "rawQueryType", discovery->rawQueryType);
PutOptionalInt(snapshot, "primaryType", discovery->primaryType());
snapshot.int_arrays["types"] = discovery->types;
snapshot.int_arrays["extraTypes"] = discovery->extraTypes();
return snapshot;
}
std::optional<DaliDomainSnapshot> DaliDomainService::dt4Snapshot(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, "dt4");
auto& dt4 = channel->dali->dt4;
PutOptionalInt(snapshot, "extendedVersion", dt4.getExtendedVersion(short_address));
PutOptionalInt(snapshot, "dimmingCurve", dt4.getDimmingCurve(short_address));
PutOptionalInt(snapshot, "dimmerTemperatureRaw", dt4.getDimmerTemperatureRaw(short_address));
PutOptionalInt(snapshot, "rmsSupplyVoltageRaw", dt4.getRmsSupplyVoltageRaw(short_address));
PutOptionalInt(snapshot, "supplyFrequencyRaw", dt4.getSupplyFrequencyRaw(short_address));
PutOptionalInt(snapshot, "rmsLoadVoltageRaw", dt4.getRmsLoadVoltageRaw(short_address));
PutOptionalInt(snapshot, "rmsLoadCurrentRaw", dt4.getRmsLoadCurrentRaw(short_address));
PutOptionalInt(snapshot, "realLoadPowerRaw", dt4.getRealLoadPowerRaw(short_address));
PutOptionalInt(snapshot, "loadRatingRaw", dt4.getLoadRatingRaw(short_address));
PutOptionalNumber(snapshot, "rmsSupplyVoltageVolts", dt4.getRmsSupplyVoltageVolts(short_address));
PutOptionalNumber(snapshot, "supplyFrequencyHertz", dt4.getSupplyFrequencyHertz(short_address));
PutOptionalNumber(snapshot, "rmsLoadVoltageVolts", dt4.getRmsLoadVoltageVolts(short_address));
PutOptionalNumber(snapshot, "rmsLoadCurrentPercent", dt4.getRmsLoadCurrentPercent(short_address));
PutOptionalNumber(snapshot, "realLoadPowerWatts", dt4.getRealLoadPowerWatts(short_address));
PutOptionalNumber(snapshot, "loadRatingAmps", dt4.getLoadRatingAmps(short_address));
PutOptionalBool(snapshot, "referenceRunning", dt4.isReferenceRunning(short_address));
PutOptionalBool(snapshot, "referenceMeasurementFailed",
dt4.isReferenceMeasurementFailed(short_address));
if (const auto status = dt4.getDimmerStatus(short_address)) {
snapshot.ints["dimmerStatusRaw"] = status->raw();
snapshot.bools["leadingEdgeModeRunning"] = status->leadingEdgeModeRunning();
snapshot.bools["trailingEdgeModeRunning"] = status->trailingEdgeModeRunning();
snapshot.bools["referenceMeasurementRunning"] = status->referenceMeasurementRunning();
snapshot.bools["nonLogarithmicDimmingCurveActive"] =
status->nonLogarithmicDimmingCurveActive();
}
if (const auto features = dt4.getFeatures(short_address)) {
snapshot.ints["featuresRaw1"] = features->raw1();
snapshot.ints["featuresRaw2"] = features->raw2();
snapshot.ints["featuresRaw3"] = features->raw3();
snapshot.ints["dimmingMethodCode"] = features->dimmingMethodCode();
snapshot.bools["canQueryTemperature"] = features->canQueryTemperature();
snapshot.bools["canQuerySupplyVoltage"] = features->canQuerySupplyVoltage();
snapshot.bools["canQueryLoadVoltage"] = features->canQueryLoadVoltage();
snapshot.bools["canQueryLoadCurrent"] = features->canQueryLoadCurrent();
snapshot.bools["canQueryRealLoadPower"] = features->canQueryRealLoadPower();
snapshot.bools["canQueryLoadRating"] = features->canQueryLoadRating();
snapshot.bools["physicalSelectionSupported"] = features->physicalSelectionSupported();
snapshot.bools["canSelectNonLogarithmicDimmingCurve"] =
features->canSelectNonLogarithmicDimmingCurve();
}
if (const auto failure = dt4.getFailureStatus(short_address)) {
snapshot.ints["failureRaw1"] = failure->raw1();
snapshot.ints["failureRaw2"] = failure->raw2();
snapshot.bools["loadOverCurrentShutdown"] = failure->loadOverCurrentShutdown();
snapshot.bools["openCircuitDetected"] = failure->openCircuitDetected();
snapshot.bools["loadDecreaseDetected"] = failure->loadDecreaseDetected();
snapshot.bools["loadIncreaseDetected"] = failure->loadIncreaseDetected();
snapshot.bools["thermalShutdown"] = failure->thermalShutdown();
snapshot.bools["thermalOverloadReduction"] = failure->thermalOverloadReduction();
snapshot.bools["referenceMeasurementFailedStatus"] = failure->referenceMeasurementFailed();
snapshot.bools["supplyVoltageOutOfLimits"] = failure->supplyVoltageOutOfLimits();
snapshot.bools["loadVoltageOutOfLimits"] = failure->loadVoltageOutOfLimits();
snapshot.bools["loadCurrentOverloadReduction"] = failure->loadCurrentOverloadReduction();
}
return snapshot;
}
std::optional<DaliDomainSnapshot> DaliDomainService::dt5Snapshot(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, "dt5");
auto& dt5 = channel->dali->dt5;
PutOptionalInt(snapshot, "extendedVersion", dt5.getExtendedVersion(short_address));
PutOptionalInt(snapshot, "dimmingCurve", dt5.getDimmingCurve(short_address));
PutOptionalInt(snapshot, "outputLevelRaw", dt5.getOutputLevelRaw(short_address));
PutOptionalNumber(snapshot, "outputLevelVolts", dt5.getOutputLevelVolts(short_address));
if (const auto features = dt5.getConverterFeatures(short_address)) {
snapshot.ints["featuresRaw"] = features->raw();
snapshot.bools["outputRange0To10VSelectable"] = features->outputRange0To10VSelectable();
snapshot.bools["internalPullUpSelectable"] = features->internalPullUpSelectable();
snapshot.bools["outputFaultDetectionSelectable"] =
features->outputFaultDetectionSelectable();
snapshot.bools["mainsRelay"] = features->mainsRelay();
snapshot.bools["outputLevelQueryable"] = features->outputLevelQueryable();
snapshot.bools["nonLogarithmicDimmingCurveSupported"] =
features->nonLogarithmicDimmingCurveSupported();
snapshot.bools["physicalSelectionByOutputLossSupported"] =
features->physicalSelectionByOutputLossSupported();
snapshot.bools["physicalSelectionSwitchSupported"] =
features->physicalSelectionSwitchSupported();
}
if (const auto failure = dt5.getFailureStatus(short_address)) {
snapshot.ints["failureRaw"] = failure->raw();
snapshot.bools["outputFaultDetected"] = failure->outputFaultDetected();
}
if (const auto status = dt5.getConverterStatus(short_address)) {
snapshot.ints["converterStatusRaw"] = status->raw();
snapshot.bools["zeroToTenVoltOperation"] = status->zeroToTenVoltOperation();
snapshot.bools["internalPullUpOn"] = status->internalPullUpOn();
snapshot.bools["nonLogarithmicDimmingCurveActive"] =
status->nonLogarithmicDimmingCurveActive();
}
return snapshot;
}
std::optional<DaliDomainSnapshot> DaliDomainService::dt6Snapshot(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, "dt6");
auto& dt6 = channel->dali->dt6;
PutOptionalInt(snapshot, "extendedVersion", dt6.getExtendedVersion(short_address));
PutOptionalInt(snapshot, "dimmingCurve", dt6.getDimmingCurve(short_address));
PutOptionalInt(snapshot, "fastFadeTime", dt6.getFastFadeTime(short_address));
PutOptionalInt(snapshot, "minFastFadeTime", dt6.getMinFastFadeTime(short_address));
PutOptionalBool(snapshot, "currentProtectorEnabled",
dt6.isCurrentProtectorEnabled(short_address));
if (const auto gear = dt6.getGearType(short_address)) {
snapshot.ints["gearTypeRaw"] = gear->raw();
snapshot.bools["ledPowerSupplyIntegrated"] = gear->ledPowerSupplyIntegrated();
snapshot.bools["ledModuleIntegrated"] = gear->ledModuleIntegrated();
snapshot.bools["acSupplyPossible"] = gear->acSupplyPossible();
snapshot.bools["dcSupplyPossible"] = gear->dcSupplyPossible();
}
if (const auto modes = dt6.getPossibleOperatingModes(short_address)) {
snapshot.ints["possibleOperatingModesRaw"] = modes->raw();
snapshot.bools["pwmModePossible"] = modes->pwmModePossible();
snapshot.bools["amModePossible"] = modes->amModePossible();
snapshot.bools["currentControlledOutputPossible"] = modes->currentControlledOutput();
snapshot.bools["highCurrentPulseModePossible"] = modes->highCurrentPulseMode();
}
if (const auto features = dt6.getFeatures(short_address)) {
snapshot.ints["featuresRaw"] = features->raw();
snapshot.bools["canQueryShortCircuit"] = features->canQueryShortCircuit();
snapshot.bools["canQueryOpenCircuit"] = features->canQueryOpenCircuit();
snapshot.bools["canQueryLoadDecrease"] = features->canQueryLoadDecrease();
snapshot.bools["canQueryLoadIncrease"] = features->canQueryLoadIncrease();
snapshot.bools["canQueryCurrentProtector"] = features->canQueryCurrentProtector();
snapshot.bools["canQueryThermalShutdown"] = features->canQueryThermalShutdown();
snapshot.bools["canQueryThermalOverloadReduction"] =
features->canQueryThermalOverloadReduction();
snapshot.bools["physicalSelectionSupported"] = features->physicalSelectionSupported();
}
if (const auto failure = dt6.getFailureStatus(short_address)) {
snapshot.ints["failureRaw"] = failure->raw();
snapshot.bools["shortCircuit"] = failure->shortCircuit();
snapshot.bools["openCircuit"] = failure->openCircuit();
snapshot.bools["loadDecrease"] = failure->loadDecrease();
snapshot.bools["loadIncrease"] = failure->loadIncrease();
snapshot.bools["currentProtectorActive"] = failure->currentProtectorActive();
snapshot.bools["thermalShutdown"] = failure->thermalShutdown();
snapshot.bools["thermalOverloadReduction"] = failure->thermalOverloadReduction();
snapshot.bools["referenceMeasurementFailed"] = failure->referenceMeasurementFailed();
}
if (const auto mode = dt6.getOperatingMode(short_address)) {
snapshot.ints["operatingModeRaw"] = mode->raw();
snapshot.bools["pwmModeActive"] = mode->pwmModeActive();
snapshot.bools["amModeActive"] = mode->amModeActive();
snapshot.bools["currentControlledOutput"] = mode->currentControlledOutput();
snapshot.bools["highCurrentPulseModeActive"] = mode->highCurrentPulseModeActive();
snapshot.bools["nonLogarithmicDimmingCurveActive"] =
mode->nonLogarithmicDimmingCurveActive();
}
return snapshot;
}
std::optional<DaliDomainSnapshot> DaliDomainService::dt8SceneColorReport(
uint8_t gateway_id, int short_address, int scene) const {
const auto* channel = findChannelByGateway(gateway_id);
if (channel == nullptr || channel->dali == nullptr) {
return std::nullopt;
}
const auto report = channel->dali->dt8.getSceneColorReport(short_address, scene);
if (!report.has_value()) {
return std::nullopt;
}
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt8_scene");
snapshot.ints["scene"] = scene;
snapshot.ints["brightness"] = report->brightness;
snapshot.ints["colorType"] = report->colorTypeValue;
if (report->hasColorTemperature()) {
snapshot.ints["colorTemperature"] = report->colorTemperature.value();
}
if (report->hasXy()) {
snapshot.number_arrays["xy"] = report->xy;
}
return snapshot;
}
std::optional<DaliDomainSnapshot> DaliDomainService::dt8PowerOnLevelColorReport(
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 report = channel->dali->dt8.getPowerOnLevelColorReport(short_address);
if (!report.has_value()) {
return std::nullopt;
}
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt8_power_on");
snapshot.ints["level"] = report->level;
snapshot.ints["colorType"] = report->colorTypeValue;
if (report->hasColorTemperature()) {
snapshot.ints["colorTemperature"] = report->colorTemperature.value();
}
if (report->hasXy()) {
snapshot.number_arrays["xy"] = report->xy;
}
return snapshot;
}
std::optional<DaliDomainSnapshot> DaliDomainService::dt8SystemFailureLevelColorReport(
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 report = channel->dali->dt8.getSystemFailureLevelColorReport(short_address);
if (!report.has_value()) {
return std::nullopt;
}
auto snapshot = MakeSnapshot(gateway_id, short_address, "dt8_system_failure");
snapshot.ints["level"] = report->level;
snapshot.ints["colorType"] = report->colorTypeValue;
if (report->hasColorTemperature()) {
snapshot.ints["colorTemperature"] = report->colorTemperature.value();
}
if (report->hasXy()) {
snapshot.number_arrays["xy"] = report->xy;
}
return snapshot;
}
bool DaliDomainService::storeDt8SceneSnapshot(uint8_t gateway_id, int short_address, int scene,
int brightness,
DaliDt8SceneColorMode color_mode,
int color_temperature, int red, int green,
int blue) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->dt8.storeSceneSnapshot(short_address, scene, brightness,
ToDaliCppColorMode(color_mode), color_temperature,
red, green, blue);
}
bool DaliDomainService::storeDt8PowerOnLevelSnapshot(uint8_t gateway_id, int short_address,
int level) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->dt8.storePowerOnLevelSnapshot(short_address, level);
}
bool DaliDomainService::storeDt8SystemFailureLevelSnapshot(uint8_t gateway_id,
int short_address, int level) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->dt8.storeSystemFailureLevelSnapshot(short_address, level);
}
bool DaliDomainService::setBright(uint8_t gateway_id, int short_address, int brightness) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
+128
View File
@@ -0,0 +1,128 @@
if(NOT CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
idf_component_register(
INCLUDE_DIRS "include"
REQUIRES dali_cpp
)
return()
endif()
set(BACNET_STACK_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../bacnet-stack")
set(BACNET_SRC_ROOT "${BACNET_STACK_ROOT}/src")
set(BACNET_ESP32_PORT "${BACNET_STACK_ROOT}/ports/esp32/src")
idf_build_get_property(IDF_PATH IDF_PATH)
set(BACNET_CORE_SRCS
"${BACNET_SRC_ROOT}/bacnet/abort.c"
"${BACNET_SRC_ROOT}/bacnet/bacaddr.c"
"${BACNET_SRC_ROOT}/bacnet/bacapp.c"
"${BACNET_SRC_ROOT}/bacnet/bacdcode.c"
"${BACNET_SRC_ROOT}/bacnet/bacdest.c"
"${BACNET_SRC_ROOT}/bacnet/bacdevobjpropref.c"
"${BACNET_SRC_ROOT}/bacnet/bacerror.c"
"${BACNET_SRC_ROOT}/bacnet/bacint.c"
"${BACNET_SRC_ROOT}/bacnet/bacprop.c"
"${BACNET_SRC_ROOT}/bacnet/bacpropstates.c"
"${BACNET_SRC_ROOT}/bacnet/bacreal.c"
"${BACNET_SRC_ROOT}/bacnet/bacstr.c"
"${BACNET_SRC_ROOT}/bacnet/bactext.c"
"${BACNET_SRC_ROOT}/bacnet/bactimevalue.c"
"${BACNET_SRC_ROOT}/bacnet/calendar_entry.c"
"${BACNET_SRC_ROOT}/bacnet/cov.c"
"${BACNET_SRC_ROOT}/bacnet/datetime.c"
"${BACNET_SRC_ROOT}/bacnet/dcc.c"
"${BACNET_SRC_ROOT}/bacnet/iam.c"
"${BACNET_SRC_ROOT}/bacnet/ihave.c"
"${BACNET_SRC_ROOT}/bacnet/list_element.c"
"${BACNET_SRC_ROOT}/bacnet/memcopy.c"
"${BACNET_SRC_ROOT}/bacnet/npdu.c"
"${BACNET_SRC_ROOT}/bacnet/proplist.c"
"${BACNET_SRC_ROOT}/bacnet/reject.c"
"${BACNET_SRC_ROOT}/bacnet/rp.c"
"${BACNET_SRC_ROOT}/bacnet/rpm.c"
"${BACNET_SRC_ROOT}/bacnet/timestamp.c"
"${BACNET_SRC_ROOT}/bacnet/whohas.c"
"${BACNET_SRC_ROOT}/bacnet/whois.c"
"${BACNET_SRC_ROOT}/bacnet/wp.c"
)
set(BACNET_BASIC_SRCS
"${BACNET_SRC_ROOT}/bacnet/basic/binding/address.c"
"${BACNET_SRC_ROOT}/bacnet/basic/npdu/h_npdu.c"
"${BACNET_SRC_ROOT}/bacnet/basic/npdu/s_router.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/ao.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/av.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/bo.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/bv.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/device.c"
"${BACNET_SRC_ROOT}/bacnet/basic/object/msv.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/h_apdu.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/h_cov.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/h_dcc.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/h_noserv.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/h_rp.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/h_rpm.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/h_whohas.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/h_whois.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/h_wp.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/s_abort.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/s_error.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/s_iam.c"
"${BACNET_SRC_ROOT}/bacnet/basic/service/s_ihave.c"
"${BACNET_SRC_ROOT}/bacnet/basic/sys/bigend.c"
"${BACNET_SRC_ROOT}/bacnet/basic/sys/days.c"
"${BACNET_SRC_ROOT}/bacnet/basic/sys/datetime_mstimer.c"
"${BACNET_SRC_ROOT}/bacnet/basic/sys/debug.c"
"${BACNET_SRC_ROOT}/bacnet/basic/sys/dst.c"
"${BACNET_SRC_ROOT}/bacnet/basic/sys/keylist.c"
"${BACNET_SRC_ROOT}/bacnet/basic/sys/linear.c"
"${BACNET_SRC_ROOT}/bacnet/basic/sys/mstimer.c"
"${BACNET_SRC_ROOT}/bacnet/basic/tsm/tsm.c"
)
set(BACNET_PORT_SRCS
"${BACNET_ESP32_PORT}/bip.c"
"${BACNET_ESP32_PORT}/mstimer_init.c"
"${BACNET_ESP32_PORT}/bip_init.c"
"${BACNET_ESP32_PORT}/bvlc.c"
)
idf_component_register(
SRCS
"src/gateway_bacnet.cpp"
"src/gateway_bacnet_stack_port.c"
"src/bip_socket_lwip.cpp"
${BACNET_CORE_SRCS}
${BACNET_BASIC_SRCS}
${BACNET_PORT_SRCS}
INCLUDE_DIRS
"include"
"${BACNET_ESP32_PORT}"
"${BACNET_SRC_ROOT}"
PRIV_INCLUDE_DIRS
"${IDF_PATH}/components/esp_netif/include"
REQUIRES dali_cpp esp_netif freertos
PRIV_REQUIRES log lwip
)
target_compile_definitions(${COMPONENT_LIB} PRIVATE
BACDL_BIP=1
BACAPP_MINIMAL=1
BACNET_GATEWAY_EXTERNAL_OBJECT_TABLE=1
BACNET_PROPERTY_LISTS=1
BACNET_PROTOCOL_REVISION=16
BACNET_STACK_DEPRECATED_DISABLE=1
BBMD_ENABLED=0
BBMD_CLIENT_ENABLED=0
MAX_ADDRESS_CACHE=8
MAX_APDU=480
MAX_TSM_TRANSACTIONS=8
PRINT_ENABLED=0
)
target_compile_options(${COMPONENT_LIB} PRIVATE
$<$<COMPILE_LANGUAGE:C>:-Wno-missing-field-initializers>
$<$<COMPILE_LANGUAGE:C>:-Wno-old-style-declaration>
$<$<COMPILE_LANGUAGE:C>:-Wno-unused-function>
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -0,0 +1,83 @@
#pragma once
#include "bridge_model.hpp"
#include "model_value.hpp"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace gateway {
struct GatewayBacnetServerConfig {
uint32_t device_instance{4194303};
std::string device_name{"DALI Gateway"};
std::string local_address;
uint16_t udp_port{47808};
uint32_t task_stack_size{8192};
UBaseType_t task_priority{5};
};
struct GatewayBacnetObjectBinding {
uint8_t gateway_id{0};
std::string model_id;
std::string name;
BridgeObjectType object_type{BridgeObjectType::unknown};
uint32_t object_instance{0};
std::string property{"presentValue"};
};
struct GatewayBacnetServerStatus {
bool started{false};
uint32_t device_instance{0};
uint16_t udp_port{0};
size_t channel_count{0};
size_t object_count{0};
};
using GatewayBacnetWriteCallback =
std::function<bool(BridgeObjectType object_type, uint32_t object_instance,
const std::string& property, const DaliValue& value)>;
class GatewayBacnetServer {
public:
static GatewayBacnetServer& instance();
esp_err_t registerChannel(uint8_t gateway_id, const GatewayBacnetServerConfig& config,
std::vector<GatewayBacnetObjectBinding> bindings,
GatewayBacnetWriteCallback write_callback);
GatewayBacnetServerStatus status() const;
bool configCompatible(const GatewayBacnetServerConfig& config) const;
bool handleWrite(BridgeObjectType object_type, uint32_t object_instance,
const DaliValue& value);
private:
GatewayBacnetServer();
~GatewayBacnetServer();
GatewayBacnetServer(const GatewayBacnetServer&) = delete;
GatewayBacnetServer& operator=(const GatewayBacnetServer&) = delete;
struct ChannelRegistration;
struct RuntimeBinding;
esp_err_t startStackLocked(const GatewayBacnetServerConfig& config);
esp_err_t rebuildObjectsLocked();
static void TaskEntry(void* arg);
void taskLoop();
GatewayBacnetServerConfig active_config_;
std::vector<ChannelRegistration> channels_;
std::vector<RuntimeBinding> runtime_bindings_;
mutable SemaphoreHandle_t lock_{nullptr};
TaskHandle_t task_handle_{nullptr};
bool started_{false};
};
} // namespace gateway
@@ -0,0 +1,58 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum gateway_bacnet_object_kind {
GW_BACNET_OBJECT_UNKNOWN = 0,
GW_BACNET_OBJECT_ANALOG_VALUE,
GW_BACNET_OBJECT_ANALOG_OUTPUT,
GW_BACNET_OBJECT_BINARY_VALUE,
GW_BACNET_OBJECT_BINARY_OUTPUT,
GW_BACNET_OBJECT_MULTI_STATE_VALUE,
} gateway_bacnet_object_kind_t;
typedef enum gateway_bacnet_write_value_kind {
GW_BACNET_WRITE_VALUE_REAL = 1,
GW_BACNET_WRITE_VALUE_BOOLEAN = 2,
GW_BACNET_WRITE_VALUE_UNSIGNED = 3,
} gateway_bacnet_write_value_kind_t;
typedef struct gateway_bacnet_write_value {
gateway_bacnet_write_value_kind_t kind;
double real_value;
bool boolean_value;
uint32_t unsigned_value;
} gateway_bacnet_write_value_t;
typedef void (*gateway_bacnet_stack_write_callback_t)(
gateway_bacnet_object_kind_t object_kind,
uint32_t object_instance,
const gateway_bacnet_write_value_t* value,
void* context);
bool gateway_bacnet_stack_start(
uint32_t device_instance,
const char* device_name,
uint16_t udp_port,
gateway_bacnet_stack_write_callback_t write_callback,
void* callback_context);
void gateway_bacnet_stack_cleanup(void);
bool gateway_bacnet_stack_upsert_object(
gateway_bacnet_object_kind_t object_kind,
uint32_t object_instance,
const char* object_name,
const char* description);
void gateway_bacnet_stack_send_i_am(void);
void gateway_bacnet_stack_poll(uint16_t elapsed_ms);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,115 @@
#include "bip.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "lwip/inet.h"
#include "lwip/sockets.h"
#include <cerrno>
#include <cstdint>
#include <cstring>
namespace {
constexpr const char* kTag = "gateway_bacnet_socket";
int s_socket = -1;
bool NetifInfo(const char* ifkey, uint8_t* local_addr, uint8_t* netmask) {
esp_netif_t* netif = esp_netif_get_handle_from_ifkey(ifkey);
if (netif == nullptr) {
return false;
}
esp_netif_ip_info_t ip_info = {};
if (esp_netif_get_ip_info(netif, &ip_info) != ESP_OK || ip_info.ip.addr == 0) {
return false;
}
std::memcpy(local_addr, &ip_info.ip.addr, 4);
std::memcpy(netmask, &ip_info.netmask.addr, 4);
return true;
}
} // namespace
extern "C" bool bip_socket_init(uint16_t port) {
bip_socket_cleanup();
s_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (s_socket < 0) {
ESP_LOGE(kTag, "failed to create BACnet/IP UDP socket errno=%d", errno);
return false;
}
int enabled = 1;
setsockopt(s_socket, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled));
setsockopt(s_socket, SOL_SOCKET, SO_BROADCAST, &enabled, sizeof(enabled));
sockaddr_in local = {};
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port = htons(port);
if (bind(s_socket, reinterpret_cast<sockaddr*>(&local), sizeof(local)) != 0) {
ESP_LOGE(kTag, "failed to bind BACnet/IP UDP port %u errno=%d", port, errno);
bip_socket_cleanup();
return false;
}
timeval timeout = {};
timeout.tv_sec = 0;
timeout.tv_usec = 1000;
setsockopt(s_socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
ESP_LOGI(kTag, "BACnet/IP UDP socket bound on port %u", port);
return true;
}
extern "C" int bip_socket_send(const uint8_t* dest_addr, uint16_t dest_port,
const uint8_t* mtu, uint16_t mtu_len) {
if (s_socket < 0 || dest_addr == nullptr || mtu == nullptr || mtu_len == 0) {
return -1;
}
sockaddr_in dest = {};
dest.sin_family = AF_INET;
dest.sin_port = htons(dest_port);
std::memcpy(&dest.sin_addr.s_addr, dest_addr, 4);
const int sent = sendto(s_socket, mtu, mtu_len, 0, reinterpret_cast<sockaddr*>(&dest),
sizeof(dest));
return sent == mtu_len ? sent : -1;
}
extern "C" int bip_socket_receive(uint8_t* buf, uint16_t buf_len, uint8_t* src_addr,
uint16_t* src_port) {
if (s_socket < 0 || buf == nullptr || src_addr == nullptr || src_port == nullptr ||
buf_len == 0) {
return 0;
}
sockaddr_in source = {};
socklen_t source_len = sizeof(source);
const int received = recvfrom(s_socket, buf, buf_len, 0, reinterpret_cast<sockaddr*>(&source),
&source_len);
if (received <= 0) {
return 0;
}
std::memcpy(src_addr, &source.sin_addr.s_addr, 4);
*src_port = ntohs(source.sin_port);
return received;
}
extern "C" void bip_socket_cleanup(void) {
if (s_socket >= 0) {
close(s_socket);
s_socket = -1;
}
}
extern "C" bool bip_get_local_network_info(uint8_t* local_addr, uint8_t* netmask) {
if (local_addr == nullptr || netmask == nullptr) {
return false;
}
return NetifInfo("WIFI_STA_DEF", local_addr, netmask) ||
NetifInfo("ETH_DEF", local_addr, netmask) ||
NetifInfo("WIFI_AP_DEF", local_addr, netmask);
}
@@ -0,0 +1,330 @@
#include "gateway_bacnet.hpp"
#include "gateway_bacnet_stack_port.h"
#include "esp_log.h"
#include "freertos/semphr.h"
#include <algorithm>
#include <map>
#include <optional>
#include <set>
#include <string_view>
#include <utility>
namespace gateway {
namespace {
constexpr const char* kTag = "gateway_bacnet";
constexpr TickType_t kPollDelayTicks = pdMS_TO_TICKS(10);
class LockGuard {
public:
explicit LockGuard(SemaphoreHandle_t lock) : lock_(lock) {
if (lock_ != nullptr) {
xSemaphoreTakeRecursive(lock_, portMAX_DELAY);
}
}
~LockGuard() {
if (lock_ != nullptr) {
xSemaphoreGiveRecursive(lock_);
}
}
private:
SemaphoreHandle_t lock_;
};
constexpr uint32_t kMaxBacnetInstance = 4194303;
GatewayBacnetServer* g_server = nullptr;
gateway_bacnet_object_kind_t ToBacnetKind(BridgeObjectType type) {
switch (type) {
case BridgeObjectType::analogValue:
return GW_BACNET_OBJECT_ANALOG_VALUE;
case BridgeObjectType::analogOutput:
return GW_BACNET_OBJECT_ANALOG_OUTPUT;
case BridgeObjectType::binaryValue:
return GW_BACNET_OBJECT_BINARY_VALUE;
case BridgeObjectType::binaryOutput:
return GW_BACNET_OBJECT_BINARY_OUTPUT;
case BridgeObjectType::multiStateValue:
return GW_BACNET_OBJECT_MULTI_STATE_VALUE;
default:
return GW_BACNET_OBJECT_UNKNOWN;
}
}
BridgeObjectType FromBacnetKind(gateway_bacnet_object_kind_t kind) {
switch (kind) {
case GW_BACNET_OBJECT_ANALOG_VALUE:
return BridgeObjectType::analogValue;
case GW_BACNET_OBJECT_ANALOG_OUTPUT:
return BridgeObjectType::analogOutput;
case GW_BACNET_OBJECT_BINARY_VALUE:
return BridgeObjectType::binaryValue;
case GW_BACNET_OBJECT_BINARY_OUTPUT:
return BridgeObjectType::binaryOutput;
case GW_BACNET_OBJECT_MULTI_STATE_VALUE:
return BridgeObjectType::multiStateValue;
default:
return BridgeObjectType::unknown;
}
}
bool IsSupportedObjectType(BridgeObjectType type) {
return ToBacnetKind(type) != GW_BACNET_OBJECT_UNKNOWN;
}
std::string ObjectName(const GatewayBacnetObjectBinding& binding) {
if (!binding.name.empty()) {
return binding.name;
}
if (!binding.model_id.empty()) {
return "DALI " + binding.model_id;
}
return "DALI BACnet " + std::to_string(binding.object_instance);
}
DaliValue StackWriteValueToDali(const gateway_bacnet_write_value_t& value) {
switch (value.kind) {
case GW_BACNET_WRITE_VALUE_REAL:
return DaliValue(value.real_value);
case GW_BACNET_WRITE_VALUE_BOOLEAN:
return DaliValue(value.boolean_value);
case GW_BACNET_WRITE_VALUE_UNSIGNED:
return DaliValue(static_cast<int>(value.unsigned_value));
default:
return DaliValue();
}
}
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) {
return;
}
g_server->handleWrite(FromBacnetKind(object_kind), object_instance, StackWriteValueToDali(*value));
}
} // namespace
struct GatewayBacnetServer::ChannelRegistration {
uint8_t gateway_id{0};
GatewayBacnetServerConfig config;
std::vector<GatewayBacnetObjectBinding> bindings;
GatewayBacnetWriteCallback write_callback;
};
struct GatewayBacnetServer::RuntimeBinding {
uint8_t gateway_id{0};
BridgeObjectType object_type{BridgeObjectType::unknown};
uint32_t object_instance{0};
std::string model_id;
std::string property{"presentValue"};
GatewayBacnetWriteCallback write_callback;
};
GatewayBacnetServer& GatewayBacnetServer::instance() {
static GatewayBacnetServer server;
return server;
}
GatewayBacnetServer::GatewayBacnetServer() : lock_(xSemaphoreCreateRecursiveMutex()) {
g_server = this;
}
GatewayBacnetServer::~GatewayBacnetServer() {
if (lock_ != nullptr) {
vSemaphoreDelete(lock_);
lock_ = nullptr;
}
}
bool GatewayBacnetServer::configCompatible(const GatewayBacnetServerConfig& config) const {
LockGuard guard(lock_);
return !started_ || (active_config_.udp_port == config.udp_port &&
active_config_.device_instance == config.device_instance);
}
GatewayBacnetServerStatus GatewayBacnetServer::status() const {
LockGuard guard(lock_);
return GatewayBacnetServerStatus{started_,
active_config_.device_instance,
active_config_.udp_port,
channels_.size(),
runtime_bindings_.size()};
}
esp_err_t GatewayBacnetServer::registerChannel(
uint8_t gateway_id, const GatewayBacnetServerConfig& config,
std::vector<GatewayBacnetObjectBinding> bindings,
GatewayBacnetWriteCallback write_callback) {
if (write_callback == nullptr) {
return ESP_ERR_INVALID_ARG;
}
bindings.erase(std::remove_if(bindings.begin(), bindings.end(), [](const auto& binding) {
return !IsSupportedObjectType(binding.object_type) ||
binding.object_instance > kMaxBacnetInstance;
}),
bindings.end());
if (bindings.empty()) {
return ESP_ERR_NOT_FOUND;
}
LockGuard guard(lock_);
if (started_ && !configCompatible(config)) {
return ESP_ERR_INVALID_STATE;
}
auto channel = std::find_if(channels_.begin(), channels_.end(), [gateway_id](const auto& item) {
return item.gateway_id == gateway_id;
});
ChannelRegistration registration{gateway_id, config, std::move(bindings),
std::move(write_callback)};
if (channel == channels_.end()) {
channels_.push_back(std::move(registration));
} else {
*channel = std::move(registration);
}
esp_err_t err = startStackLocked(config);
if (err != ESP_OK) {
return err;
}
return rebuildObjectsLocked();
}
esp_err_t GatewayBacnetServer::startStackLocked(const GatewayBacnetServerConfig& config) {
if (started_) {
return ESP_OK;
}
active_config_ = config;
if (active_config_.device_name.empty()) {
active_config_.device_name = "DALI Gateway";
}
if (active_config_.udp_port == 0) {
active_config_.udp_port = 47808;
}
if (active_config_.task_stack_size < 6144) {
active_config_.task_stack_size = 6144;
}
if (!gateway_bacnet_stack_start(active_config_.device_instance, active_config_.device_name.c_str(),
active_config_.udp_port, HandleStackWrite, this)) {
ESP_LOGE(kTag, "failed to initialize BACnet/IP port %u", active_config_.udp_port);
return ESP_FAIL;
}
const BaseType_t created = xTaskCreate(&GatewayBacnetServer::TaskEntry, "gw_bacnet_ip",
active_config_.task_stack_size, this,
active_config_.task_priority, &task_handle_);
if (created != pdPASS) {
task_handle_ = nullptr;
gateway_bacnet_stack_cleanup();
return ESP_ERR_NO_MEM;
}
started_ = true;
gateway_bacnet_stack_send_i_am();
ESP_LOGI(kTag, "BACnet/IP server started device=%lu port=%u",
static_cast<unsigned long>(active_config_.device_instance), active_config_.udp_port);
return ESP_OK;
}
esp_err_t GatewayBacnetServer::rebuildObjectsLocked() {
runtime_bindings_.clear();
std::set<std::pair<BridgeObjectType, uint32_t>> used_objects;
for (const auto& channel : channels_) {
for (const auto& binding : channel.bindings) {
const auto key = std::make_pair(binding.object_type, binding.object_instance);
if (used_objects.find(key) != used_objects.end()) {
ESP_LOGE(kTag, "duplicate BACnet object type=%d instance=%lu",
static_cast<int>(binding.object_type),
static_cast<unsigned long>(binding.object_instance));
return ESP_ERR_INVALID_STATE;
}
used_objects.insert(key);
const std::string name = ObjectName(binding);
if (!gateway_bacnet_stack_upsert_object(ToBacnetKind(binding.object_type),
binding.object_instance, name.c_str(),
binding.model_id.c_str())) {
return ESP_FAIL;
}
runtime_bindings_.push_back(RuntimeBinding{channel.gateway_id,
binding.object_type,
binding.object_instance,
binding.model_id,
binding.property.empty() ? "presentValue"
: binding.property,
channel.write_callback});
}
}
ESP_LOGI(kTag, "BACnet/IP object table updated objects=%u",
static_cast<unsigned>(runtime_bindings_.size()));
return ESP_OK;
}
bool GatewayBacnetServer::handleWrite(BridgeObjectType object_type, uint32_t object_instance,
const DaliValue& value) {
GatewayBacnetWriteCallback callback;
std::string property;
std::string model_id;
uint8_t gateway_id = 0;
{
LockGuard guard(lock_);
const auto binding = std::find_if(runtime_bindings_.begin(), runtime_bindings_.end(),
[object_type, object_instance](const auto& item) {
return item.object_type == object_type &&
item.object_instance == object_instance;
});
if (binding == runtime_bindings_.end()) {
ESP_LOGW(kTag, "write for unmapped BACnet object type=%d instance=%lu",
static_cast<int>(object_type), static_cast<unsigned long>(object_instance));
return false;
}
callback = binding->write_callback;
property = binding->property;
model_id = binding->model_id;
gateway_id = binding->gateway_id;
}
const bool ok = callback != nullptr && callback(object_type, object_instance, property, value);
if (!ok) {
ESP_LOGW(kTag, "gateway=%u BACnet write failed model=%s object=%lu",
gateway_id, model_id.c_str(), static_cast<unsigned long>(object_instance));
}
return ok;
}
void GatewayBacnetServer::TaskEntry(void* arg) {
static_cast<GatewayBacnetServer*>(arg)->taskLoop();
}
void GatewayBacnetServer::taskLoop() {
TickType_t last_timer = xTaskGetTickCount();
while (true) {
const TickType_t now = xTaskGetTickCount();
const TickType_t elapsed = now - last_timer;
uint16_t elapsed_ms = 0;
if (elapsed >= pdMS_TO_TICKS(1000)) {
elapsed_ms = static_cast<uint16_t>(elapsed * portTICK_PERIOD_MS);
last_timer = now;
}
{
LockGuard guard(lock_);
gateway_bacnet_stack_poll(elapsed_ms);
}
vTaskDelay(kPollDelayTicks);
}
}
} // namespace gateway
@@ -0,0 +1,288 @@
#include "gateway_bacnet_stack_port.h"
#include <stddef.h>
#include <string.h>
#include "bacnet/apdu.h"
#include "bacnet/basic/binding/address.h"
#include "bacnet/basic/object/ao.h"
#include "bacnet/basic/object/av.h"
#include "bacnet/basic/object/bo.h"
#include "bacnet/basic/object/bv.h"
#include "bacnet/basic/object/device.h"
#include "bacnet/basic/object/msv.h"
#include "bacnet/basic/service/h_apdu.h"
#include "bacnet/basic/services.h"
#include "bacnet/basic/tsm/tsm.h"
#include "bacnet/bacdef.h"
#include "bacnet/npdu.h"
#include "bip.h"
static gateway_bacnet_stack_write_callback_t Write_Callback;
static void* Write_Callback_Context;
static uint8_t Rx_Buffer[BIP_MPDU_MAX];
static const char Multistate_Value_States[] =
"State 1\0"
"State 2\0"
"State 3\0"
"State 4\0"
"State 5\0"
"State 6\0"
"State 7\0"
"State 8\0"
"State 9\0"
"State 10\0"
"State 11\0"
"State 12\0"
"State 13\0"
"State 14\0"
"State 15\0"
"State 16\0";
static void notify_write_real(
gateway_bacnet_object_kind_t object_kind, uint32_t object_instance, double value)
{
if (Write_Callback) {
gateway_bacnet_write_value_t write_value = {
.kind = GW_BACNET_WRITE_VALUE_REAL,
.real_value = value,
.boolean_value = false,
.unsigned_value = 0,
};
Write_Callback(object_kind, object_instance, &write_value, Write_Callback_Context);
}
}
static void notify_write_boolean(
gateway_bacnet_object_kind_t object_kind, uint32_t object_instance, bool value)
{
if (Write_Callback) {
gateway_bacnet_write_value_t write_value = {
.kind = GW_BACNET_WRITE_VALUE_BOOLEAN,
.real_value = 0.0,
.boolean_value = value,
.unsigned_value = 0,
};
Write_Callback(object_kind, object_instance, &write_value, Write_Callback_Context);
}
}
static void notify_write_unsigned(
gateway_bacnet_object_kind_t object_kind, uint32_t object_instance, uint32_t value)
{
if (Write_Callback) {
gateway_bacnet_write_value_t write_value = {
.kind = GW_BACNET_WRITE_VALUE_UNSIGNED,
.real_value = 0.0,
.boolean_value = false,
.unsigned_value = value,
};
Write_Callback(object_kind, object_instance, &write_value, Write_Callback_Context);
}
}
static void analog_value_write(uint32_t object_instance, float old_value, float value)
{
(void)old_value;
notify_write_real(GW_BACNET_OBJECT_ANALOG_VALUE, object_instance, value);
}
static void analog_output_write(uint32_t object_instance, float old_value, float value)
{
(void)old_value;
notify_write_real(GW_BACNET_OBJECT_ANALOG_OUTPUT, object_instance, value);
}
static void binary_value_write(
uint32_t object_instance, BACNET_BINARY_PV old_value, BACNET_BINARY_PV value)
{
(void)old_value;
notify_write_boolean(GW_BACNET_OBJECT_BINARY_VALUE, object_instance, value == BINARY_ACTIVE);
}
static void binary_output_write(
uint32_t object_instance, BACNET_BINARY_PV old_value, BACNET_BINARY_PV value)
{
(void)old_value;
notify_write_boolean(GW_BACNET_OBJECT_BINARY_OUTPUT, object_instance, value == BINARY_ACTIVE);
}
static void multistate_value_write(uint32_t object_instance, uint32_t old_value, uint32_t value)
{
(void)old_value;
notify_write_unsigned(GW_BACNET_OBJECT_MULTI_STATE_VALUE, object_instance, value);
}
static object_functions_t Object_Table[] = {
{ OBJECT_DEVICE, NULL, Device_Count, Device_Index_To_Instance,
Device_Valid_Object_Instance_Number, Device_Object_Name, Device_Read_Property_Local,
Device_Write_Property_Local, Device_Property_Lists, DeviceGetRRInfo, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, Device_Writable_Property_List },
{ OBJECT_ANALOG_VALUE, Analog_Value_Init, Analog_Value_Count,
Analog_Value_Index_To_Instance, Analog_Value_Valid_Instance, Analog_Value_Object_Name,
Analog_Value_Read_Property, Analog_Value_Write_Property, Analog_Value_Property_Lists,
NULL, NULL, Analog_Value_Encode_Value_List, Analog_Value_Change_Of_Value,
Analog_Value_Change_Of_Value_Clear, NULL, NULL, NULL, Analog_Value_Create,
Analog_Value_Delete, NULL, Analog_Value_Writable_Property_List },
{ OBJECT_ANALOG_OUTPUT, Analog_Output_Init, Analog_Output_Count,
Analog_Output_Index_To_Instance, Analog_Output_Valid_Instance, Analog_Output_Object_Name,
Analog_Output_Read_Property, Analog_Output_Write_Property, Analog_Output_Property_Lists,
NULL, NULL, Analog_Output_Encode_Value_List, Analog_Output_Change_Of_Value,
Analog_Output_Change_Of_Value_Clear, NULL, NULL, NULL, Analog_Output_Create,
Analog_Output_Delete, NULL, Analog_Output_Writable_Property_List },
{ OBJECT_BINARY_VALUE, Binary_Value_Init, Binary_Value_Count,
Binary_Value_Index_To_Instance, Binary_Value_Valid_Instance, Binary_Value_Object_Name,
Binary_Value_Read_Property, Binary_Value_Write_Property, Binary_Value_Property_Lists,
NULL, NULL, Binary_Value_Encode_Value_List, Binary_Value_Change_Of_Value,
Binary_Value_Change_Of_Value_Clear, NULL, NULL, NULL, Binary_Value_Create,
Binary_Value_Delete, NULL, Binary_Value_Writable_Property_List },
{ OBJECT_BINARY_OUTPUT, Binary_Output_Init, Binary_Output_Count,
Binary_Output_Index_To_Instance, Binary_Output_Valid_Instance, Binary_Output_Object_Name,
Binary_Output_Read_Property, Binary_Output_Write_Property, Binary_Output_Property_Lists,
NULL, NULL, Binary_Output_Encode_Value_List, Binary_Output_Change_Of_Value,
Binary_Output_Change_Of_Value_Clear, NULL, NULL, NULL, Binary_Output_Create,
Binary_Output_Delete, NULL, Binary_Output_Writable_Property_List },
{ OBJECT_MULTI_STATE_VALUE, Multistate_Value_Init, Multistate_Value_Count,
Multistate_Value_Index_To_Instance, Multistate_Value_Valid_Instance,
Multistate_Value_Object_Name, Multistate_Value_Read_Property,
Multistate_Value_Write_Property, Multistate_Value_Property_Lists, NULL, NULL,
Multistate_Value_Encode_Value_List, Multistate_Value_Change_Of_Value,
Multistate_Value_Change_Of_Value_Clear, NULL, NULL, NULL, Multistate_Value_Create,
Multistate_Value_Delete, NULL, Multistate_Value_Writable_Property_List },
{ MAX_BACNET_OBJECT_TYPE, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL },
};
bool gateway_bacnet_stack_start(
uint32_t device_instance,
const char* device_name,
uint16_t udp_port,
gateway_bacnet_stack_write_callback_t write_callback,
void* callback_context)
{
if (!device_name || device_name[0] == '\0') {
device_name = "DALI Gateway";
}
if (udp_port == 0) {
udp_port = 47808;
}
Write_Callback = write_callback;
Write_Callback_Context = callback_context;
address_init();
Device_Set_Object_Instance_Number(device_instance);
Device_Init(Object_Table);
Device_Object_Name_ANSI_Init(device_name);
Device_Set_Vendor_Name("TonyCloud", strlen("TonyCloud"));
Device_Set_Vendor_Identifier(260);
Device_Set_Model_Name("DALI Gateway", strlen("DALI Gateway"));
Device_Set_Description("DALI BACnet/IP bridge", strlen("DALI BACnet/IP bridge"));
Analog_Value_Write_Present_Value_Callback_Set(analog_value_write);
Analog_Output_Write_Present_Value_Callback_Set(analog_output_write);
Binary_Value_Write_Present_Value_Callback_Set(binary_value_write);
Binary_Output_Write_Present_Value_Callback_Set(binary_output_write);
Multistate_Value_Write_Present_Value_Callback_Set(multistate_value_write);
apdu_set_unrecognized_service_handler_handler(handler_unrecognized_service);
apdu_set_unconfirmed_handler(SERVICE_UNCONFIRMED_WHO_IS, handler_who_is);
apdu_set_unconfirmed_handler(SERVICE_UNCONFIRMED_WHO_HAS, handler_who_has);
apdu_set_confirmed_handler(SERVICE_CONFIRMED_READ_PROPERTY, handler_read_property);
apdu_set_confirmed_handler(
SERVICE_CONFIRMED_READ_PROP_MULTIPLE, handler_read_property_multiple);
apdu_set_confirmed_handler(SERVICE_CONFIRMED_WRITE_PROPERTY, handler_write_property);
apdu_set_confirmed_handler(
SERVICE_CONFIRMED_DEVICE_COMMUNICATION_CONTROL, handler_device_communication_control);
return bip_init(udp_port);
}
void gateway_bacnet_stack_cleanup(void)
{
bip_cleanup();
Write_Callback = NULL;
Write_Callback_Context = NULL;
}
bool gateway_bacnet_stack_upsert_object(
gateway_bacnet_object_kind_t object_kind,
uint32_t object_instance,
const char* object_name,
const char* description)
{
if (!object_name || object_name[0] == '\0') {
object_name = "DALI BACnet Object";
}
if (!description) {
description = "";
}
switch (object_kind) {
case GW_BACNET_OBJECT_ANALOG_VALUE:
if (!Analog_Value_Valid_Instance(object_instance)) {
Analog_Value_Create(object_instance);
}
Analog_Value_Name_Set(object_instance, object_name);
Analog_Value_Description_Set(object_instance, description);
Analog_Value_Units_Set(object_instance, UNITS_PERCENT);
Analog_Value_Present_Value_Set(object_instance, 0.0f, BACNET_NO_PRIORITY);
return true;
case GW_BACNET_OBJECT_ANALOG_OUTPUT:
if (!Analog_Output_Valid_Instance(object_instance)) {
Analog_Output_Create(object_instance);
}
Analog_Output_Name_Set(object_instance, object_name);
Analog_Output_Description_Set(object_instance, description);
Analog_Output_Units_Set(object_instance, UNITS_PERCENT);
Analog_Output_Present_Value_Set(object_instance, 0.0f, BACNET_MAX_PRIORITY);
return true;
case GW_BACNET_OBJECT_BINARY_VALUE:
if (!Binary_Value_Valid_Instance(object_instance)) {
Binary_Value_Create(object_instance);
}
Binary_Value_Name_Set(object_instance, object_name);
Binary_Value_Description_Set(object_instance, description);
Binary_Value_Write_Enable(object_instance);
Binary_Value_Present_Value_Set(object_instance, BINARY_INACTIVE);
return true;
case GW_BACNET_OBJECT_BINARY_OUTPUT:
if (!Binary_Output_Valid_Instance(object_instance)) {
Binary_Output_Create(object_instance);
}
Binary_Output_Name_Set(object_instance, object_name);
Binary_Output_Description_Set(object_instance, description);
Binary_Output_Present_Value_Set(object_instance, BINARY_INACTIVE, BACNET_MAX_PRIORITY);
return true;
case GW_BACNET_OBJECT_MULTI_STATE_VALUE:
if (!Multistate_Value_Valid_Instance(object_instance)) {
Multistate_Value_Create(object_instance);
}
Multistate_Value_Name_Set(object_instance, object_name);
Multistate_Value_Description_Set(object_instance, description);
Multistate_Value_State_Text_List_Set(object_instance, Multistate_Value_States);
Multistate_Value_Write_Enable(object_instance);
Multistate_Value_Present_Value_Set(object_instance, 1);
return true;
default:
return false;
}
}
void gateway_bacnet_stack_send_i_am(void)
{
Send_I_Am(&Handler_Transmit_Buffer[0]);
}
void gateway_bacnet_stack_poll(uint16_t elapsed_ms)
{
BACNET_ADDRESS src = { 0 };
uint16_t pdu_len = bip_receive(&src, Rx_Buffer, sizeof(Rx_Buffer), 0);
if (pdu_len > 0) {
npdu_handler(&src, Rx_Buffer, pdu_len);
}
if (elapsed_ms > 0) {
tsm_timer_milliseconds(elapsed_ms);
Device_Timer(elapsed_ms);
}
}
+18
View File
@@ -0,0 +1,18 @@
set(GATEWAY_BRIDGE_REQUIRES
dali_domain
dali_cpp
espressif__cjson
freertos
log
lwip
nvs_flash
)
idf_component_register(
SRCS "src/gateway_bridge.cpp"
INCLUDE_DIRS "include"
REQUIRES ${GATEWAY_BRIDGE_REQUIRES}
PRIV_REQUIRES gateway_bacnet
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -0,0 +1,59 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
namespace gateway {
class DaliDomainService;
struct GatewayBridgeServiceConfig {
bool bridge_enabled{true};
bool modbus_enabled{true};
bool modbus_startup_enabled{false};
bool bacnet_enabled{false};
bool bacnet_startup_enabled{false};
bool cloud_enabled{true};
bool cloud_startup_enabled{false};
uint32_t modbus_task_stack_size{6144};
UBaseType_t modbus_task_priority{4};
uint32_t bacnet_task_stack_size{8192};
UBaseType_t bacnet_task_priority{5};
};
struct GatewayBridgeHttpResponse {
esp_err_t err{ESP_OK};
std::string body;
};
class GatewayBridgeService {
public:
GatewayBridgeService(DaliDomainService& dali_domain,
GatewayBridgeServiceConfig config = {});
~GatewayBridgeService();
esp_err_t start();
GatewayBridgeHttpResponse handleGet(const std::string& action, int gateway_id = -1,
const std::string& query = {}) const;
GatewayBridgeHttpResponse handlePost(const std::string& action, int gateway_id,
const std::string& body);
private:
struct ChannelRuntime;
ChannelRuntime* findRuntime(uint8_t gateway_id);
const ChannelRuntime* findRuntime(uint8_t gateway_id) const;
DaliDomainService& dali_domain_;
GatewayBridgeServiceConfig config_;
std::vector<std::unique_ptr<ChannelRuntime>> runtimes_;
};
} // namespace gateway
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
idf_component_register(
SRCS "src/gateway_network.cpp"
INCLUDE_DIRS "include"
REQUIRES dali_domain esp_event esp_http_server esp_netif esp_wifi freertos gateway_controller gateway_runtime log lwip espressif__cjson
REQUIRES dali_domain esp_event esp_http_server esp_netif esp_wifi freertos gateway_bridge gateway_controller gateway_runtime log lwip espressif__cjson
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -21,6 +21,7 @@ namespace gateway {
class GatewayController;
class DaliDomainService;
class GatewayBridgeService;
struct DaliRawFrame;
struct GatewayNetworkServiceConfig {
@@ -48,7 +49,9 @@ struct GatewayNetworkServiceConfig {
class GatewayNetworkService {
public:
GatewayNetworkService(GatewayController& controller, GatewayRuntime& runtime,
DaliDomainService& dali_domain, GatewayNetworkServiceConfig config = {});
DaliDomainService& dali_domain,
GatewayNetworkServiceConfig config = {},
GatewayBridgeService* bridge_service = nullptr);
esp_err_t start();
@@ -58,6 +61,8 @@ class GatewayNetworkService {
static esp_err_t HandleInfoGet(httpd_req_t* req);
static esp_err_t HandleCommandGet(httpd_req_t* req);
static esp_err_t HandleCommandPost(httpd_req_t* req);
static esp_err_t HandleBridgeGet(httpd_req_t* req);
static esp_err_t HandleBridgePost(httpd_req_t* req);
static esp_err_t HandleLedOnGet(httpd_req_t* req);
static esp_err_t HandleLedOffGet(httpd_req_t* req);
static esp_err_t HandleJqJsGet(httpd_req_t* req);
@@ -91,12 +96,14 @@ class GatewayNetworkService {
std::string deviceInfoJson() const;
std::string deviceInfoDoubleEncodedJson() const;
std::string gatewaySnapshotJson();
esp_err_t sendBridgeResponse(httpd_req_t* req, bool post);
void setStatusLed(bool on);
GatewayController& controller_;
GatewayRuntime& runtime_;
DaliDomainService& dali_domain_;
GatewayNetworkServiceConfig config_;
GatewayBridgeService* bridge_service_{nullptr};
bool started_{false};
httpd_handle_t http_server_{nullptr};
esp_netif_t* wifi_sta_netif_{nullptr};
@@ -1,6 +1,7 @@
#include "gateway_network.hpp"
#include "dali_domain.hpp"
#include "gateway_bridge.hpp"
#include "gateway_controller.hpp"
#include "gateway_runtime.hpp"
@@ -18,6 +19,7 @@
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <string_view>
#include <utility>
#include <vector>
@@ -182,14 +184,79 @@ esp_err_t RegisterUri(httpd_handle_t server, const char* uri, httpd_method_t met
return httpd_register_uri_handler(server, &route);
}
std::string QueryValue(httpd_req_t* req, const char* key) {
if (req == nullptr || key == nullptr) {
return {};
}
const size_t len = httpd_req_get_url_query_len(req) + 1;
if (len <= 1) {
return {};
}
std::string query(len, '\0');
if (httpd_req_get_url_query_str(req, query.data(), query.size()) != ESP_OK) {
return {};
}
char value[64] = {0};
if (httpd_query_key_value(query.c_str(), key, value, sizeof(value)) != ESP_OK) {
return {};
}
return std::string(value);
}
std::string QueryString(httpd_req_t* req) {
if (req == nullptr) {
return {};
}
const size_t len = httpd_req_get_url_query_len(req) + 1;
if (len <= 1) {
return {};
}
std::string query(len, '\0');
if (httpd_req_get_url_query_str(req, query.data(), query.size()) != ESP_OK) {
return {};
}
if (!query.empty() && query.back() == '\0') {
query.pop_back();
}
return query;
}
std::optional<uint8_t> QueryGatewayId(httpd_req_t* req) {
const auto raw = QueryValue(req, "gw");
if (raw.empty()) {
return std::nullopt;
}
char* end = nullptr;
const long parsed = std::strtol(raw.c_str(), &end, 10);
if (end == raw.c_str() || *end != '\0' || parsed < 0 || parsed > 255) {
return std::nullopt;
}
return static_cast<uint8_t>(parsed);
}
esp_err_t SendJsonResponse(httpd_req_t* req, const GatewayBridgeHttpResponse& response) {
if (response.err == ESP_ERR_INVALID_ARG) {
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, response.body.c_str());
}
if (response.err == ESP_ERR_NOT_FOUND) {
return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, response.body.c_str());
}
if (response.err != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, response.body.c_str());
}
httpd_resp_set_type(req, "application/json");
return httpd_resp_send(req, response.body.data(), response.body.size());
}
} // namespace
GatewayNetworkService::GatewayNetworkService(GatewayController& controller,
GatewayRuntime& runtime,
DaliDomainService& dali_domain,
GatewayNetworkServiceConfig config)
GatewayNetworkServiceConfig config,
GatewayBridgeService* bridge_service)
: controller_(controller), runtime_(runtime), dali_domain_(dali_domain), config_(config),
udp_lock_(xSemaphoreCreateMutex()) {}
bridge_service_(bridge_service), udp_lock_(xSemaphoreCreateMutex()) {}
esp_err_t GatewayNetworkService::start() {
if (started_) {
@@ -635,6 +702,8 @@ esp_err_t GatewayNetworkService::startHttpServer() {
{"/info", HTTP_GET, &GatewayNetworkService::HandleInfoGet},
{"/dali/cmd", HTTP_GET, &GatewayNetworkService::HandleCommandGet},
{"/dali/cmd", HTTP_POST, &GatewayNetworkService::HandleCommandPost},
{"/bridge", HTTP_GET, &GatewayNetworkService::HandleBridgeGet},
{"/bridge", HTTP_POST, &GatewayNetworkService::HandleBridgePost},
{"/led/1", HTTP_GET, &GatewayNetworkService::HandleLedOnGet},
{"/led/0", HTTP_GET, &GatewayNetworkService::HandleLedOffGet},
{"/jq.js", HTTP_GET, &GatewayNetworkService::HandleJqJsGet},
@@ -1227,6 +1296,42 @@ esp_err_t GatewayNetworkService::HandleCommandPost(httpd_req_t* req) {
return httpd_resp_sendstr(req, "ok");
}
esp_err_t GatewayNetworkService::HandleBridgeGet(httpd_req_t* req) {
auto* service = static_cast<GatewayNetworkService*>(req->user_ctx);
if (service == nullptr) {
return ESP_FAIL;
}
return service->sendBridgeResponse(req, false);
}
esp_err_t GatewayNetworkService::HandleBridgePost(httpd_req_t* req) {
auto* service = static_cast<GatewayNetworkService*>(req->user_ctx);
if (service == nullptr) {
return ESP_FAIL;
}
return service->sendBridgeResponse(req, true);
}
esp_err_t GatewayNetworkService::sendBridgeResponse(httpd_req_t* req, bool post) {
if (bridge_service_ == nullptr) {
return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Bridge service is not enabled");
}
const std::string action = QueryValue(req, "action");
const auto gateway_id = QueryGatewayId(req);
const int selected_gateway_id = gateway_id.has_value() ? gateway_id.value() : -1;
if (!post) {
return SendJsonResponse(req, bridge_service_->handleGet(action, selected_gateway_id,
QueryString(req)));
}
std::string body;
if (ReadRequestBody(req, body) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request");
}
return SendJsonResponse(req, bridge_service_->handlePost(action, selected_gateway_id, body));
}
esp_err_t GatewayNetworkService::HandleLedOnGet(httpd_req_t* req) {
auto* service = static_cast<GatewayNetworkService*>(req->user_ctx);
if (service == nullptr) {