36d10702da
- Introduced EtsDeviceRuntime class to manage device runtime functionalities including handling tunnel frames and function property commands. - Added support for individual address management and memory snapshot retrieval. - Updated EtsMemorySnapshot structure to include individual address. - Implemented identity application for DALI devices in the memory loader. - Enhanced CMakeLists.txt to include new source files and compile definitions. - Updated header files to include new dependencies and declarations. - Refactored existing memory loading logic to accommodate new device runtime features. Signed-off-by: Tony <tonylu@tony-cloud.com>
2146 lines
80 KiB
C++
2146 lines
80 KiB
C++
#include "gateway_knx.hpp"
|
|
|
|
#include "dali_define.hpp"
|
|
#include "driver/uart.h"
|
|
#include "esp_log.h"
|
|
#include "lwip/inet.h"
|
|
#include "lwip/sockets.h"
|
|
#include "openknx_idf/ets_device_runtime.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cerrno>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <initializer_list>
|
|
#include <set>
|
|
#include <utility>
|
|
#include <unistd.h>
|
|
|
|
namespace gateway {
|
|
namespace {
|
|
|
|
constexpr const char* kTag = "gateway_knx";
|
|
constexpr uint8_t kCemiLDataReq = 0x11;
|
|
constexpr uint8_t kCemiLDataInd = 0x29;
|
|
constexpr uint8_t kCemiLDataCon = 0x2e;
|
|
constexpr uint16_t kServiceConnectRequest = 0x0205;
|
|
constexpr uint16_t kServiceConnectResponse = 0x0206;
|
|
constexpr uint16_t kServiceConnectionStateRequest = 0x0207;
|
|
constexpr uint16_t kServiceConnectionStateResponse = 0x0208;
|
|
constexpr uint16_t kServiceDisconnectRequest = 0x0209;
|
|
constexpr uint16_t kServiceDisconnectResponse = 0x020a;
|
|
constexpr uint16_t kServiceTunnellingRequest = 0x0420;
|
|
constexpr uint16_t kServiceTunnellingAck = 0x0421;
|
|
constexpr uint16_t kServiceRoutingIndication = 0x0530;
|
|
constexpr uint8_t kKnxNetIpHeaderSize = 0x06;
|
|
constexpr uint8_t kKnxNetIpVersion10 = 0x10;
|
|
constexpr uint8_t kKnxNoError = 0x00;
|
|
constexpr uint8_t kKnxErrorConnectionId = 0x21;
|
|
constexpr uint8_t kKnxErrorConnectionType = 0x22;
|
|
constexpr uint8_t kKnxErrorNoMoreConnections = 0x24;
|
|
constexpr uint8_t kKnxErrorSequenceNumber = 0x04;
|
|
constexpr uint8_t kKnxConnectionTypeTunnel = 0x04;
|
|
constexpr uint8_t kKnxTunnelLayerLink = 0x02;
|
|
constexpr uint8_t kTpUartResetRequest = 0x01;
|
|
constexpr uint8_t kTpUartResetIndication = 0x03;
|
|
constexpr uint8_t kTpUartStateRequest = 0x02;
|
|
constexpr uint8_t kTpUartStateIndicationMask = 0x07;
|
|
constexpr uint8_t kTpUartSetAddressRequest = 0x28;
|
|
constexpr uint8_t kTpUartAckInfo = 0x10;
|
|
constexpr uint8_t kTpUartLDataConfirmPositive = 0x8b;
|
|
constexpr uint8_t kTpUartLDataConfirmNegative = 0x0b;
|
|
constexpr uint8_t kTpUartLDataStart = 0x80;
|
|
constexpr uint8_t kTpUartLDataEnd = 0x40;
|
|
constexpr uint8_t kTpUartBusy = 0xc0;
|
|
constexpr uint16_t kGwReg1AdrKoOffset = 12;
|
|
constexpr uint16_t kGwReg1AdrKoBlockSize = 18;
|
|
constexpr uint16_t kGwReg1GrpKoOffset = 1164;
|
|
constexpr uint16_t kGwReg1GrpKoBlockSize = 17;
|
|
constexpr uint16_t kGwReg1AppKoBroadcastSwitch = 1;
|
|
constexpr uint16_t kGwReg1AppKoBroadcastDimm = 2;
|
|
constexpr uint8_t kGwReg1KoSwitch = 0;
|
|
constexpr uint8_t kGwReg1KoDimmAbsolute = 3;
|
|
constexpr uint8_t kGwReg1KoColor = 6;
|
|
constexpr uint8_t kReg1DaliFunctionObjectIndex = 160;
|
|
constexpr uint8_t kReg1DaliFunctionPropertyId = 1;
|
|
constexpr uint8_t kReg1FunctionType = 2;
|
|
constexpr uint8_t kReg1FunctionScan = 3;
|
|
constexpr uint8_t kReg1FunctionAssign = 4;
|
|
constexpr uint8_t kReg1FunctionEvgWrite = 10;
|
|
constexpr uint8_t kReg1FunctionEvgRead = 11;
|
|
constexpr uint8_t kReg1FunctionSetScene = 12;
|
|
constexpr uint8_t kReg1FunctionGetScene = 13;
|
|
constexpr uint8_t kReg1FunctionIdentify = 14;
|
|
constexpr uint8_t kReg1DeviceTypeDt8 = 8;
|
|
constexpr uint8_t kReg1ColorTypeTw = 1;
|
|
constexpr uint8_t kDaliDeviceTypeNone = 0xfe;
|
|
constexpr uint8_t kDaliDeviceTypeMultiple = 0xff;
|
|
|
|
struct DecodedGroupWrite {
|
|
uint16_t group_address{0};
|
|
std::vector<uint8_t> data;
|
|
};
|
|
|
|
uint16_t ReadBe16(const uint8_t* data) {
|
|
return static_cast<uint16_t>((static_cast<uint16_t>(data[0]) << 8) | data[1]);
|
|
}
|
|
|
|
void WriteBe16(uint8_t* data, uint16_t value) {
|
|
data[0] = static_cast<uint8_t>((value >> 8) & 0xff);
|
|
data[1] = static_cast<uint8_t>(value & 0xff);
|
|
}
|
|
|
|
std::optional<int> ObjectIntAny(const DaliValue::Object& object,
|
|
std::initializer_list<const char*> keys) {
|
|
for (const char* key : keys) {
|
|
if (const auto value = getObjectInt(object, key)) {
|
|
return value;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<bool> ObjectBoolAny(const DaliValue::Object& object,
|
|
std::initializer_list<const char*> keys) {
|
|
for (const char* key : keys) {
|
|
if (const auto value = getObjectBool(object, key)) {
|
|
return value;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<std::string> ObjectStringAny(const DaliValue::Object& object,
|
|
std::initializer_list<const char*> keys) {
|
|
for (const char* key : keys) {
|
|
if (const auto value = getObjectString(object, key)) {
|
|
return value;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
const DaliValue* ObjectValueAny(const DaliValue::Object& object,
|
|
std::initializer_list<const char*> keys) {
|
|
for (const char* key : keys) {
|
|
if (const auto* value = getObjectValue(object, key)) {
|
|
return value;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::string NormalizeModeString(std::string value) {
|
|
value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char ch) {
|
|
return ch == '_' || ch == '-' || std::isspace(ch) != 0;
|
|
}),
|
|
value.end());
|
|
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
|
return static_cast<char>(std::tolower(ch));
|
|
});
|
|
return value;
|
|
}
|
|
|
|
std::optional<uint16_t> ParseGroupAddressString(const std::string& value) {
|
|
int parts[3] = {-1, -1, -1};
|
|
size_t start = 0;
|
|
for (int index = 0; index < 3; ++index) {
|
|
const size_t slash = value.find('/', start);
|
|
const bool last = index == 2;
|
|
if ((slash == std::string::npos) != last) {
|
|
return std::nullopt;
|
|
}
|
|
const std::string token = value.substr(start, last ? std::string::npos : slash - start);
|
|
if (token.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
char* end = nullptr;
|
|
errno = 0;
|
|
const long parsed = std::strtol(token.c_str(), &end, 10);
|
|
if (errno != 0 || end == token.c_str() || *end != '\0') {
|
|
return std::nullopt;
|
|
}
|
|
parts[index] = static_cast<int>(parsed);
|
|
start = slash + 1;
|
|
}
|
|
if (parts[0] < 0 || parts[0] > 31 || parts[1] < 0 || parts[1] > 7 || parts[2] < 0 ||
|
|
parts[2] > 255) {
|
|
return std::nullopt;
|
|
}
|
|
return static_cast<uint16_t>(((parts[0] & 0x1f) << 11) | ((parts[1] & 0x07) << 8) |
|
|
(parts[2] & 0xff));
|
|
}
|
|
|
|
std::optional<uint16_t> ObjectGroupAddressAny(const DaliValue::Object& object,
|
|
std::initializer_list<const char*> keys) {
|
|
for (const char* key : keys) {
|
|
const auto* value = getObjectValue(object, key);
|
|
if (value == nullptr) {
|
|
continue;
|
|
}
|
|
if (const auto raw = value->asInt()) {
|
|
if (raw.value() >= 0 && raw.value() <= 0xffff) {
|
|
return static_cast<uint16_t>(raw.value());
|
|
}
|
|
}
|
|
if (const auto raw = value->asString()) {
|
|
if (const auto parsed = ParseGroupAddressString(raw.value())) {
|
|
return parsed.value();
|
|
}
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::vector<GatewayKnxEtsAssociation> ParseEtsAssociations(const DaliValue::Object& object) {
|
|
std::vector<GatewayKnxEtsAssociation> associations;
|
|
const auto* raw_associations = ObjectValueAny(
|
|
object, {"etsAssociations", "ets_associations", "etsBindings", "ets_bindings",
|
|
"associationTable", "association_table"});
|
|
const auto* array = raw_associations == nullptr ? nullptr : raw_associations->asArray();
|
|
if (array == nullptr) {
|
|
return associations;
|
|
}
|
|
associations.reserve(array->size());
|
|
for (const auto& item : *array) {
|
|
const auto* entry = item.asObject();
|
|
if (entry == nullptr) {
|
|
continue;
|
|
}
|
|
const auto group_address = ObjectGroupAddressAny(
|
|
*entry, {"groupAddress", "group_address", "address", "rawAddress", "raw_address"});
|
|
const auto object_number = ObjectIntAny(
|
|
*entry, {"objectNumber", "object_number", "groupObjectNumber", "group_object_number",
|
|
"ko", "asap"});
|
|
if (!group_address.has_value() || !object_number.has_value() || object_number.value() < 0 ||
|
|
object_number.value() > kGwReg1GrpKoOffset + (kGwReg1GrpKoBlockSize * 16)) {
|
|
continue;
|
|
}
|
|
associations.push_back(GatewayKnxEtsAssociation{
|
|
group_address.value(), static_cast<uint16_t>(object_number.value())});
|
|
}
|
|
return associations;
|
|
}
|
|
|
|
std::string TargetName(const GatewayKnxDaliTarget& target) {
|
|
switch (target.kind) {
|
|
case GatewayKnxDaliTargetKind::kBroadcast:
|
|
return "Broadcast";
|
|
case GatewayKnxDaliTargetKind::kShortAddress:
|
|
return "A" + std::to_string(target.address);
|
|
case GatewayKnxDaliTargetKind::kGroup:
|
|
return "Group " + std::to_string(target.address);
|
|
case GatewayKnxDaliTargetKind::kNone:
|
|
default:
|
|
return "Unmapped";
|
|
}
|
|
}
|
|
|
|
std::string DataTypeName(GatewayKnxDaliDataType data_type) {
|
|
switch (data_type) {
|
|
case GatewayKnxDaliDataType::kSwitch:
|
|
return "Switch";
|
|
case GatewayKnxDaliDataType::kBrightness:
|
|
return "Dimmer";
|
|
case GatewayKnxDaliDataType::kColorTemperature:
|
|
return "Color Temperature";
|
|
case GatewayKnxDaliDataType::kRgb:
|
|
return "RGB";
|
|
case GatewayKnxDaliDataType::kUnknown:
|
|
default:
|
|
return "Unknown";
|
|
}
|
|
}
|
|
|
|
const char* DataTypeDpt(GatewayKnxDaliDataType data_type) {
|
|
switch (data_type) {
|
|
case GatewayKnxDaliDataType::kSwitch:
|
|
return "DPST-1-1";
|
|
case GatewayKnxDaliDataType::kBrightness:
|
|
return "DPST-5-1";
|
|
case GatewayKnxDaliDataType::kColorTemperature:
|
|
return "DPST-7-600";
|
|
case GatewayKnxDaliDataType::kRgb:
|
|
return "DPST-232-600";
|
|
case GatewayKnxDaliDataType::kUnknown:
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
std::optional<DecodedGroupWrite> DecodeCemiGroupWrite(const uint8_t* data, size_t len) {
|
|
if (data == nullptr || len < 10) {
|
|
return std::nullopt;
|
|
}
|
|
const uint8_t message_code = data[0];
|
|
if (message_code != kCemiLDataReq && message_code != kCemiLDataInd &&
|
|
message_code != kCemiLDataCon) {
|
|
return std::nullopt;
|
|
}
|
|
const size_t base = 2U + data[1];
|
|
if (len < base + 8U) {
|
|
return std::nullopt;
|
|
}
|
|
const uint8_t control2 = data[base + 1];
|
|
if ((control2 & 0x80) == 0) {
|
|
return std::nullopt;
|
|
}
|
|
const uint16_t destination = ReadBe16(data + base + 4);
|
|
const size_t tpdu_len = static_cast<size_t>(data[base + 6]) + 1U;
|
|
if (tpdu_len < 2U || len < base + 7U + tpdu_len) {
|
|
return std::nullopt;
|
|
}
|
|
const uint8_t* tpdu = data + base + 7;
|
|
const uint16_t apci = static_cast<uint16_t>(((tpdu[0] & 0x03) << 8) | (tpdu[1] & 0xc0));
|
|
if (apci != 0x80) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
DecodedGroupWrite out;
|
|
out.group_address = destination;
|
|
if (tpdu_len == 2U) {
|
|
out.data.push_back(tpdu[1] & 0x3f);
|
|
} else {
|
|
out.data.assign(tpdu + 2, tpdu + tpdu_len);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
uint8_t Reg1PercentToArc(uint8_t value) {
|
|
if (value == 0 || value == 0xff) {
|
|
return value;
|
|
}
|
|
const double arc = ((253.0 / 3.0) * (std::log10(static_cast<double>(value)) + 1.0)) + 1.0;
|
|
return static_cast<uint8_t>(std::clamp(static_cast<int>(arc + 0.5), 0, 254));
|
|
}
|
|
|
|
uint8_t Reg1ArcToPercent(uint8_t value) {
|
|
if (value == 0 || value == 0xff) {
|
|
return value;
|
|
}
|
|
const double percent = std::pow(10.0, ((static_cast<double>(value) - 1.0) / (253.0 / 3.0)) - 1.0);
|
|
return static_cast<uint8_t>(std::clamp(static_cast<int>(percent + 0.5), 0, 100));
|
|
}
|
|
|
|
GatewayKnxDaliTarget Reg1SceneTarget(uint8_t encoded_target) {
|
|
if ((encoded_target & 0x80) != 0) {
|
|
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup,
|
|
static_cast<int>(encoded_target & 0x0f)};
|
|
}
|
|
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress,
|
|
static_cast<int>(encoded_target & 0x3f)};
|
|
}
|
|
|
|
DaliBridgeRequest FunctionRequest(const char* sequence, BridgeOperation operation) {
|
|
DaliBridgeRequest request;
|
|
request.sequence = sequence == nullptr ? "knx-function-property" : sequence;
|
|
request.operation = operation;
|
|
return request;
|
|
}
|
|
|
|
void ApplyTargetToRequest(const GatewayKnxDaliTarget& target, DaliBridgeRequest* request) {
|
|
if (request == nullptr) {
|
|
return;
|
|
}
|
|
switch (target.kind) {
|
|
case GatewayKnxDaliTargetKind::kBroadcast:
|
|
request->metadata["broadcast"] = true;
|
|
break;
|
|
case GatewayKnxDaliTargetKind::kShortAddress:
|
|
request->shortAddress = target.address;
|
|
break;
|
|
case GatewayKnxDaliTargetKind::kGroup:
|
|
request->metadata["group"] = target.address;
|
|
break;
|
|
case GatewayKnxDaliTargetKind::kNone:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
DaliBridgeResult ExecuteRaw(DaliBridgeEngine& engine, BridgeOperation operation, uint8_t addr,
|
|
uint8_t cmd, const char* sequence) {
|
|
DaliBridgeRequest request = FunctionRequest(sequence, operation);
|
|
request.rawAddress = addr;
|
|
request.rawCommand = cmd;
|
|
return engine.execute(request);
|
|
}
|
|
|
|
std::optional<int> QueryShort(DaliBridgeEngine& engine, uint8_t short_address, uint8_t command,
|
|
const char* sequence) {
|
|
const auto result = ExecuteRaw(engine, BridgeOperation::query, DaliComm::toCmdAddr(short_address),
|
|
command, sequence);
|
|
if (!result.ok || !result.data.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
return result.data.value();
|
|
}
|
|
|
|
bool SendRaw(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, const char* sequence) {
|
|
return ExecuteRaw(engine, BridgeOperation::send, addr, cmd, sequence).ok;
|
|
}
|
|
|
|
bool SendRawExt(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, const char* sequence) {
|
|
return ExecuteRaw(engine, BridgeOperation::sendExt, addr, cmd, sequence).ok;
|
|
}
|
|
|
|
std::optional<int> MetadataInt(const DaliBridgeResult& result, const std::string& key) {
|
|
return getObjectInt(result.metadata, key);
|
|
}
|
|
|
|
DaliBridgeRequest RequestForTarget(uint16_t group_address,
|
|
const GatewayKnxDaliTarget& target,
|
|
BridgeOperation operation) {
|
|
DaliBridgeRequest request;
|
|
request.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
|
|
request.operation = operation;
|
|
switch (target.kind) {
|
|
case GatewayKnxDaliTargetKind::kBroadcast:
|
|
request.metadata["broadcast"] = true;
|
|
break;
|
|
case GatewayKnxDaliTargetKind::kShortAddress:
|
|
request.shortAddress = target.address;
|
|
break;
|
|
case GatewayKnxDaliTargetKind::kGroup:
|
|
request.metadata["group"] = target.address;
|
|
break;
|
|
case GatewayKnxDaliTargetKind::kNone:
|
|
default:
|
|
break;
|
|
}
|
|
request.metadata["sourceProtocol"] = "knx";
|
|
request.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address);
|
|
return request;
|
|
}
|
|
|
|
DaliBridgeResult ErrorResult(uint16_t group_address, const char* message) {
|
|
DaliBridgeResult result;
|
|
result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
|
|
result.error = message == nullptr ? "KNX error" : message;
|
|
return result;
|
|
}
|
|
|
|
bool SendAll(int sock, const uint8_t* data, size_t len, const sockaddr_in& remote) {
|
|
return sendto(sock, data, len, 0, reinterpret_cast<const sockaddr*>(&remote),
|
|
sizeof(remote)) == static_cast<int>(len);
|
|
}
|
|
|
|
std::vector<uint8_t> KnxNetIpPacket(uint16_t service, const std::vector<uint8_t>& body) {
|
|
std::vector<uint8_t> packet(6 + body.size());
|
|
packet[0] = kKnxNetIpHeaderSize;
|
|
packet[1] = kKnxNetIpVersion10;
|
|
WriteBe16(packet.data() + 2, service);
|
|
WriteBe16(packet.data() + 4, static_cast<uint16_t>(packet.size()));
|
|
if (!body.empty()) {
|
|
std::memcpy(packet.data() + 6, body.data(), body.size());
|
|
}
|
|
return packet;
|
|
}
|
|
|
|
std::array<uint8_t, 8> HpaiForRemote(const sockaddr_in& remote) {
|
|
std::array<uint8_t, 8> hpai{};
|
|
hpai[0] = 0x08;
|
|
hpai[1] = 0x01;
|
|
const uint32_t address = ntohl(remote.sin_addr.s_addr);
|
|
hpai[2] = static_cast<uint8_t>((address >> 24) & 0xff);
|
|
hpai[3] = static_cast<uint8_t>((address >> 16) & 0xff);
|
|
hpai[4] = static_cast<uint8_t>((address >> 8) & 0xff);
|
|
hpai[5] = static_cast<uint8_t>(address & 0xff);
|
|
WriteBe16(hpai.data() + 6, ntohs(remote.sin_port));
|
|
return hpai;
|
|
}
|
|
|
|
bool ParseKnxNetIpHeader(const uint8_t* data, size_t len, uint16_t* service,
|
|
uint16_t* total_len) {
|
|
if (data == nullptr || len < 6 || data[0] != kKnxNetIpHeaderSize ||
|
|
data[1] != kKnxNetIpVersion10) {
|
|
return false;
|
|
}
|
|
*service = ReadBe16(data + 2);
|
|
*total_len = ReadBe16(data + 4);
|
|
return *total_len >= 6 && *total_len <= len;
|
|
}
|
|
|
|
bool IsExtendedTpFrame(const uint8_t* data, size_t len) {
|
|
return len > 0 && (data[0] & 0xD3) == 0x10;
|
|
}
|
|
|
|
size_t ExpectedTpFrameSize(const uint8_t* data, size_t len) {
|
|
if (data == nullptr || len < 6) {
|
|
return 0;
|
|
}
|
|
if (IsExtendedTpFrame(data, len)) {
|
|
return 9U + data[6];
|
|
}
|
|
return 8U + (data[5] & 0x0F);
|
|
}
|
|
|
|
bool ValidateTpChecksum(const uint8_t* data, size_t len) {
|
|
if (data == nullptr || len < 2) {
|
|
return false;
|
|
}
|
|
uint8_t crc = 0xFF;
|
|
for (size_t index = 0; index + 1 < len; ++index) {
|
|
crc ^= data[index];
|
|
}
|
|
return data[len - 1] == crc;
|
|
}
|
|
|
|
bool IsTpUartControlByte(uint8_t byte) {
|
|
return byte == kTpUartResetIndication ||
|
|
byte == kTpUartLDataConfirmPositive ||
|
|
byte == kTpUartLDataConfirmNegative || byte == kTpUartBusy ||
|
|
(byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask;
|
|
}
|
|
|
|
bool IsTpUartFrameStart(uint8_t byte, bool* extended) {
|
|
if (extended == nullptr) {
|
|
return false;
|
|
}
|
|
*extended = (byte & 0x80) == 0;
|
|
return (byte & 0x50) == 0x10;
|
|
}
|
|
|
|
std::vector<uint8_t> WrapTpUartTelegram(const std::vector<uint8_t>& telegram) {
|
|
std::vector<uint8_t> wrapped;
|
|
wrapped.reserve(telegram.size() * 2U);
|
|
for (size_t index = 0; index < telegram.size(); ++index) {
|
|
const uint8_t control = static_cast<uint8_t>(
|
|
(index + 1U == telegram.size() ? kTpUartLDataEnd : kTpUartLDataStart) |
|
|
(index & 0x3fU));
|
|
wrapped.push_back(control);
|
|
wrapped.push_back(telegram[index]);
|
|
}
|
|
return wrapped;
|
|
}
|
|
|
|
bool TpTelegramEqualsIgnoringRepeatBit(const std::vector<uint8_t>& left,
|
|
const std::vector<uint8_t>& right) {
|
|
if (left.size() != right.size() || left.empty()) {
|
|
return false;
|
|
}
|
|
if ((left[0] & static_cast<uint8_t>(~0x20U)) !=
|
|
(right[0] & static_cast<uint8_t>(~0x20U))) {
|
|
return false;
|
|
}
|
|
return std::equal(left.begin() + 1, left.end(), right.begin() + 1);
|
|
}
|
|
|
|
std::optional<std::vector<uint8_t>> CemiToTpTelegram(const uint8_t* data, size_t len) {
|
|
if (data == nullptr || len < 10 || data[1] != 0) {
|
|
return std::nullopt;
|
|
}
|
|
const uint8_t* ctrl = data + 2;
|
|
const bool standard = (ctrl[0] & 0x80) != 0;
|
|
const size_t tp_len = standard ? len - 2U : len - 1U;
|
|
if (tp_len < 8) {
|
|
return std::nullopt;
|
|
}
|
|
std::vector<uint8_t> telegram(tp_len, 0);
|
|
if (standard) {
|
|
telegram[0] = ctrl[0];
|
|
std::memcpy(telegram.data() + 1, ctrl + 2, 4);
|
|
telegram[5] = static_cast<uint8_t>((ctrl[1] & 0xF0) | (ctrl[6] & 0x0F));
|
|
if (tp_len > 7U) {
|
|
std::memcpy(telegram.data() + 6, ctrl + 7, tp_len - 7U);
|
|
}
|
|
} else {
|
|
std::memcpy(telegram.data(), ctrl, tp_len - 1U);
|
|
}
|
|
uint8_t crc = 0xFF;
|
|
for (size_t index = 0; index + 1 < telegram.size(); ++index) {
|
|
crc ^= telegram[index];
|
|
}
|
|
telegram.back() = crc;
|
|
return telegram;
|
|
}
|
|
|
|
std::optional<std::vector<uint8_t>> TpTelegramToCemi(const uint8_t* data, size_t len) {
|
|
if (data == nullptr || len < 8 || !ValidateTpChecksum(data, len)) {
|
|
return std::nullopt;
|
|
}
|
|
const bool extended = IsExtendedTpFrame(data, len);
|
|
const size_t cemi_len = len + (extended ? 2U : 3U) - 1U;
|
|
std::vector<uint8_t> cemi(cemi_len, 0);
|
|
cemi[0] = kCemiLDataInd;
|
|
cemi[1] = 0x00;
|
|
cemi[2] = data[0];
|
|
if (extended) {
|
|
std::memcpy(cemi.data() + 2, data, len - 1U);
|
|
} else {
|
|
cemi[3] = data[5] & 0xF0;
|
|
std::memcpy(cemi.data() + 4, data + 1, 4);
|
|
cemi[8] = data[5] & 0x0F;
|
|
const size_t copy_len = static_cast<size_t>(cemi[8]) + 1U;
|
|
if (9U + copy_len > cemi.size() || 6U + copy_len > len) {
|
|
return std::nullopt;
|
|
}
|
|
std::memcpy(cemi.data() + 9, data + 6, copy_len);
|
|
}
|
|
return cemi;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value) {
|
|
if (value == nullptr || value->asObject() == nullptr) {
|
|
return std::nullopt;
|
|
}
|
|
const auto& object = *value->asObject();
|
|
GatewayKnxConfig config;
|
|
config.dali_router_enabled = ObjectBoolAny(object, {"daliRouterEnabled", "dali_router_enabled"})
|
|
.value_or(config.dali_router_enabled);
|
|
config.ip_router_enabled = ObjectBoolAny(object, {"ipRouterEnabled", "ip_router_enabled"})
|
|
.value_or(config.ip_router_enabled);
|
|
config.tunnel_enabled = ObjectBoolAny(object, {"tunnelEnabled", "tunnel_enabled"})
|
|
.value_or(config.tunnel_enabled);
|
|
config.multicast_enabled = ObjectBoolAny(object, {"multicastEnabled", "multicast_enabled"})
|
|
.value_or(config.multicast_enabled);
|
|
if (const auto mode = ObjectStringAny(object, {"mappingMode", "mapping_mode"})) {
|
|
config.mapping_mode = GatewayKnxMappingModeFromString(mode.value());
|
|
}
|
|
config.ets_database_enabled = ObjectBoolAny(object, {"etsDatabaseEnabled", "ets_database_enabled"})
|
|
.value_or(config.ets_database_enabled);
|
|
config.ets_associations = ParseEtsAssociations(object);
|
|
config.main_group = static_cast<uint8_t>(
|
|
std::clamp(ObjectIntAny(object, {"mainGroup", "main_group"}).value_or(config.main_group),
|
|
0, 31));
|
|
config.udp_port = static_cast<uint16_t>(std::clamp(
|
|
ObjectIntAny(object, {"udpPort", "port", "udp_port"}).value_or(config.udp_port), 1,
|
|
65535));
|
|
config.multicast_address = ObjectStringAny(object, {"multicastAddress", "multicast_address"})
|
|
.value_or(config.multicast_address);
|
|
config.individual_address = static_cast<uint16_t>(std::clamp(
|
|
ObjectIntAny(object, {"individualAddress", "individual_address"})
|
|
.value_or(config.individual_address),
|
|
0, 0xffff));
|
|
|
|
const auto* tp_uart = getObjectValue(object, "tpUart");
|
|
if (tp_uart == nullptr) {
|
|
tp_uart = getObjectValue(object, "tp_uart");
|
|
}
|
|
if (tp_uart != nullptr && tp_uart->asObject() != nullptr) {
|
|
const auto& serial = *tp_uart->asObject();
|
|
config.tp_uart.uart_port = std::clamp(
|
|
ObjectIntAny(serial, {"uartPort", "uart_port"}).value_or(config.tp_uart.uart_port), 0,
|
|
2);
|
|
config.tp_uart.tx_pin = ObjectIntAny(serial, {"txPin", "tx_pin"}).value_or(config.tp_uart.tx_pin);
|
|
config.tp_uart.rx_pin = ObjectIntAny(serial, {"rxPin", "rx_pin"}).value_or(config.tp_uart.rx_pin);
|
|
config.tp_uart.baudrate = static_cast<uint32_t>(std::max(
|
|
1200, ObjectIntAny(serial, {"baudrate", "baud"}).value_or(config.tp_uart.baudrate)));
|
|
config.tp_uart.rx_buffer_size = static_cast<size_t>(std::max(
|
|
128, ObjectIntAny(serial, {"rxBufferSize", "rx_buffer_size"})
|
|
.value_or(static_cast<int>(config.tp_uart.rx_buffer_size))));
|
|
config.tp_uart.tx_buffer_size = static_cast<size_t>(std::max(
|
|
128, ObjectIntAny(serial, {"txBufferSize", "tx_buffer_size"})
|
|
.value_or(static_cast<int>(config.tp_uart.tx_buffer_size))));
|
|
config.tp_uart.read_timeout_ms = static_cast<uint32_t>(std::max(
|
|
1, ObjectIntAny(serial, {"readTimeoutMs", "read_timeout_ms"})
|
|
.value_or(static_cast<int>(config.tp_uart.read_timeout_ms))));
|
|
}
|
|
return config;
|
|
}
|
|
|
|
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) {
|
|
DaliValue::Object out;
|
|
out["daliRouterEnabled"] = config.dali_router_enabled;
|
|
out["ipRouterEnabled"] = config.ip_router_enabled;
|
|
out["tunnelEnabled"] = config.tunnel_enabled;
|
|
out["multicastEnabled"] = config.multicast_enabled;
|
|
out["etsDatabaseEnabled"] = config.ets_database_enabled;
|
|
out["mappingMode"] = GatewayKnxMappingModeToString(config.mapping_mode);
|
|
out["mainGroup"] = static_cast<int>(config.main_group);
|
|
out["udpPort"] = static_cast<int>(config.udp_port);
|
|
out["multicastAddress"] = config.multicast_address;
|
|
out["individualAddress"] = static_cast<int>(config.individual_address);
|
|
DaliValue::Object serial;
|
|
serial["uartPort"] = config.tp_uart.uart_port;
|
|
serial["txPin"] = config.tp_uart.tx_pin;
|
|
serial["rxPin"] = config.tp_uart.rx_pin;
|
|
serial["baudrate"] = static_cast<int>(config.tp_uart.baudrate);
|
|
serial["rxBufferSize"] = static_cast<int>(config.tp_uart.rx_buffer_size);
|
|
serial["txBufferSize"] = static_cast<int>(config.tp_uart.tx_buffer_size);
|
|
serial["readTimeoutMs"] = static_cast<int>(config.tp_uart.read_timeout_ms);
|
|
out["tpUart"] = std::move(serial);
|
|
DaliValue::Array ets_associations;
|
|
ets_associations.reserve(config.ets_associations.size());
|
|
for (const auto& association : config.ets_associations) {
|
|
DaliValue::Object entry;
|
|
entry["groupAddress"] = static_cast<int>(association.group_address);
|
|
entry["groupObjectNumber"] = static_cast<int>(association.group_object_number);
|
|
ets_associations.emplace_back(std::move(entry));
|
|
}
|
|
out["etsAssociations"] = std::move(ets_associations);
|
|
return DaliValue(std::move(out));
|
|
}
|
|
|
|
const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode) {
|
|
switch (mode) {
|
|
case GatewayKnxMappingMode::kEtsDatabase:
|
|
return "ets_database";
|
|
case GatewayKnxMappingMode::kGwReg1Direct:
|
|
return "gw_reg1_direct";
|
|
case GatewayKnxMappingMode::kManual:
|
|
return "manual";
|
|
case GatewayKnxMappingMode::kFormula:
|
|
default:
|
|
return "formula";
|
|
}
|
|
}
|
|
|
|
GatewayKnxMappingMode GatewayKnxMappingModeFromString(const std::string& value) {
|
|
const std::string normalized = NormalizeModeString(value);
|
|
if (normalized == "gwreg1direct" || normalized == "gwreg1" ||
|
|
normalized == "gwreg1channel" || normalized == "channelindex") {
|
|
return GatewayKnxMappingMode::kGwReg1Direct;
|
|
}
|
|
if (normalized == "manual" || normalized == "database" || normalized == "db") {
|
|
return GatewayKnxMappingMode::kManual;
|
|
}
|
|
if (normalized == "etsdatabase" || normalized == "ets" || normalized == "openknx") {
|
|
return GatewayKnxMappingMode::kEtsDatabase;
|
|
}
|
|
return GatewayKnxMappingMode::kFormula;
|
|
}
|
|
|
|
const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type) {
|
|
switch (data_type) {
|
|
case GatewayKnxDaliDataType::kSwitch:
|
|
return "switch";
|
|
case GatewayKnxDaliDataType::kBrightness:
|
|
return "brightness";
|
|
case GatewayKnxDaliDataType::kColorTemperature:
|
|
return "color_temperature";
|
|
case GatewayKnxDaliDataType::kRgb:
|
|
return "rgb";
|
|
case GatewayKnxDaliDataType::kUnknown:
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
const char* GatewayKnxTargetKindToString(GatewayKnxDaliTargetKind kind) {
|
|
switch (kind) {
|
|
case GatewayKnxDaliTargetKind::kBroadcast:
|
|
return "broadcast";
|
|
case GatewayKnxDaliTargetKind::kShortAddress:
|
|
return "short_address";
|
|
case GatewayKnxDaliTargetKind::kGroup:
|
|
return "group";
|
|
case GatewayKnxDaliTargetKind::kNone:
|
|
default:
|
|
return "none";
|
|
}
|
|
}
|
|
|
|
std::optional<GatewayKnxDaliDataType> GatewayKnxDaliDataTypeForMiddleGroup(
|
|
uint8_t middle_group) {
|
|
switch (middle_group) {
|
|
case 1:
|
|
return GatewayKnxDaliDataType::kSwitch;
|
|
case 2:
|
|
return GatewayKnxDaliDataType::kBrightness;
|
|
case 3:
|
|
return GatewayKnxDaliDataType::kColorTemperature;
|
|
case 4:
|
|
return GatewayKnxDaliDataType::kRgb;
|
|
default:
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
std::optional<GatewayKnxDaliTarget> GatewayKnxDaliTargetForSubgroup(uint8_t sub_group) {
|
|
if (sub_group == 0) {
|
|
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127};
|
|
}
|
|
if (sub_group >= 1 && sub_group <= 64) {
|
|
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress,
|
|
static_cast<int>(sub_group - 1)};
|
|
}
|
|
if (sub_group >= 65 && sub_group <= 80) {
|
|
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup,
|
|
static_cast<int>(sub_group - 65)};
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
uint16_t GatewayKnxGroupAddress(uint8_t main_group, uint8_t middle_group,
|
|
uint8_t sub_group) {
|
|
return static_cast<uint16_t>(((main_group & 0x1f) << 11) |
|
|
((middle_group & 0x07) << 8) | sub_group);
|
|
}
|
|
|
|
std::string GatewayKnxGroupAddressString(uint16_t group_address) {
|
|
const int main = (group_address >> 11) & 0x1f;
|
|
const int middle = (group_address >> 8) & 0x07;
|
|
const int sub = group_address & 0xff;
|
|
return std::to_string(main) + "/" + std::to_string(middle) + "/" +
|
|
std::to_string(sub);
|
|
}
|
|
|
|
namespace {
|
|
|
|
uint16_t GwReg1GroupAddressForObject(uint8_t main_group, uint16_t object_number) {
|
|
return GatewayKnxGroupAddress(main_group, static_cast<uint8_t>(object_number >> 8),
|
|
static_cast<uint8_t>(object_number & 0xff));
|
|
}
|
|
|
|
GatewayKnxDaliBinding MakeGwReg1Binding(uint8_t main_group, uint16_t object_number,
|
|
int channel_index, const char* object_role,
|
|
GatewayKnxDaliDataType data_type,
|
|
GatewayKnxDaliTarget target) {
|
|
GatewayKnxDaliBinding binding;
|
|
binding.mapping_mode = GatewayKnxMappingMode::kGwReg1Direct;
|
|
binding.group_object_number = static_cast<int>(object_number);
|
|
binding.channel_index = channel_index;
|
|
binding.object_role = object_role;
|
|
binding.main_group = main_group;
|
|
binding.middle_group = static_cast<uint8_t>((object_number >> 8) & 0x07);
|
|
binding.sub_group = static_cast<uint8_t>(object_number & 0xff);
|
|
binding.group_address = GwReg1GroupAddressForObject(main_group, object_number);
|
|
binding.address = GatewayKnxGroupAddressString(binding.group_address);
|
|
binding.data_type = data_type;
|
|
binding.target = target;
|
|
binding.datapoint_type = DataTypeDpt(data_type);
|
|
binding.name = std::string("GW-REG1 ") + TargetName(target) + " - " +
|
|
DataTypeName(data_type);
|
|
return binding;
|
|
}
|
|
|
|
std::optional<GatewayKnxDaliBinding> GwReg1BindingForObject(uint8_t main_group,
|
|
uint16_t object_number) {
|
|
if (object_number == kGwReg1AppKoBroadcastSwitch) {
|
|
return MakeGwReg1Binding(
|
|
main_group, object_number, -1, "broadcast_switch", GatewayKnxDaliDataType::kSwitch,
|
|
GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127});
|
|
}
|
|
if (object_number == kGwReg1AppKoBroadcastDimm) {
|
|
return MakeGwReg1Binding(
|
|
main_group, object_number, -1, "broadcast_dimm_absolute",
|
|
GatewayKnxDaliDataType::kBrightness,
|
|
GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127});
|
|
}
|
|
|
|
const int adr_relative = static_cast<int>(object_number) - kGwReg1AdrKoOffset;
|
|
if (adr_relative >= 0 && adr_relative < kGwReg1AdrKoBlockSize * 64) {
|
|
const int channel = adr_relative / kGwReg1AdrKoBlockSize;
|
|
const int slot = adr_relative % kGwReg1AdrKoBlockSize;
|
|
const GatewayKnxDaliTarget target{GatewayKnxDaliTargetKind::kShortAddress, channel};
|
|
if (slot == kGwReg1KoSwitch) {
|
|
return MakeGwReg1Binding(main_group, object_number, channel, "switch",
|
|
GatewayKnxDaliDataType::kSwitch, target);
|
|
}
|
|
if (slot == kGwReg1KoDimmAbsolute) {
|
|
return MakeGwReg1Binding(main_group, object_number, channel, "dimm_absolute",
|
|
GatewayKnxDaliDataType::kBrightness, target);
|
|
}
|
|
if (slot == kGwReg1KoColor) {
|
|
return MakeGwReg1Binding(main_group, object_number, channel, "color",
|
|
GatewayKnxDaliDataType::kRgb, target);
|
|
}
|
|
}
|
|
|
|
const int group_relative = static_cast<int>(object_number) - kGwReg1GrpKoOffset;
|
|
if (group_relative >= 0 && group_relative < kGwReg1GrpKoBlockSize * 16) {
|
|
const int group = group_relative / kGwReg1GrpKoBlockSize;
|
|
const int slot = group_relative % kGwReg1GrpKoBlockSize;
|
|
const GatewayKnxDaliTarget target{GatewayKnxDaliTargetKind::kGroup, group};
|
|
if (slot == kGwReg1KoSwitch) {
|
|
return MakeGwReg1Binding(main_group, object_number, group, "switch",
|
|
GatewayKnxDaliDataType::kSwitch, target);
|
|
}
|
|
if (slot == kGwReg1KoDimmAbsolute) {
|
|
return MakeGwReg1Binding(main_group, object_number, group, "dimm_absolute",
|
|
GatewayKnxDaliDataType::kBrightness, target);
|
|
}
|
|
if (slot == kGwReg1KoColor) {
|
|
return MakeGwReg1Binding(main_group, object_number, group, "color",
|
|
GatewayKnxDaliDataType::kRgb, target);
|
|
}
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<GatewayKnxDaliBinding> EtsBindingForAssociation(uint8_t main_group,
|
|
const GatewayKnxEtsAssociation& association) {
|
|
auto binding = GwReg1BindingForObject(main_group, association.group_object_number);
|
|
if (!binding.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
binding->mapping_mode = GatewayKnxMappingMode::kEtsDatabase;
|
|
binding->group_address = association.group_address;
|
|
binding->address = GatewayKnxGroupAddressString(association.group_address);
|
|
binding->name = std::string("ETS ") + binding->name;
|
|
return binding;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
GatewayKnxBridge::GatewayKnxBridge(DaliBridgeEngine& engine) : engine_(engine) {}
|
|
|
|
void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) {
|
|
config_ = config;
|
|
rebuildEtsBindings();
|
|
}
|
|
|
|
const GatewayKnxConfig& GatewayKnxBridge::config() const { return config_; }
|
|
|
|
size_t GatewayKnxBridge::etsBindingCount() const {
|
|
size_t count = 0;
|
|
for (const auto& entry : ets_bindings_by_group_address_) {
|
|
count += entry.second.size();
|
|
}
|
|
return count;
|
|
}
|
|
|
|
std::vector<GatewayKnxDaliBinding> GatewayKnxBridge::describeDaliBindings() const {
|
|
std::vector<GatewayKnxDaliBinding> bindings;
|
|
std::set<uint16_t> ets_group_addresses;
|
|
if (config_.ets_database_enabled) {
|
|
for (const auto& entry : ets_bindings_by_group_address_) {
|
|
ets_group_addresses.insert(entry.first);
|
|
bindings.insert(bindings.end(), entry.second.begin(), entry.second.end());
|
|
}
|
|
}
|
|
if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) {
|
|
bindings.reserve(2 + (64 * 3) + (16 * 3));
|
|
if (const auto binding = GwReg1BindingForObject(config_.main_group,
|
|
kGwReg1AppKoBroadcastSwitch)) {
|
|
if (ets_group_addresses.count(binding->group_address) == 0) {
|
|
bindings.push_back(binding.value());
|
|
}
|
|
}
|
|
if (const auto binding = GwReg1BindingForObject(config_.main_group,
|
|
kGwReg1AppKoBroadcastDimm)) {
|
|
if (ets_group_addresses.count(binding->group_address) == 0) {
|
|
bindings.push_back(binding.value());
|
|
}
|
|
}
|
|
for (int address = 0; address < 64; ++address) {
|
|
const uint16_t base = static_cast<uint16_t>(kGwReg1AdrKoOffset +
|
|
(address * kGwReg1AdrKoBlockSize));
|
|
for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmAbsolute, kGwReg1KoColor}) {
|
|
if (const auto binding = GwReg1BindingForObject(config_.main_group, base + slot)) {
|
|
if (ets_group_addresses.count(binding->group_address) == 0) {
|
|
bindings.push_back(binding.value());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (int group = 0; group < 16; ++group) {
|
|
const uint16_t base = static_cast<uint16_t>(kGwReg1GrpKoOffset +
|
|
(group * kGwReg1GrpKoBlockSize));
|
|
for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmAbsolute, kGwReg1KoColor}) {
|
|
if (const auto binding = GwReg1BindingForObject(config_.main_group, base + slot)) {
|
|
if (ets_group_addresses.count(binding->group_address) == 0) {
|
|
bindings.push_back(binding.value());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return bindings;
|
|
}
|
|
|
|
bindings.reserve(4 * 81);
|
|
for (uint8_t middle = 1; middle <= 4; ++middle) {
|
|
const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle);
|
|
if (!data_type.has_value()) {
|
|
continue;
|
|
}
|
|
for (uint8_t sub = 0; sub <= 80; ++sub) {
|
|
const auto target = GatewayKnxDaliTargetForSubgroup(sub);
|
|
if (!target.has_value()) {
|
|
continue;
|
|
}
|
|
GatewayKnxDaliBinding binding;
|
|
binding.mapping_mode = GatewayKnxMappingMode::kFormula;
|
|
binding.main_group = config_.main_group;
|
|
binding.middle_group = middle;
|
|
binding.sub_group = sub;
|
|
binding.group_address = GatewayKnxGroupAddress(config_.main_group, middle, sub);
|
|
binding.address = GatewayKnxGroupAddressString(binding.group_address);
|
|
binding.data_type = data_type.value();
|
|
binding.target = target.value();
|
|
if (ets_group_addresses.count(binding.group_address) != 0) {
|
|
continue;
|
|
}
|
|
binding.object_role = GatewayKnxDataTypeToString(data_type.value());
|
|
binding.datapoint_type = DataTypeDpt(data_type.value());
|
|
binding.name = TargetName(target.value()) + " - " + DataTypeName(data_type.value());
|
|
bindings.push_back(std::move(binding));
|
|
}
|
|
}
|
|
return bindings;
|
|
}
|
|
|
|
DaliBridgeResult GatewayKnxBridge::handleCemiFrame(const uint8_t* data, size_t len) {
|
|
const auto decoded = DecodeCemiGroupWrite(data, len);
|
|
if (!decoded.has_value()) {
|
|
return ErrorResult(0, "unsupported or non group-write cEMI frame");
|
|
}
|
|
return handleGroupWrite(decoded->group_address, decoded->data.data(), decoded->data.size());
|
|
}
|
|
|
|
DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, const uint8_t* data,
|
|
size_t len) {
|
|
if (!config_.dali_router_enabled) {
|
|
return ErrorResult(group_address, "KNX to DALI router disabled");
|
|
}
|
|
if (config_.ets_database_enabled) {
|
|
const auto ets_bindings = ets_bindings_by_group_address_.find(group_address);
|
|
if (ets_bindings != ets_bindings_by_group_address_.end()) {
|
|
return executeEtsBindings(group_address, ets_bindings->second, data, len);
|
|
}
|
|
}
|
|
const uint8_t main = static_cast<uint8_t>((group_address >> 11) & 0x1f);
|
|
const uint8_t middle = static_cast<uint8_t>((group_address >> 8) & 0x07);
|
|
const uint8_t sub = static_cast<uint8_t>(group_address & 0xff);
|
|
if (main != config_.main_group) {
|
|
return ErrorResult(group_address, "KNX main group does not match gateway config");
|
|
}
|
|
if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) {
|
|
const uint16_t object_number = static_cast<uint16_t>((middle << 8) | sub);
|
|
const auto binding = GwReg1BindingForObject(config_.main_group, object_number);
|
|
if (!binding.has_value()) {
|
|
return ErrorResult(group_address, "unmapped GW-REG1 KNX object address");
|
|
}
|
|
return executeForDecodedWrite(group_address, binding->data_type, binding->target, data, len);
|
|
}
|
|
if (config_.mapping_mode == GatewayKnxMappingMode::kManual) {
|
|
return ErrorResult(group_address, "manual KNX mapping dataset is not configured");
|
|
}
|
|
const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle);
|
|
const auto target = GatewayKnxDaliTargetForSubgroup(sub);
|
|
if (!data_type.has_value() || !target.has_value()) {
|
|
return ErrorResult(group_address, "unmapped KNX group address");
|
|
}
|
|
return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len);
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
|
|
const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId ||
|
|
data == nullptr || len == 0 || response == nullptr) {
|
|
return false;
|
|
}
|
|
switch (data[0]) {
|
|
case kReg1FunctionType:
|
|
return handleReg1TypeCommand(data, len, response);
|
|
case kReg1FunctionScan:
|
|
return handleReg1ScanCommand(data, len, response);
|
|
case kReg1FunctionAssign:
|
|
return handleReg1AssignCommand(data, len, response);
|
|
case kReg1FunctionEvgWrite:
|
|
return handleReg1EvgWriteCommand(data, len, response);
|
|
case kReg1FunctionEvgRead:
|
|
return handleReg1EvgReadCommand(data, len, response);
|
|
case kReg1FunctionSetScene:
|
|
return handleReg1SetSceneCommand(data, len, response);
|
|
case kReg1FunctionGetScene:
|
|
return handleReg1GetSceneCommand(data, len, response);
|
|
case kReg1FunctionIdentify:
|
|
return handleReg1IdentifyCommand(data, len, response);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
|
|
const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId ||
|
|
data == nullptr || len == 0 || response == nullptr) {
|
|
return false;
|
|
}
|
|
switch (data[0]) {
|
|
case kReg1FunctionScan:
|
|
case 5:
|
|
return handleReg1ScanState(data, len, response);
|
|
case kReg1FunctionAssign:
|
|
return handleReg1AssignState(data, len, response);
|
|
case 7:
|
|
return handleReg1FoundEvgsState(data, len, response);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1TypeCommand(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (len < 2 || response == nullptr) {
|
|
return false;
|
|
}
|
|
const uint8_t short_address = data[1];
|
|
const auto type_response = QueryShort(engine_, short_address, DALI_CMD_QUERY_DEVICE_TYPE,
|
|
"knx-function-type");
|
|
if (!type_response.has_value()) {
|
|
*response = {0x01};
|
|
return true;
|
|
}
|
|
uint8_t device_type = static_cast<uint8_t>(type_response.value());
|
|
if (device_type == kDaliDeviceTypeMultiple) {
|
|
for (int index = 0; index < 16; ++index) {
|
|
const auto next_type = QueryShort(engine_, short_address, DALI_CMD_QUERY_NEXT_DEVICE_TYPE,
|
|
"knx-function-next-device-type");
|
|
if (!next_type.has_value()) {
|
|
*response = {0x01};
|
|
return true;
|
|
}
|
|
if (next_type.value() == kDaliDeviceTypeNone) {
|
|
break;
|
|
}
|
|
if (next_type.value() < 20) {
|
|
device_type = static_cast<uint8_t>(next_type.value());
|
|
}
|
|
}
|
|
}
|
|
*response = {0x00, device_type};
|
|
if (device_type == kReg1DeviceTypeDt8) {
|
|
if (!SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8,
|
|
"knx-function-dt8-select")) {
|
|
*response = {0x02};
|
|
return true;
|
|
}
|
|
const auto color_features = QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_TYPE,
|
|
"knx-function-color-type");
|
|
if (!color_features.has_value()) {
|
|
*response = {0x02};
|
|
return true;
|
|
}
|
|
response->push_back(static_cast<uint8_t>(color_features.value()));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1ScanCommand(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (len < 5 || response == nullptr) {
|
|
return false;
|
|
}
|
|
commissioning_scan_done_ = false;
|
|
commissioning_found_ballasts_.clear();
|
|
|
|
const bool delete_all = data[3] == 1;
|
|
const bool assign = data[4] == 1;
|
|
if (assign || delete_all) {
|
|
DaliBridgeRequest allocate = FunctionRequest(
|
|
"knx-function-scan-allocate",
|
|
delete_all ? BridgeOperation::resetAndAllocateShortAddresses
|
|
: BridgeOperation::allocateAllShortAddresses);
|
|
allocate.value = DaliValue::Object{{"start", 0}, {"removeAddrFirst", delete_all}};
|
|
engine_.execute(allocate);
|
|
}
|
|
|
|
DaliBridgeRequest search = FunctionRequest("knx-function-scan-search", BridgeOperation::searchAddressRange);
|
|
search.value = DaliValue::Object{{"start", 0}, {"end", 63}};
|
|
const auto search_result = engine_.execute(search);
|
|
if (search_result.ok) {
|
|
if (const auto* addresses_value = getObjectValue(search_result.metadata, "addresses")) {
|
|
if (const auto* addresses = addresses_value->asArray()) {
|
|
for (const auto& address_value : *addresses) {
|
|
const auto short_address = address_value.asInt();
|
|
if (!short_address.has_value() || short_address.value() < 0 || short_address.value() > 63) {
|
|
continue;
|
|
}
|
|
GatewayKnxCommissioningBallast ballast;
|
|
ballast.short_address = static_cast<uint8_t>(short_address.value());
|
|
ballast.high = static_cast<uint8_t>(
|
|
QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_H,
|
|
"knx-function-scan-rand-h")
|
|
.value_or(0));
|
|
ballast.middle = static_cast<uint8_t>(
|
|
QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_M,
|
|
"knx-function-scan-rand-m")
|
|
.value_or(0));
|
|
ballast.low = static_cast<uint8_t>(
|
|
QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_L,
|
|
"knx-function-scan-rand-l")
|
|
.value_or(0));
|
|
commissioning_found_ballasts_.push_back(ballast);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
commissioning_scan_done_ = true;
|
|
response->clear();
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1AssignCommand(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (len < 5 || response == nullptr) {
|
|
return false;
|
|
}
|
|
commissioning_assign_done_ = false;
|
|
const uint8_t short_address = data[1] == 99 ? 0xff : data[1];
|
|
const bool ok = SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, 0x00,
|
|
"knx-function-assign-init") &&
|
|
SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRH, data[2],
|
|
"knx-function-assign-search-h") &&
|
|
SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRM, data[3],
|
|
"knx-function-assign-search-m") &&
|
|
SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRL, data[4],
|
|
"knx-function-assign-search-l") &&
|
|
SendRaw(engine_, DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS,
|
|
short_address == 0xff ? 0xff : DaliComm::toCmdAddr(short_address),
|
|
"knx-function-assign-program") &&
|
|
SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, 0x00,
|
|
"knx-function-assign-terminate");
|
|
commissioning_assign_done_ = true;
|
|
if (!ok) {
|
|
ESP_LOGW(kTag, "REG1-Dali assign command failed while programming short address %u",
|
|
short_address);
|
|
}
|
|
response->clear();
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1EvgWriteCommand(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (len < 10 || response == nullptr) {
|
|
return false;
|
|
}
|
|
const uint8_t short_address = data[1];
|
|
DaliBridgeRequest settings = FunctionRequest("knx-function-evg-write-settings",
|
|
BridgeOperation::setAddressSettings);
|
|
settings.shortAddress = short_address;
|
|
settings.value = DaliValue::Object{
|
|
{"minLevel", Reg1PercentToArc(data[2])},
|
|
{"maxLevel", Reg1PercentToArc(data[3])},
|
|
{"powerOnLevel", Reg1PercentToArc(data[4])},
|
|
{"systemFailureLevel", Reg1PercentToArc(data[5])},
|
|
{"fadeTime", static_cast<int>((data[6] >> 4) & 0x0f)},
|
|
{"fadeRate", static_cast<int>(data[6] & 0x0f)},
|
|
};
|
|
const bool settings_ok = engine_.execute(settings).ok;
|
|
|
|
DaliBridgeRequest groups = FunctionRequest("knx-function-evg-write-groups",
|
|
BridgeOperation::setGroupMask);
|
|
groups.shortAddress = short_address;
|
|
groups.value = static_cast<int>(static_cast<uint16_t>(data[8]) |
|
|
(static_cast<uint16_t>(data[9]) << 8));
|
|
const bool groups_ok = engine_.execute(groups).ok;
|
|
if (!settings_ok || !groups_ok) {
|
|
ESP_LOGW(kTag, "REG1-Dali EVG write command failed for short address %u", short_address);
|
|
}
|
|
response->clear();
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1EvgReadCommand(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (len < 2 || response == nullptr) {
|
|
return false;
|
|
}
|
|
const uint8_t short_address = data[1];
|
|
response->assign(12, 0x00);
|
|
(*response)[0] = 0x00;
|
|
uint8_t error_byte = 0;
|
|
|
|
DaliBridgeRequest settings = FunctionRequest("knx-function-evg-read-settings",
|
|
BridgeOperation::getAddressSettings);
|
|
settings.shortAddress = short_address;
|
|
const auto settings_result = engine_.execute(settings);
|
|
const auto set_level = [&](size_t index, const char* key, uint8_t error_mask) {
|
|
const auto value = MetadataInt(settings_result, key);
|
|
if (!settings_result.ok || !value.has_value()) {
|
|
error_byte |= error_mask;
|
|
(*response)[index] = 0xff;
|
|
return;
|
|
}
|
|
(*response)[index] = Reg1ArcToPercent(static_cast<uint8_t>(std::clamp(value.value(), 0, 255)));
|
|
};
|
|
set_level(1, "minLevel", 0b00000001);
|
|
set_level(2, "maxLevel", 0b00000010);
|
|
set_level(3, "powerOnLevel", 0b00000100);
|
|
set_level(4, "systemFailureLevel", 0b00001000);
|
|
const auto fade_time = MetadataInt(settings_result, "fadeTime");
|
|
const auto fade_rate = MetadataInt(settings_result, "fadeRate");
|
|
if (!settings_result.ok || !fade_time.has_value() || !fade_rate.has_value()) {
|
|
error_byte |= 0b00010000;
|
|
(*response)[5] = 0xff;
|
|
} else {
|
|
(*response)[5] = static_cast<uint8_t>(((fade_rate.value() & 0x0f) << 4) |
|
|
(fade_time.value() & 0x0f));
|
|
}
|
|
|
|
DaliBridgeRequest groups = FunctionRequest("knx-function-evg-read-groups", BridgeOperation::getGroupMask);
|
|
groups.shortAddress = short_address;
|
|
const auto groups_result = engine_.execute(groups);
|
|
if (!groups_result.ok || !groups_result.data.has_value()) {
|
|
error_byte |= 0b11000000;
|
|
} else {
|
|
const uint16_t mask = static_cast<uint16_t>(groups_result.data.value());
|
|
(*response)[7] = static_cast<uint8_t>(mask & 0xff);
|
|
(*response)[8] = static_cast<uint8_t>((mask >> 8) & 0xff);
|
|
}
|
|
(*response)[9] = error_byte;
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1SetSceneCommand(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (len < 10 || response == nullptr) {
|
|
return false;
|
|
}
|
|
const GatewayKnxDaliTarget target = Reg1SceneTarget(data[1]);
|
|
const uint8_t scene = data[2] & 0x0f;
|
|
const bool enabled = data[3] != 0;
|
|
DaliBridgeRequest request = FunctionRequest(
|
|
enabled ? "knx-function-set-scene" : "knx-function-remove-scene",
|
|
enabled ? (data[4] == kReg1DeviceTypeDt8 ? BridgeOperation::storeDt8SceneSnapshot
|
|
: BridgeOperation::setSceneLevel)
|
|
: BridgeOperation::removeSceneLevel);
|
|
ApplyTargetToRequest(target, &request);
|
|
DaliValue::Object value{{"scene", static_cast<int>(scene)}};
|
|
if (enabled) {
|
|
value["brightness"] = static_cast<int>(Reg1PercentToArc(data[6]));
|
|
if (data[4] == kReg1DeviceTypeDt8) {
|
|
if (data[5] == kReg1ColorTypeTw) {
|
|
const uint16_t kelvin = ReadBe16(data + 7);
|
|
value["colorMode"] = "color_temperature";
|
|
value["colorTemperature"] = static_cast<int>(kelvin);
|
|
} else {
|
|
value["colorMode"] = "rgb";
|
|
value["r"] = static_cast<int>(data[7]);
|
|
value["g"] = static_cast<int>(data[8]);
|
|
value["b"] = static_cast<int>(data[9]);
|
|
}
|
|
}
|
|
}
|
|
request.value = std::move(value);
|
|
const auto result = engine_.execute(request);
|
|
if (!result.ok) {
|
|
ESP_LOGW(kTag, "REG1-Dali set scene command failed for scene %u", scene);
|
|
}
|
|
response->clear();
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1GetSceneCommand(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (len < 5 || response == nullptr) {
|
|
return false;
|
|
}
|
|
const uint8_t short_address = data[1];
|
|
const uint8_t scene = data[2] & 0x0f;
|
|
DaliBridgeRequest request = FunctionRequest("knx-function-get-scene", BridgeOperation::getSceneLevel);
|
|
request.shortAddress = short_address;
|
|
request.value = DaliValue::Object{{"scene", static_cast<int>(scene)}};
|
|
const auto result = engine_.execute(request);
|
|
if (!result.ok || !result.data.has_value()) {
|
|
*response = {0xff};
|
|
return true;
|
|
}
|
|
const uint8_t raw_level = static_cast<uint8_t>(std::clamp(result.data.value(), 0, 255));
|
|
*response = {static_cast<uint8_t>(raw_level == 0xff ? 0xff : Reg1ArcToPercent(raw_level))};
|
|
if (raw_level != 0xff && data[3] == kReg1DeviceTypeDt8) {
|
|
if (data[4] == kReg1ColorTypeTw) {
|
|
response->resize(3, 0);
|
|
SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, 0xe2, "knx-function-get-scene-ct-selector");
|
|
SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8,
|
|
"knx-function-get-scene-ct-dt-select");
|
|
const uint16_t mirek = static_cast<uint16_t>(
|
|
(QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_VALUE,
|
|
"knx-function-get-scene-mirek-h")
|
|
.value_or(0)
|
|
<< 8) |
|
|
QueryShort(engine_, short_address, DALI_CMD_QUERY_CONTENT_DTR,
|
|
"knx-function-get-scene-mirek-l")
|
|
.value_or(0));
|
|
const uint16_t kelvin = mirek == 0 ? 0 : static_cast<uint16_t>(1000000U / mirek);
|
|
(*response)[1] = static_cast<uint8_t>((kelvin >> 8) & 0xff);
|
|
(*response)[2] = static_cast<uint8_t>(kelvin & 0xff);
|
|
} else {
|
|
response->resize(4, 0);
|
|
const std::array<uint8_t, 3> selectors{0xe9, 0xea, 0xeb};
|
|
for (size_t index = 0; index < selectors.size(); ++index) {
|
|
SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, selectors[index],
|
|
"knx-function-get-scene-rgb-selector");
|
|
SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8,
|
|
"knx-function-get-scene-rgb-dt-select");
|
|
(*response)[index + 1] = static_cast<uint8_t>(
|
|
QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_VALUE,
|
|
"knx-function-get-scene-rgb-value")
|
|
.value_or(0));
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1IdentifyCommand(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (len < 2 || response == nullptr) {
|
|
return false;
|
|
}
|
|
DaliBridgeRequest off = FunctionRequest("knx-function-identify-broadcast-off", BridgeOperation::off);
|
|
off.metadata["broadcast"] = true;
|
|
engine_.execute(off);
|
|
DaliBridgeRequest identify = FunctionRequest("knx-function-identify-recall-max",
|
|
BridgeOperation::recallMaxLevel);
|
|
identify.shortAddress = data[1];
|
|
engine_.execute(identify);
|
|
response->clear();
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1ScanState(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (len < 1 || response == nullptr) {
|
|
return false;
|
|
}
|
|
response->clear();
|
|
response->push_back(commissioning_scan_done_ ? 1 : 0);
|
|
if (data[0] == kReg1FunctionScan) {
|
|
response->push_back(static_cast<uint8_t>(
|
|
std::min<size_t>(commissioning_found_ballasts_.size(), 0xff)));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1AssignState(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (len < 1 || response == nullptr) {
|
|
return false;
|
|
}
|
|
*response = {static_cast<uint8_t>(commissioning_assign_done_ ? 1 : 0)};
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1FoundEvgsState(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (len < 2 || response == nullptr) {
|
|
return false;
|
|
}
|
|
if (data[1] == 254) {
|
|
commissioning_found_ballasts_.clear();
|
|
response->clear();
|
|
return true;
|
|
}
|
|
const size_t index = data[1];
|
|
response->clear();
|
|
response->push_back(index < commissioning_found_ballasts_.size() ? 1 : 0);
|
|
if (index < commissioning_found_ballasts_.size()) {
|
|
const auto& ballast = commissioning_found_ballasts_[index];
|
|
response->push_back(ballast.high);
|
|
response->push_back(ballast.middle);
|
|
response->push_back(ballast.low);
|
|
response->push_back(ballast.short_address);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
DaliBridgeResult GatewayKnxBridge::executeEtsBindings(
|
|
uint16_t group_address, const std::vector<GatewayKnxDaliBinding>& bindings,
|
|
const uint8_t* data, size_t len) {
|
|
if (bindings.empty()) {
|
|
return ErrorResult(group_address, "unmapped ETS KNX group address");
|
|
}
|
|
DaliBridgeResult result;
|
|
result.ok = true;
|
|
result.metadata["source"] = "ets_database";
|
|
result.metadata["groupAddress"] = GatewayKnxGroupAddressString(group_address);
|
|
result.metadata["bindingCount"] = static_cast<int>(bindings.size());
|
|
for (const auto& binding : bindings) {
|
|
DaliBridgeResult child = executeForDecodedWrite(group_address, binding.data_type,
|
|
binding.target, data, len);
|
|
result.ok = result.ok && child.ok;
|
|
result.results.emplace_back(child.toJson());
|
|
}
|
|
result.data = static_cast<int>(result.results.size());
|
|
if (!result.ok) {
|
|
result.error = "one or more ETS KNX bindings failed";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void GatewayKnxBridge::rebuildEtsBindings() {
|
|
ets_bindings_by_group_address_.clear();
|
|
for (const auto& association : config_.ets_associations) {
|
|
const auto binding = EtsBindingForAssociation(config_.main_group, association);
|
|
if (!binding.has_value()) {
|
|
continue;
|
|
}
|
|
ets_bindings_by_group_address_[association.group_address].push_back(binding.value());
|
|
}
|
|
}
|
|
|
|
DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address,
|
|
GatewayKnxDaliDataType data_type,
|
|
GatewayKnxDaliTarget target,
|
|
const uint8_t* data, size_t len) {
|
|
if (target.kind == GatewayKnxDaliTargetKind::kNone) {
|
|
return ErrorResult(group_address, "missing DALI target");
|
|
}
|
|
switch (data_type) {
|
|
case GatewayKnxDaliDataType::kSwitch: {
|
|
if (data == nullptr || len < 1) {
|
|
return ErrorResult(group_address, "missing DPT1 switch payload");
|
|
}
|
|
DaliBridgeRequest request = RequestForTarget(
|
|
group_address, target, (data[0] & 0x01) != 0 ? BridgeOperation::on : BridgeOperation::off);
|
|
return engine_.execute(request);
|
|
}
|
|
case GatewayKnxDaliDataType::kBrightness: {
|
|
if (data == nullptr || len < 1) {
|
|
return ErrorResult(group_address, "missing DPT5 brightness payload");
|
|
}
|
|
DaliBridgeRequest request = RequestForTarget(group_address, target,
|
|
BridgeOperation::setBrightnessPercent);
|
|
request.value = (static_cast<double>(data[0]) * 100.0) / 255.0;
|
|
return engine_.execute(request);
|
|
}
|
|
case GatewayKnxDaliDataType::kColorTemperature: {
|
|
if (data == nullptr || len < 2) {
|
|
return ErrorResult(group_address, "missing DPT7 color temperature payload");
|
|
}
|
|
DaliBridgeRequest request = RequestForTarget(group_address, target,
|
|
BridgeOperation::setColorTemperature);
|
|
request.value = static_cast<int>(ReadBe16(data));
|
|
return engine_.execute(request);
|
|
}
|
|
case GatewayKnxDaliDataType::kRgb: {
|
|
if (data == nullptr || len < 3) {
|
|
return ErrorResult(group_address, "missing DPT232 RGB payload");
|
|
}
|
|
DaliBridgeRequest request = RequestForTarget(group_address, target,
|
|
BridgeOperation::setColourRGB);
|
|
DaliValue::Object rgb;
|
|
rgb["r"] = static_cast<int>(data[0]);
|
|
rgb["g"] = static_cast<int>(data[1]);
|
|
rgb["b"] = static_cast<int>(data[2]);
|
|
request.value = std::move(rgb);
|
|
return engine_.execute(request);
|
|
}
|
|
case GatewayKnxDaliDataType::kUnknown:
|
|
default:
|
|
return ErrorResult(group_address, "unsupported KNX data type");
|
|
}
|
|
}
|
|
|
|
GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler,
|
|
std::string openknx_namespace)
|
|
: bridge_(bridge),
|
|
handler_(std::move(handler)),
|
|
openknx_namespace_(std::move(openknx_namespace)) {}
|
|
|
|
GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { stop(); }
|
|
|
|
void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; }
|
|
|
|
const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; }
|
|
|
|
esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) {
|
|
if (started_ || task_handle_ != nullptr) {
|
|
return ESP_OK;
|
|
}
|
|
if (!config_.ip_router_enabled) {
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
stop_requested_ = false;
|
|
last_error_.clear();
|
|
if (!configureSocket()) {
|
|
return ESP_FAIL;
|
|
}
|
|
ets_device_ = std::make_unique<openknx::EtsDeviceRuntime>(openknx_namespace_,
|
|
config_.individual_address);
|
|
ets_device_->setFunctionPropertyHandlers(
|
|
[this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
return bridge_.handleFunctionPropertyCommand(object_index, property_id, data, len, response);
|
|
},
|
|
[this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
return bridge_.handleFunctionPropertyState(object_index, property_id, data, len, response);
|
|
});
|
|
if (!configureTpUart()) {
|
|
ets_device_.reset();
|
|
closeSockets();
|
|
return ESP_FAIL;
|
|
}
|
|
const BaseType_t created = xTaskCreate(&GatewayKnxTpIpRouter::TaskEntry, "gw_knx_ip",
|
|
task_stack_size, this, task_priority, &task_handle_);
|
|
if (created != pdPASS) {
|
|
task_handle_ = nullptr;
|
|
closeSockets();
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
started_ = true;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t GatewayKnxTpIpRouter::stop() {
|
|
stop_requested_ = true;
|
|
closeSockets();
|
|
const TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
|
for (int attempt = 0; task_handle_ != nullptr && task_handle_ != current_task && attempt < 50;
|
|
++attempt) {
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::started() const { return started_; }
|
|
|
|
const std::string& GatewayKnxTpIpRouter::lastError() const { return last_error_; }
|
|
|
|
void GatewayKnxTpIpRouter::TaskEntry(void* arg) {
|
|
static_cast<GatewayKnxTpIpRouter*>(arg)->taskLoop();
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::taskLoop() {
|
|
std::array<uint8_t, 768> buffer{};
|
|
while (!stop_requested_) {
|
|
sockaddr_in remote{};
|
|
socklen_t remote_len = sizeof(remote);
|
|
const int received = recvfrom(udp_sock_, buffer.data(), buffer.size(), 0,
|
|
reinterpret_cast<sockaddr*>(&remote), &remote_len);
|
|
if (received <= 0) {
|
|
pollTpUart();
|
|
if (ets_device_ != nullptr) {
|
|
ets_device_->loop();
|
|
}
|
|
if (!stop_requested_) {
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
}
|
|
continue;
|
|
}
|
|
handleUdpDatagram(buffer.data(), static_cast<size_t>(received), remote);
|
|
pollTpUart();
|
|
if (ets_device_ != nullptr) {
|
|
ets_device_->loop();
|
|
}
|
|
}
|
|
finishTask();
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::finishTask() {
|
|
closeSockets();
|
|
ets_device_.reset();
|
|
started_ = false;
|
|
task_handle_ = nullptr;
|
|
vTaskDelete(nullptr);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::closeSockets() {
|
|
if (udp_sock_ >= 0) {
|
|
shutdown(udp_sock_, SHUT_RDWR);
|
|
close(udp_sock_);
|
|
udp_sock_ = -1;
|
|
}
|
|
if (tp_uart_port_ >= 0) {
|
|
uart_driver_delete(static_cast<uart_port_t>(tp_uart_port_));
|
|
tp_uart_port_ = -1;
|
|
}
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::configureSocket() {
|
|
udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
|
if (udp_sock_ < 0) {
|
|
last_error_ = "failed to create KNXnet/IP UDP socket";
|
|
return false;
|
|
}
|
|
int reuse = 1;
|
|
setsockopt(udp_sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
|
|
|
|
sockaddr_in bind_addr{};
|
|
bind_addr.sin_family = AF_INET;
|
|
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
|
bind_addr.sin_port = htons(config_.udp_port);
|
|
if (bind(udp_sock_, reinterpret_cast<sockaddr*>(&bind_addr), sizeof(bind_addr)) < 0) {
|
|
last_error_ = "failed to bind KNXnet/IP UDP socket";
|
|
closeSockets();
|
|
return false;
|
|
}
|
|
|
|
timeval timeout{};
|
|
timeout.tv_sec = 0;
|
|
timeout.tv_usec = 20000;
|
|
setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
|
|
|
if (config_.multicast_enabled) {
|
|
uint8_t multicast_loop = 0;
|
|
setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &multicast_loop,
|
|
sizeof(multicast_loop));
|
|
ip_mreq mreq{};
|
|
mreq.imr_multiaddr.s_addr = inet_addr(config_.multicast_address.c_str());
|
|
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
|
|
if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
|
|
ESP_LOGW(kTag, "failed to join KNX multicast group %s", config_.multicast_address.c_str());
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::configureTpUart() {
|
|
const auto& serial = config_.tp_uart;
|
|
if (serial.uart_port < 0 || serial.uart_port > 2) {
|
|
last_error_ = "invalid KNX TP-UART port";
|
|
return false;
|
|
}
|
|
uart_config_t uart_config{};
|
|
uart_config.baud_rate = static_cast<int>(serial.baudrate);
|
|
uart_config.data_bits = UART_DATA_8_BITS;
|
|
uart_config.parity = UART_PARITY_EVEN;
|
|
uart_config.stop_bits = UART_STOP_BITS_1;
|
|
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
|
|
uart_config.source_clk = UART_SCLK_DEFAULT;
|
|
const uart_port_t uart_port = static_cast<uart_port_t>(serial.uart_port);
|
|
if (uart_param_config(uart_port, &uart_config) != ESP_OK) {
|
|
last_error_ = "failed to configure KNX TP-UART parameters";
|
|
return false;
|
|
}
|
|
if (uart_set_pin(uart_port, serial.tx_pin, serial.rx_pin, UART_PIN_NO_CHANGE,
|
|
UART_PIN_NO_CHANGE) != ESP_OK) {
|
|
last_error_ = "failed to configure KNX TP-UART pins";
|
|
return false;
|
|
}
|
|
if (uart_driver_install(uart_port, serial.rx_buffer_size, serial.tx_buffer_size, 0, nullptr,
|
|
0) != ESP_OK) {
|
|
last_error_ = "failed to install KNX TP-UART driver";
|
|
return false;
|
|
}
|
|
tp_uart_port_ = serial.uart_port;
|
|
return initializeTpUart();
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::initializeTpUart() {
|
|
if (tp_uart_port_ < 0) {
|
|
return false;
|
|
}
|
|
const uart_port_t uart_port = static_cast<uart_port_t>(tp_uart_port_);
|
|
tp_rx_frame_.clear();
|
|
tp_last_sent_telegram_.clear();
|
|
tp_uart_last_byte_tick_ = 0;
|
|
tp_uart_extended_frame_ = false;
|
|
tp_uart_online_ = false;
|
|
uart_flush_input(uart_port);
|
|
|
|
const uint8_t reset_request = kTpUartResetRequest;
|
|
if (uart_write_bytes(uart_port, &reset_request, 1) != 1) {
|
|
last_error_ = "failed to send KNX TP-UART reset request";
|
|
return false;
|
|
}
|
|
|
|
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(1500);
|
|
bool saw_reset = false;
|
|
std::array<uint8_t, 32> buffer{};
|
|
while (xTaskGetTickCount() < deadline) {
|
|
const int read = uart_read_bytes(uart_port, buffer.data(), buffer.size(),
|
|
pdMS_TO_TICKS(config_.tp_uart.read_timeout_ms));
|
|
if (read <= 0) {
|
|
continue;
|
|
}
|
|
for (int index = 0; index < read; ++index) {
|
|
const uint8_t byte = buffer[static_cast<size_t>(index)];
|
|
if (!saw_reset) {
|
|
if (byte == kTpUartResetIndication) {
|
|
saw_reset = true;
|
|
const std::array<uint8_t, 3> set_address{
|
|
kTpUartSetAddressRequest,
|
|
static_cast<uint8_t>((effectiveIndividualAddress() >> 8) & 0xff),
|
|
static_cast<uint8_t>(effectiveIndividualAddress() & 0xff),
|
|
};
|
|
uart_write_bytes(uart_port, set_address.data(), set_address.size());
|
|
const uint8_t state_request = kTpUartStateRequest;
|
|
uart_write_bytes(uart_port, &state_request, 1);
|
|
}
|
|
continue;
|
|
}
|
|
if ((byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask) {
|
|
tp_uart_online_ = true;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
last_error_ = saw_reset ? "timed out waiting for KNX TP-UART state indication"
|
|
: "timed out waiting for KNX TP-UART reset indication";
|
|
return false;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len,
|
|
const sockaddr_in& remote) {
|
|
uint16_t service = 0;
|
|
uint16_t total_len = 0;
|
|
if (!ParseKnxNetIpHeader(data, len, &service, &total_len)) {
|
|
return;
|
|
}
|
|
const uint8_t* body = data + 6;
|
|
const size_t body_len = total_len - 6;
|
|
switch (service) {
|
|
case kServiceRoutingIndication:
|
|
if (config_.multicast_enabled) {
|
|
handleRoutingIndication(body, body_len);
|
|
}
|
|
break;
|
|
case kServiceTunnellingRequest:
|
|
if (config_.tunnel_enabled) {
|
|
handleTunnellingRequest(body, body_len, remote);
|
|
}
|
|
break;
|
|
case kServiceConnectRequest:
|
|
if (config_.tunnel_enabled) {
|
|
handleConnectRequest(body, body_len, remote);
|
|
}
|
|
break;
|
|
case kServiceConnectionStateRequest:
|
|
handleConnectionStateRequest(body, body_len, remote);
|
|
break;
|
|
case kServiceDisconnectRequest:
|
|
handleDisconnectRequest(body, body_len, remote);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* body, size_t len) {
|
|
if (body == nullptr || len == 0) {
|
|
return;
|
|
}
|
|
const DaliBridgeResult result = handler_(body, len);
|
|
if (!result.ok && !result.error.empty()) {
|
|
ESP_LOGD(kTag, "KNX routing indication ignored: %s", result.error.c_str());
|
|
}
|
|
forwardCemiToTp(body, len);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t len,
|
|
const sockaddr_in& remote) {
|
|
if (body == nullptr || len < 5 || body[0] != 0x04) {
|
|
return;
|
|
}
|
|
const uint8_t channel_id = body[1];
|
|
const uint8_t sequence = body[2];
|
|
if (!tunnel_connected_ || channel_id != tunnel_channel_id_) {
|
|
sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote);
|
|
return;
|
|
}
|
|
if (sequence != expected_tunnel_sequence_) {
|
|
sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote);
|
|
return;
|
|
}
|
|
expected_tunnel_sequence_ = static_cast<uint8_t>((expected_tunnel_sequence_ + 1) & 0xff);
|
|
sendTunnellingAck(channel_id, sequence, kKnxNoError, remote);
|
|
const uint8_t* cemi = body + 4;
|
|
const size_t cemi_len = len - 4;
|
|
const bool consumed_by_openknx = handleOpenKnxTunnelFrame(cemi, cemi_len);
|
|
if (consumed_by_openknx) {
|
|
return;
|
|
}
|
|
const DaliBridgeResult result = handler_(cemi, cemi_len);
|
|
if (!result.ok && !result.error.empty()) {
|
|
ESP_LOGD(kTag, "KNX tunnel frame not routed to DALI: %s", result.error.c_str());
|
|
}
|
|
forwardCemiToTp(cemi, cemi_len);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* body, size_t len,
|
|
const sockaddr_in& remote) {
|
|
if (body == nullptr || len < 20) {
|
|
return;
|
|
}
|
|
const size_t cri_offset = 16;
|
|
if (body[cri_offset] < 4 || body[cri_offset + 1] != kKnxConnectionTypeTunnel ||
|
|
body[cri_offset + 2] != kKnxTunnelLayerLink) {
|
|
sendConnectResponse(0, kKnxErrorConnectionType, remote);
|
|
return;
|
|
}
|
|
if (tunnel_connected_) {
|
|
sendConnectResponse(0, kKnxErrorNoMoreConnections, remote);
|
|
return;
|
|
}
|
|
tunnel_connected_ = true;
|
|
expected_tunnel_sequence_ = 0;
|
|
tunnel_send_sequence_ = 0;
|
|
tunnel_remote_ = remote;
|
|
sendConnectResponse(tunnel_channel_id_, kKnxNoError, remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* body, size_t len,
|
|
const sockaddr_in& remote) {
|
|
if (body == nullptr || len < 2) {
|
|
return;
|
|
}
|
|
const uint8_t channel_id = body[0];
|
|
sendConnectionStateResponse(
|
|
channel_id, tunnel_connected_ && channel_id == tunnel_channel_id_ ? kKnxNoError
|
|
: kKnxErrorConnectionId,
|
|
remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* body, size_t len,
|
|
const sockaddr_in& remote) {
|
|
if (body == nullptr || len < 2) {
|
|
return;
|
|
}
|
|
const uint8_t channel_id = body[0];
|
|
const uint8_t status = tunnel_connected_ && channel_id == tunnel_channel_id_
|
|
? kKnxNoError
|
|
: kKnxErrorConnectionId;
|
|
if (status == kKnxNoError) {
|
|
tunnel_connected_ = false;
|
|
expected_tunnel_sequence_ = 0;
|
|
tunnel_send_sequence_ = 0;
|
|
}
|
|
sendDisconnectResponse(channel_id, status, remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequence,
|
|
uint8_t status, const sockaddr_in& remote) {
|
|
const std::vector<uint8_t> body{0x04, channel_id, sequence, status};
|
|
const auto packet = KnxNetIpPacket(kServiceTunnellingAck, body);
|
|
SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) {
|
|
if (!tunnel_connected_ || udp_sock_ < 0 || data == nullptr || len == 0) {
|
|
return;
|
|
}
|
|
std::vector<uint8_t> body;
|
|
body.reserve(4 + len);
|
|
body.push_back(0x04);
|
|
body.push_back(tunnel_channel_id_);
|
|
body.push_back(tunnel_send_sequence_++);
|
|
body.push_back(0x00);
|
|
body.insert(body.end(), data, data + len);
|
|
const auto packet = KnxNetIpPacket(kServiceTunnellingRequest, body);
|
|
SendAll(udp_sock_, packet.data(), packet.size(), tunnel_remote_);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
|
|
const sockaddr_in& remote) {
|
|
const std::vector<uint8_t> body{channel_id, status};
|
|
const auto packet = KnxNetIpPacket(kServiceConnectionStateResponse, body);
|
|
SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendDisconnectResponse(uint8_t channel_id, uint8_t status,
|
|
const sockaddr_in& remote) {
|
|
const std::vector<uint8_t> body{channel_id, status};
|
|
const auto packet = KnxNetIpPacket(kServiceDisconnectResponse, body);
|
|
SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t status,
|
|
const sockaddr_in& remote) {
|
|
std::vector<uint8_t> body;
|
|
body.reserve(16);
|
|
body.push_back(channel_id);
|
|
body.push_back(status);
|
|
const auto data_endpoint = HpaiForRemote(remote);
|
|
body.insert(body.end(), data_endpoint.begin(), data_endpoint.end());
|
|
body.push_back(0x04);
|
|
body.push_back(kKnxConnectionTypeTunnel);
|
|
body.push_back(static_cast<uint8_t>((effectiveTunnelAddress() >> 8) & 0xff));
|
|
body.push_back(static_cast<uint8_t>(effectiveTunnelAddress() & 0xff));
|
|
const auto packet = KnxNetIpPacket(kServiceConnectResponse, body);
|
|
SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len) {
|
|
if (!config_.multicast_enabled || udp_sock_ < 0 || data == nullptr || len == 0) {
|
|
return;
|
|
}
|
|
sockaddr_in remote{};
|
|
remote.sin_family = AF_INET;
|
|
remote.sin_port = htons(config_.udp_port);
|
|
remote.sin_addr.s_addr = inet_addr(config_.multicast_address.c_str());
|
|
const std::vector<uint8_t> body(data, data + len);
|
|
const auto packet = KnxNetIpPacket(kServiceRoutingIndication, body);
|
|
SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len) {
|
|
if (ets_device_ == nullptr) {
|
|
return false;
|
|
}
|
|
const bool consumed = ets_device_->handleTunnelFrame(
|
|
data, len, [this](const uint8_t* response, size_t response_len) {
|
|
sendTunnelIndication(response, response_len);
|
|
});
|
|
syncOpenKnxConfigFromDevice();
|
|
return consumed;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() {
|
|
if (ets_device_ == nullptr) {
|
|
return;
|
|
}
|
|
const auto snapshot = ets_device_->snapshot();
|
|
bool changed = false;
|
|
GatewayKnxConfig updated = config_;
|
|
if (snapshot.individual_address != 0 && snapshot.individual_address != 0xffff &&
|
|
snapshot.individual_address != updated.individual_address) {
|
|
updated.individual_address = snapshot.individual_address;
|
|
changed = true;
|
|
}
|
|
if (snapshot.configured || !snapshot.associations.empty()) {
|
|
std::vector<GatewayKnxEtsAssociation> associations;
|
|
associations.reserve(snapshot.associations.size());
|
|
for (const auto& association : snapshot.associations) {
|
|
associations.push_back(GatewayKnxEtsAssociation{association.group_address,
|
|
association.group_object_number});
|
|
}
|
|
if (associations.size() != updated.ets_associations.size() ||
|
|
!std::equal(associations.begin(), associations.end(), updated.ets_associations.begin(),
|
|
[](const GatewayKnxEtsAssociation& lhs,
|
|
const GatewayKnxEtsAssociation& rhs) {
|
|
return lhs.group_address == rhs.group_address &&
|
|
lhs.group_object_number == rhs.group_object_number;
|
|
})) {
|
|
updated.ets_associations = std::move(associations);
|
|
changed = true;
|
|
}
|
|
}
|
|
if (!changed) {
|
|
return;
|
|
}
|
|
config_ = updated;
|
|
bridge_.setConfig(config_);
|
|
}
|
|
|
|
uint16_t GatewayKnxTpIpRouter::effectiveIndividualAddress() const {
|
|
if (ets_device_ != nullptr) {
|
|
const uint16_t address = ets_device_->individualAddress();
|
|
if (address != 0 && address != 0xffff) {
|
|
return address;
|
|
}
|
|
}
|
|
return config_.individual_address;
|
|
}
|
|
|
|
uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddress() const {
|
|
if (ets_device_ != nullptr) {
|
|
const uint16_t address = ets_device_->tunnelClientAddress();
|
|
if (address != 0 && address != 0xffff) {
|
|
return address;
|
|
}
|
|
}
|
|
uint16_t device = static_cast<uint16_t>((config_.individual_address & 0x00ff) + 1);
|
|
if (device == 0 || device > 0xff) {
|
|
device = 1;
|
|
}
|
|
return static_cast<uint16_t>((config_.individual_address & 0xff00) | device);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::pollTpUart() {
|
|
if (tp_uart_port_ < 0) {
|
|
return;
|
|
}
|
|
std::array<uint8_t, 128> buffer{};
|
|
const int read = uart_read_bytes(static_cast<uart_port_t>(tp_uart_port_), buffer.data(),
|
|
buffer.size(), 0);
|
|
if (read <= 0) {
|
|
return;
|
|
}
|
|
for (int index = 0; index < read; ++index) {
|
|
const uint8_t byte = buffer[static_cast<size_t>(index)];
|
|
if (tp_rx_frame_.empty()) {
|
|
if (IsTpUartControlByte(byte)) {
|
|
handleTpUartControlByte(byte);
|
|
continue;
|
|
}
|
|
if (byte == 0xcb || (byte & 0x17U) == 0x13U) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const TickType_t now = xTaskGetTickCount();
|
|
if (!tp_rx_frame_.empty() && tp_uart_last_byte_tick_ != 0 &&
|
|
now - tp_uart_last_byte_tick_ > pdMS_TO_TICKS(1000)) {
|
|
tp_rx_frame_.clear();
|
|
}
|
|
|
|
if (tp_rx_frame_.empty()) {
|
|
if (IsTpUartFrameStart(byte, &tp_uart_extended_frame_)) {
|
|
tp_rx_frame_.push_back(byte);
|
|
tp_uart_last_byte_tick_ = now;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
tp_rx_frame_.push_back(byte);
|
|
tp_uart_last_byte_tick_ = now;
|
|
const size_t expected = ExpectedTpFrameSize(tp_rx_frame_.data(), tp_rx_frame_.size());
|
|
if (expected == 0) {
|
|
continue;
|
|
}
|
|
if (tp_rx_frame_.size() == expected) {
|
|
const uint8_t ack = kTpUartAckInfo;
|
|
uart_write_bytes(static_cast<uart_port_t>(tp_uart_port_), &ack, 1);
|
|
handleTpTelegram(tp_rx_frame_.data(), tp_rx_frame_.size());
|
|
tp_rx_frame_.clear();
|
|
} else if (tp_rx_frame_.size() > expected || tp_rx_frame_.size() > 263U) {
|
|
tp_rx_frame_.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleTpUartControlByte(uint8_t byte) {
|
|
if (byte == kTpUartResetIndication) {
|
|
ESP_LOGW(kTag, "KNX TP-UART reset indication received; marking link offline");
|
|
tp_uart_online_ = false;
|
|
return;
|
|
}
|
|
if (byte == kTpUartBusy) {
|
|
last_error_ = "KNX TP-UART bus busy";
|
|
ESP_LOGW(kTag, "%s", last_error_.c_str());
|
|
return;
|
|
}
|
|
if (byte == kTpUartLDataConfirmNegative) {
|
|
last_error_ = "KNX TP-UART negative confirmation";
|
|
ESP_LOGW(kTag, "%s", last_error_.c_str());
|
|
return;
|
|
}
|
|
if (byte == kTpUartLDataConfirmPositive) {
|
|
return;
|
|
}
|
|
if ((byte & kTpUartStateIndicationMask) == kTpUartStateIndicationMask) {
|
|
tp_uart_online_ = true;
|
|
}
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleTpTelegram(const uint8_t* data, size_t len) {
|
|
if (data == nullptr || len == 0) {
|
|
return;
|
|
}
|
|
const std::vector<uint8_t> telegram(data, data + len);
|
|
if (!tp_last_sent_telegram_.empty() &&
|
|
TpTelegramEqualsIgnoringRepeatBit(telegram, tp_last_sent_telegram_)) {
|
|
tp_last_sent_telegram_.clear();
|
|
return;
|
|
}
|
|
const auto cemi = TpTelegramToCemi(data, len);
|
|
if (!cemi.has_value()) {
|
|
return;
|
|
}
|
|
const DaliBridgeResult result = handler_(cemi->data(), cemi->size());
|
|
if (!result.ok && !result.error.empty()) {
|
|
ESP_LOGD(kTag, "KNX TP frame not routed to DALI: %s", result.error.c_str());
|
|
}
|
|
sendTunnelIndication(cemi->data(), cemi->size());
|
|
sendRoutingIndication(cemi->data(), cemi->size());
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::forwardCemiToTp(const uint8_t* data, size_t len) {
|
|
if (tp_uart_port_ < 0 || data == nullptr || len == 0 || !tp_uart_online_) {
|
|
return;
|
|
}
|
|
const auto telegram = CemiToTpTelegram(data, len);
|
|
if (!telegram.has_value()) {
|
|
return;
|
|
}
|
|
tp_last_sent_telegram_ = *telegram;
|
|
const auto wrapped = WrapTpUartTelegram(*telegram);
|
|
uart_write_bytes(static_cast<uart_port_t>(tp_uart_port_), wrapped.data(), wrapped.size());
|
|
}
|
|
|
|
} // namespace gateway
|