b447da5bfc
Signed-off-by: Tony <tonylu@tony-cloud.com>
4307 lines
174 KiB
C++
4307 lines
174 KiB
C++
#include "gateway_knx.hpp"
|
|
|
|
#include "dali_define.hpp"
|
|
#include "driver/gpio.h"
|
|
#include "driver/uart.h"
|
|
#include "esp_mac.h"
|
|
#include "esp_netif.h"
|
|
#include "esp_log.h"
|
|
#include "lwip/inet.h"
|
|
#include "lwip/sockets.h"
|
|
#include "ets_device_runtime.h"
|
|
#include "gateway_knx_internal.h"
|
|
#include "soc/uart_periph.h"
|
|
#include "tpuart_uart_interface.h"
|
|
|
|
#include "knx/cemi_frame.h"
|
|
#include "knx/knx_ip_connect_request.h"
|
|
#include "knx/knx_ip_connect_response.h"
|
|
#include "knx/knx_ip_config_request.h"
|
|
#include "knx/knx_ip_description_request.h"
|
|
#include "knx/knx_ip_routing_indication.h"
|
|
#include "knx/knx_ip_disconnect_request.h"
|
|
#include "knx/knx_ip_disconnect_response.h"
|
|
#include "knx/knx_ip_search_request.h"
|
|
#include "knx/knx_ip_search_response.h"
|
|
#include "knx/knx_ip_description_response.h"
|
|
#include "knx/knx_ip_state_request.h"
|
|
#include "knx/knx_ip_state_response.h"
|
|
#include "knx/knx_ip_tunneling_ack.h"
|
|
#include "knx/knx_ip_tunneling_request.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cerrno>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <initializer_list>
|
|
#include <set>
|
|
#include <utility>
|
|
#include <unistd.h>
|
|
|
|
namespace gateway {
|
|
namespace {
|
|
|
|
constexpr const char* kTag = "gateway_knx";
|
|
constexpr uint16_t kServiceSearchRequest = 0x0201;
|
|
constexpr uint16_t kServiceSearchResponse = 0x0202;
|
|
constexpr uint16_t kServiceDescriptionRequest = 0x0203;
|
|
constexpr uint16_t kServiceDescriptionResponse = 0x0204;
|
|
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 kServiceSearchRequestExt = 0x020b;
|
|
constexpr uint16_t kServiceSearchResponseExt = 0x020c;
|
|
constexpr uint16_t kServiceDeviceConfigurationRequest = 0x0310;
|
|
constexpr uint16_t kServiceDeviceConfigurationAck = 0x0311;
|
|
constexpr uint16_t kServiceTunnellingRequest = 0x0420;
|
|
constexpr uint16_t kServiceTunnellingAck = 0x0421;
|
|
constexpr uint16_t kServiceRoutingIndication = 0x0530;
|
|
constexpr uint16_t kServiceSecureWrapper = 0x0950;
|
|
constexpr uint16_t kServiceSecureSessionRequest = 0x0951;
|
|
constexpr uint16_t kServiceSecureSessionResponse = 0x0952;
|
|
constexpr uint16_t kServiceSecureSessionAuth = 0x0953;
|
|
constexpr uint16_t kServiceSecureSessionStatus = 0x0954;
|
|
constexpr uint16_t kServiceSecureGroupSync = 0x0955;
|
|
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 kKnxErrorTunnellingLayer = 0x29;
|
|
constexpr uint8_t kKnxErrorSequenceNumber = 0x04;
|
|
constexpr uint8_t kKnxSecureStatusAuthFailed = 0x01;
|
|
constexpr uint8_t kKnxSecureStatusUnauthenticated = 0x02;
|
|
constexpr uint8_t kKnxConnectionTypeDeviceManagement = 0x03;
|
|
constexpr uint8_t kKnxConnectionTypeTunnel = 0x04;
|
|
constexpr uint8_t kKnxTunnelLayerLink = 0x02;
|
|
constexpr uint8_t kKnxHpaiIpv4Udp = 0x01;
|
|
constexpr uint8_t kKnxHpaiIpv4Tcp = 0x02;
|
|
constexpr uint8_t kKnxDibDeviceInfo = 0x01;
|
|
constexpr uint8_t kKnxDibSupportedServices = 0x02;
|
|
constexpr uint8_t kKnxDibIpConfig = 0x03;
|
|
constexpr uint8_t kKnxDibCurrentIpConfig = 0x04;
|
|
constexpr uint8_t kKnxDibKnxAddresses = 0x05;
|
|
constexpr uint8_t kKnxDibTunnellingInfo = 0x07;
|
|
constexpr uint8_t kKnxDibExtendedDeviceInfo = 0x08;
|
|
constexpr uint8_t kKnxMediumTp1 = 0x02;
|
|
constexpr uint8_t kKnxMediumIp = 0x20;
|
|
constexpr uint8_t kKnxServiceFamilyCore = 0x02;
|
|
constexpr uint8_t kKnxServiceFamilyDeviceManagement = 0x03;
|
|
constexpr uint8_t kKnxServiceFamilyTunnelling = 0x04;
|
|
constexpr uint8_t kKnxServiceFamilyRouting = 0x05;
|
|
constexpr uint16_t kKnxIpOnlyDeviceDescriptor = 0x57b0;
|
|
constexpr uint16_t kKnxTpIpInterfaceDeviceDescriptor = 0x091a;
|
|
constexpr uint8_t kKnxIpAssignmentManual = 0x01;
|
|
constexpr uint8_t kKnxIpCapabilityManual = 0x01;
|
|
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 uint16_t kGwReg1AppKoScene = 5;
|
|
constexpr uint8_t kGwReg1KoSwitch = 0;
|
|
constexpr uint8_t kGwReg1KoDimmRelative = 2;
|
|
constexpr uint8_t kGwReg1KoDimmAbsolute = 3;
|
|
constexpr uint8_t kGwReg1KoColor = 6;
|
|
constexpr uint8_t kGwReg1KoSwitchState = 1;
|
|
constexpr uint8_t kGwReg1KoDimmState = 4;
|
|
constexpr uint8_t kReg1SceneTelegramNumberMask = 0x3f;
|
|
constexpr uint8_t kReg1SceneTelegramStoreMask = 0x80;
|
|
constexpr size_t kReg1SceneEntryCount = 64;
|
|
constexpr uint32_t kReg1SceneParamBlockOffset = 47;
|
|
constexpr uint32_t kReg1SceneParamBlockSize = 4;
|
|
constexpr uint8_t kReg1SceneTypeNone = 0;
|
|
constexpr uint8_t kReg1SceneTypeAddress = 1;
|
|
constexpr uint8_t kReg1SceneTypeGroup = 2;
|
|
constexpr uint8_t kReg1SceneTypeBroadcast = 3;
|
|
constexpr uint8_t kDaliCmdStepDownOff = 0x07;
|
|
constexpr uint8_t kDaliCmdOnStepUp = 0x08;
|
|
constexpr uint8_t kDaliCmdStopFade = 0xff;
|
|
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;
|
|
};
|
|
|
|
struct KnxNetifInfo {
|
|
const char* key{nullptr};
|
|
esp_netif_t* netif{nullptr};
|
|
uint32_t address{0};
|
|
uint32_t netmask{0};
|
|
uint32_t gateway{0};
|
|
};
|
|
|
|
class SemaphoreGuard {
|
|
public:
|
|
explicit SemaphoreGuard(SemaphoreHandle_t semaphore) : semaphore_(semaphore) {
|
|
if (semaphore_ != nullptr) {
|
|
xSemaphoreTake(semaphore_, portMAX_DELAY);
|
|
locked_ = true;
|
|
}
|
|
}
|
|
|
|
~SemaphoreGuard() {
|
|
if (locked_) {
|
|
xSemaphoreGive(semaphore_);
|
|
}
|
|
}
|
|
|
|
private:
|
|
SemaphoreHandle_t semaphore_{nullptr};
|
|
bool locked_{false};
|
|
};
|
|
|
|
uint8_t DaliArcLevelToDpt5(uint8_t actual_level) {
|
|
return static_cast<uint8_t>(
|
|
std::clamp<int>(static_cast<int>(std::lround(actual_level * 255.0 / 254.0)), 0, 255));
|
|
}
|
|
|
|
uint16_t ReadBe16(const uint8_t* data) {
|
|
return static_cast<uint16_t>((static_cast<uint16_t>(data[0]) << 8) | data[1]);
|
|
}
|
|
|
|
std::string EspErrDetail(const std::string& message, esp_err_t err) {
|
|
return std::string(message) + ": " + esp_err_to_name(err) + "(" + std::to_string(err) + ")";
|
|
}
|
|
|
|
std::string ErrnoDetail(const std::string& message, int err) {
|
|
return std::string(message) + ": errno=" + std::to_string(err) + " (" + std::strerror(err) + ")";
|
|
}
|
|
|
|
bool ResolveUartIoPin(uart_port_t uart_port, int configured_pin, uint32_t pin_index,
|
|
int* resolved_pin) {
|
|
if (resolved_pin == nullptr) {
|
|
return false;
|
|
}
|
|
if (configured_pin >= 0) {
|
|
*resolved_pin = configured_pin;
|
|
return true;
|
|
}
|
|
if (uart_port < 0 || uart_port >= SOC_UART_NUM || pin_index >= SOC_UART_PINS_COUNT) {
|
|
*resolved_pin = UART_PIN_NO_CHANGE;
|
|
return false;
|
|
}
|
|
const int default_pin = uart_periph_signal[uart_port].pins[pin_index].default_gpio;
|
|
if (default_pin < 0) {
|
|
*resolved_pin = UART_PIN_NO_CHANGE;
|
|
return false;
|
|
}
|
|
*resolved_pin = default_pin;
|
|
return true;
|
|
}
|
|
|
|
std::string UartPinDescription(int configured_pin, int resolved_pin) {
|
|
if (configured_pin >= 0) {
|
|
return std::to_string(configured_pin);
|
|
}
|
|
if (resolved_pin >= 0) {
|
|
return std::to_string(resolved_pin) + " (default from -1)";
|
|
}
|
|
return "unrouted (-1 with no target default)";
|
|
}
|
|
|
|
std::string Ipv4String(uint32_t network_address) {
|
|
const uint32_t address = ntohl(network_address);
|
|
char buffer[16]{};
|
|
std::snprintf(buffer, sizeof(buffer), "%u.%u.%u.%u",
|
|
static_cast<unsigned>((address >> 24) & 0xff),
|
|
static_cast<unsigned>((address >> 16) & 0xff),
|
|
static_cast<unsigned>((address >> 8) & 0xff),
|
|
static_cast<unsigned>(address & 0xff));
|
|
return buffer;
|
|
}
|
|
|
|
std::string EndpointString(const sockaddr_in& endpoint) {
|
|
return Ipv4String(endpoint.sin_addr.s_addr) + ":" + std::to_string(ntohs(endpoint.sin_port));
|
|
}
|
|
|
|
bool EndpointEquals(const sockaddr_in& lhs, const sockaddr_in& rhs) {
|
|
return lhs.sin_family == rhs.sin_family && lhs.sin_addr.s_addr == rhs.sin_addr.s_addr &&
|
|
lhs.sin_port == rhs.sin_port;
|
|
}
|
|
|
|
void WriteIp(uint8_t* data, uint32_t network_address) {
|
|
const uint32_t address = ntohl(network_address);
|
|
data[0] = static_cast<uint8_t>((address >> 24) & 0xff);
|
|
data[1] = static_cast<uint8_t>((address >> 16) & 0xff);
|
|
data[2] = static_cast<uint8_t>((address >> 8) & 0xff);
|
|
data[3] = static_cast<uint8_t>(address & 0xff);
|
|
}
|
|
|
|
bool ReadBaseMac(uint8_t* data) {
|
|
if (data == nullptr) {
|
|
return false;
|
|
}
|
|
if (esp_efuse_mac_get_default(data) == ESP_OK) {
|
|
return true;
|
|
}
|
|
return esp_read_mac(data, ESP_MAC_WIFI_STA) == ESP_OK;
|
|
}
|
|
|
|
std::vector<KnxNetifInfo> ActiveKnxNetifs() {
|
|
std::vector<KnxNetifInfo> out;
|
|
constexpr std::array<const char*, 3> kIfKeys{"ETH_DEF", "WIFI_STA_DEF", "WIFI_AP_DEF"};
|
|
for (const char* key : kIfKeys) {
|
|
esp_netif_t* netif = esp_netif_get_handle_from_ifkey(key);
|
|
if (netif == nullptr || !esp_netif_is_netif_up(netif)) {
|
|
continue;
|
|
}
|
|
esp_netif_ip_info_t ip_info{};
|
|
if (esp_netif_get_ip_info(netif, &ip_info) != ESP_OK || ip_info.ip.addr == 0) {
|
|
continue;
|
|
}
|
|
out.push_back(KnxNetifInfo{key, netif, ip_info.ip.addr, ip_info.netmask.addr,
|
|
ip_info.gw.addr});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::optional<KnxNetifInfo> SelectKnxNetifForRemote(const sockaddr_in& remote) {
|
|
const auto netifs = ActiveKnxNetifs();
|
|
if (netifs.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
const uint32_t remote_address = remote.sin_addr.s_addr;
|
|
for (const auto& netif : netifs) {
|
|
if ((remote_address & netif.netmask) == (netif.address & netif.netmask)) {
|
|
return netif;
|
|
}
|
|
}
|
|
return netifs.front();
|
|
}
|
|
|
|
sockaddr_in EndpointFromOpenKnxHpai(const IpHostProtocolAddressInformation& hpai,
|
|
const sockaddr_in& fallback) {
|
|
sockaddr_in out = fallback;
|
|
if (hpai.length() != LEN_IPHPAI ||
|
|
(hpai.code() != IPV4_UDP && hpai.code() != IPV4_TCP)) {
|
|
return out;
|
|
}
|
|
const uint32_t address = hpai.ipAddress();
|
|
const uint16_t port = hpai.ipPortNumber();
|
|
if (address != 0) {
|
|
out.sin_addr.s_addr = htonl(address);
|
|
}
|
|
if (port != 0) {
|
|
out.sin_port = htons(port);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
bool OpenKnxHpaiUsesUnsupportedProtocol(const IpHostProtocolAddressInformation& hpai,
|
|
bool allow_tcp) {
|
|
if (hpai.length() != LEN_IPHPAI) {
|
|
return false;
|
|
}
|
|
const HostProtocolCode protocol = hpai.code();
|
|
return protocol != IPV4_UDP && !(allow_tcp && protocol == IPV4_TCP);
|
|
}
|
|
|
|
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::kBrightnessRelative:
|
|
return "Dimmer Relative";
|
|
case GatewayKnxDaliDataType::kColorTemperature:
|
|
return "Color Temperature";
|
|
case GatewayKnxDaliDataType::kRgb:
|
|
return "RGB";
|
|
case GatewayKnxDaliDataType::kScene:
|
|
return "Scene";
|
|
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::kBrightnessRelative:
|
|
return "DPST-3-7";
|
|
case GatewayKnxDaliDataType::kColorTemperature:
|
|
return "DPST-7-600";
|
|
case GatewayKnxDaliDataType::kRgb:
|
|
return "DPST-232-600";
|
|
case GatewayKnxDaliDataType::kScene:
|
|
return "DPST-17-1";
|
|
case GatewayKnxDaliDataType::kUnknown:
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
std::optional<DecodedGroupWrite> DecodeOpenKnxGroupWrite(const uint8_t* data, size_t len) {
|
|
if (data == nullptr || len < 10) {
|
|
return std::nullopt;
|
|
}
|
|
std::vector<uint8_t> frame_data(data, data + len);
|
|
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
|
|
if (!frame.valid()) {
|
|
return std::nullopt;
|
|
}
|
|
const MessageCode message_code = frame.messageCode();
|
|
if (message_code != L_data_req && message_code != L_data_ind && message_code != L_data_con) {
|
|
return std::nullopt;
|
|
}
|
|
if (frame.addressType() != GroupAddress) {
|
|
return std::nullopt;
|
|
}
|
|
const TpduType tpdu_type = frame.tpdu().type();
|
|
if (tpdu_type != DataGroup && tpdu_type != DataBroadcast) {
|
|
return std::nullopt;
|
|
}
|
|
if (frame.apdu().type() != GroupValueWrite) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
DecodedGroupWrite out;
|
|
out.group_address = frame.destinationAddress();
|
|
const uint8_t apdu_length = frame.apdu().length();
|
|
const uint8_t* apdu_data = frame.apdu().data();
|
|
if (apdu_data == nullptr || apdu_length == 0) {
|
|
return std::nullopt;
|
|
}
|
|
if (apdu_length == 1U) {
|
|
out.data.push_back(apdu_data[0] & 0x3f);
|
|
} else {
|
|
out.data.assign(apdu_data + 1, apdu_data + apdu_length);
|
|
}
|
|
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> ExecuteRawQuery(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd,
|
|
const char* sequence) {
|
|
const auto result = ExecuteRaw(engine, BridgeOperation::query, addr, cmd, sequence);
|
|
if (!result.ok || !result.data.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
return result.data.value();
|
|
}
|
|
|
|
std::optional<uint8_t> RawCommandAddressForTarget(const GatewayKnxDaliTarget& target) {
|
|
switch (target.kind) {
|
|
case GatewayKnxDaliTargetKind::kBroadcast:
|
|
return static_cast<uint8_t>(0xff);
|
|
case GatewayKnxDaliTargetKind::kShortAddress:
|
|
if (target.address < 0 || target.address > 63) {
|
|
return std::nullopt;
|
|
}
|
|
return DaliComm::toCmdAddr(target.address);
|
|
case GatewayKnxDaliTargetKind::kGroup:
|
|
if (target.address < 0 || target.address > 15) {
|
|
return std::nullopt;
|
|
}
|
|
return static_cast<uint8_t>(0x80 + (target.address * 2) + 1);
|
|
case GatewayKnxDaliTargetKind::kNone:
|
|
default:
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
DaliBridgeResult SendRawForTarget(DaliBridgeEngine& engine, uint16_t group_address,
|
|
const GatewayKnxDaliTarget& target, uint8_t cmd) {
|
|
const auto raw_addr = RawCommandAddressForTarget(target);
|
|
if (!raw_addr.has_value()) {
|
|
DaliBridgeResult result;
|
|
result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
|
|
result.error = "invalid DALI target for raw command";
|
|
return result;
|
|
}
|
|
DaliBridgeRequest request;
|
|
request.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
|
|
request.operation = BridgeOperation::send;
|
|
request.rawAddress = raw_addr.value();
|
|
request.rawCommand = cmd;
|
|
request.metadata["sourceProtocol"] = "knx";
|
|
request.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address);
|
|
request.metadata["daliTarget"] = TargetName(target);
|
|
return engine.execute(request);
|
|
}
|
|
|
|
DaliBridgeResult SendRawExtForTarget(DaliBridgeEngine& engine, uint16_t group_address,
|
|
const GatewayKnxDaliTarget& target, uint8_t cmd) {
|
|
const auto raw_addr = RawCommandAddressForTarget(target);
|
|
if (!raw_addr.has_value()) {
|
|
DaliBridgeResult result;
|
|
result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
|
|
result.error = "invalid DALI target for raw command";
|
|
return result;
|
|
}
|
|
DaliBridgeRequest request;
|
|
request.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
|
|
request.operation = BridgeOperation::sendExt;
|
|
request.rawAddress = raw_addr.value();
|
|
request.rawCommand = cmd;
|
|
request.metadata["sourceProtocol"] = "knx";
|
|
request.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address);
|
|
request.metadata["daliTarget"] = TargetName(target);
|
|
return engine.execute(request);
|
|
}
|
|
|
|
std::optional<int> MetadataInt(const DaliBridgeResult& result, const std::string& key) {
|
|
return getObjectInt(result.metadata, key);
|
|
}
|
|
|
|
std::string HexBytes(const uint8_t* data, size_t len) {
|
|
if (data == nullptr || len == 0) {
|
|
return {};
|
|
}
|
|
std::string out;
|
|
out.reserve(len * 3);
|
|
char buffer[4] = {0};
|
|
for (size_t index = 0; index < len; ++index) {
|
|
std::snprintf(buffer, sizeof(buffer), "%02X", data[index]);
|
|
out += buffer;
|
|
if (index + 1 < len) {
|
|
out.push_back(' ');
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::optional<MessageCode> CemiMessageCode(const uint8_t* data, size_t len) {
|
|
if (data == nullptr || len == 0) {
|
|
return std::nullopt;
|
|
}
|
|
return static_cast<MessageCode>(data[0]);
|
|
}
|
|
|
|
uint16_t KnxIpServiceForCemi(const uint8_t* data, size_t len, uint16_t fallback_service) {
|
|
const auto message_code = CemiMessageCode(data, len);
|
|
if (!message_code.has_value()) {
|
|
return fallback_service;
|
|
}
|
|
switch (message_code.value()) {
|
|
case L_data_req:
|
|
case L_data_con:
|
|
case L_data_ind:
|
|
return kServiceTunnellingRequest;
|
|
default:
|
|
return kServiceDeviceConfigurationRequest;
|
|
}
|
|
}
|
|
|
|
bool MatchesOpenKnxLocalIndividualAddress(const CemiFrame& frame,
|
|
const openknx::EtsDeviceRuntime& ets_device) {
|
|
if (frame.addressType() != IndividualAddress) {
|
|
return false;
|
|
}
|
|
const uint16_t dest = frame.destinationAddress();
|
|
const uint16_t own_address = ets_device.individualAddress();
|
|
const uint16_t client_address = ets_device.tunnelClientAddress();
|
|
const bool commissioning = !ets_device.configured() || ets_device.programmingMode();
|
|
return dest == own_address || dest == client_address || (commissioning && dest == 0xffff);
|
|
}
|
|
|
|
bool BuildLocalRoutingTunnelFrame(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* local_frame) {
|
|
if (data == nullptr || local_frame == nullptr || len < 2) {
|
|
return false;
|
|
}
|
|
std::vector<uint8_t> frame_data(data, data + len);
|
|
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
|
|
if (!frame.valid()) {
|
|
return false;
|
|
}
|
|
switch (frame.messageCode()) {
|
|
case L_data_req:
|
|
*local_frame = std::move(frame_data);
|
|
return true;
|
|
case L_data_ind:
|
|
frame.messageCode(L_data_req);
|
|
*local_frame = std::move(frame_data);
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool IsLocalRoutingEchoIndication(const uint8_t* response, size_t response_len,
|
|
const uint8_t* original_request,
|
|
size_t original_request_len) {
|
|
if (response == nullptr || original_request == nullptr || response_len < 2 ||
|
|
original_request_len < 2) {
|
|
return false;
|
|
}
|
|
std::vector<uint8_t> response_data(response, response + response_len);
|
|
std::vector<uint8_t> original_data(original_request,
|
|
original_request + original_request_len);
|
|
CemiFrame response_frame(response_data.data(), static_cast<uint16_t>(response_data.size()));
|
|
CemiFrame original_frame(original_data.data(), static_cast<uint16_t>(original_data.size()));
|
|
if (!response_frame.valid() || !original_frame.valid() ||
|
|
response_frame.messageCode() != L_data_ind ||
|
|
original_frame.addressType() != IndividualAddress ||
|
|
response_frame.addressType() != original_frame.addressType() ||
|
|
response_frame.sourceAddress() != original_frame.sourceAddress() ||
|
|
response_frame.destinationAddress() != original_frame.destinationAddress() ||
|
|
response_frame.tpdu().type() != original_frame.tpdu().type() ||
|
|
response_frame.apdu().type() != original_frame.apdu().type()) {
|
|
return false;
|
|
}
|
|
const uint8_t response_apdu_length = response_frame.apdu().length();
|
|
const uint8_t original_apdu_length = original_frame.apdu().length();
|
|
if (response_apdu_length != original_apdu_length) {
|
|
return false;
|
|
}
|
|
const uint8_t* response_apdu = response_frame.apdu().data();
|
|
const uint8_t* original_apdu = original_frame.apdu().data();
|
|
if (response_apdu_length == 0) {
|
|
return true;
|
|
}
|
|
if (response_apdu == nullptr || original_apdu == nullptr) {
|
|
return false;
|
|
}
|
|
return std::memcmp(response_apdu, original_apdu, response_apdu_length) == 0;
|
|
}
|
|
|
|
bool BuildTunnelConfirmationFrame(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* confirmation) {
|
|
if (data == nullptr || confirmation == nullptr || len < 2) {
|
|
return false;
|
|
}
|
|
std::vector<uint8_t> frame_data(data, data + len);
|
|
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
|
|
if (!frame.valid() || frame.messageCode() != L_data_req) {
|
|
return false;
|
|
}
|
|
frame.messageCode(L_data_con);
|
|
frame.confirm(ConfirmNoError);
|
|
*confirmation = std::move(frame_data);
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
DaliBridgeResult IgnoredResult(uint16_t group_address, uint16_t group_object_number,
|
|
const char* reason) {
|
|
DaliBridgeResult result;
|
|
result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
|
|
result.ok = true;
|
|
result.metadata["ignored"] = true;
|
|
result.metadata["groupObjectNumber"] = static_cast<int>(group_object_number);
|
|
result.metadata["reason"] = reason == nullptr ? "ignored" : reason;
|
|
return result;
|
|
}
|
|
|
|
bool SetSearchAddress(DaliBridgeEngine& engine, uint32_t search_address, const char* sequence) {
|
|
return SendRaw(engine, DALI_CMD_SPECIAL_SEARCHADDRH,
|
|
static_cast<uint8_t>((search_address >> 16) & 0xff), sequence) &&
|
|
SendRaw(engine, DALI_CMD_SPECIAL_SEARCHADDRM,
|
|
static_cast<uint8_t>((search_address >> 8) & 0xff), sequence) &&
|
|
SendRaw(engine, DALI_CMD_SPECIAL_SEARCHADDRL,
|
|
static_cast<uint8_t>(search_address & 0xff), sequence);
|
|
}
|
|
|
|
std::optional<bool> CompareSelectedSearchAddress(DaliBridgeEngine& engine, uint32_t search_address,
|
|
const char* sequence) {
|
|
if (!SetSearchAddress(engine, search_address, sequence)) {
|
|
return std::nullopt;
|
|
}
|
|
const auto raw = ExecuteRawQuery(engine, DALI_CMD_SPECIAL_COMPARE, DALI_CMD_OFF, sequence);
|
|
if (!raw.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
return raw.value() == 0xff;
|
|
}
|
|
|
|
std::optional<uint32_t> FindLowestSelectedRandomAddress(DaliBridgeEngine& engine) {
|
|
const auto any = CompareSelectedSearchAddress(engine, 0x00ffffffu,
|
|
"knx-function-scan-compare-any");
|
|
if (!any.has_value() || !any.value()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
uint32_t low = 0;
|
|
uint32_t high = 0x00ffffffu;
|
|
while (low < high) {
|
|
const uint32_t mid = low + ((high - low) / 2);
|
|
const auto match = CompareSelectedSearchAddress(engine, mid,
|
|
"knx-function-scan-compare-binary");
|
|
if (!match.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
if (match.value()) {
|
|
high = mid;
|
|
} else {
|
|
low = mid + 1;
|
|
}
|
|
}
|
|
if (!SetSearchAddress(engine, low, "knx-function-scan-compare-final")) {
|
|
return std::nullopt;
|
|
}
|
|
return low;
|
|
}
|
|
|
|
std::optional<uint8_t> QuerySelectedShortAddress(DaliBridgeEngine& engine) {
|
|
const auto raw = ExecuteRawQuery(engine, DALI_CMD_SPECIAL_QUERY_SHORT_ADDRESS, DALI_CMD_OFF,
|
|
"knx-function-scan-query-short");
|
|
if (!raw.has_value() || raw.value() < 0 || raw.value() > 0xff || raw.value() == 0xff) {
|
|
return std::nullopt;
|
|
}
|
|
return static_cast<uint8_t>((raw.value() >> 1) & 0x3f);
|
|
}
|
|
|
|
bool VerifyShortAddress(DaliBridgeEngine& engine, uint8_t short_address) {
|
|
const auto raw = ExecuteRawQuery(engine, DALI_CMD_SPECIAL_VERIFY_SHORT_ADDRESS,
|
|
DaliComm::toCmdAddr(short_address),
|
|
"knx-function-scan-verify-short");
|
|
return raw.has_value() && raw.value() == 0xff;
|
|
}
|
|
|
|
std::array<bool, 64> QueryUsedShortAddresses(DaliBridgeEngine& engine) {
|
|
std::array<bool, 64> used{};
|
|
for (int short_address = 0; short_address < static_cast<int>(used.size()); ++short_address) {
|
|
used[short_address] = QueryShort(engine, static_cast<uint8_t>(short_address),
|
|
DALI_CMD_QUERY_STATUS,
|
|
"knx-function-scan-query-used")
|
|
.has_value();
|
|
}
|
|
return used;
|
|
}
|
|
|
|
std::optional<uint8_t> NextFreeShortAddress(const std::array<bool, 64>& used) {
|
|
for (size_t index = 0; index < used.size(); ++index) {
|
|
if (!used[index]) {
|
|
return static_cast<uint8_t>(index);
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
uint8_t Reg1SceneTypeForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) {
|
|
const uint32_t addr = kReg1SceneParamBlockOffset +
|
|
(kReg1SceneParamBlockSize * static_cast<uint32_t>(index));
|
|
return static_cast<uint8_t>((runtime.paramByte(addr) >> 6) & 0x03);
|
|
}
|
|
|
|
bool Reg1SceneSaveAllowedForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) {
|
|
const uint32_t addr = kReg1SceneParamBlockOffset +
|
|
(kReg1SceneParamBlockSize * static_cast<uint32_t>(index));
|
|
return runtime.paramBit(addr, 2);
|
|
}
|
|
|
|
uint8_t Reg1KnxSceneNumberForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) {
|
|
const uint32_t addr = kReg1SceneParamBlockOffset +
|
|
(kReg1SceneParamBlockSize * static_cast<uint32_t>(index)) + 1;
|
|
return static_cast<uint8_t>((runtime.paramByte(addr) >> 1) & 0x7f);
|
|
}
|
|
|
|
uint8_t Reg1DaliSceneNumberForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) {
|
|
const uint32_t addr = kReg1SceneParamBlockOffset +
|
|
(kReg1SceneParamBlockSize * static_cast<uint32_t>(index));
|
|
return static_cast<uint8_t>((runtime.paramByte(addr) >> 1) & 0x0f);
|
|
}
|
|
|
|
std::optional<GatewayKnxDaliTarget> Reg1SceneTargetForEntry(
|
|
const openknx::EtsDeviceRuntime& runtime, size_t index) {
|
|
const uint32_t base = kReg1SceneParamBlockOffset +
|
|
(kReg1SceneParamBlockSize * static_cast<uint32_t>(index));
|
|
switch (Reg1SceneTypeForEntry(runtime, index)) {
|
|
case kReg1SceneTypeAddress:
|
|
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress,
|
|
static_cast<int>((runtime.paramByte(base + 2) >> 2) & 0x3f)};
|
|
case kReg1SceneTypeGroup:
|
|
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup,
|
|
static_cast<int>((runtime.paramByte(base + 3) >> 4) & 0x0f)};
|
|
case kReg1SceneTypeBroadcast:
|
|
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127};
|
|
case kReg1SceneTypeNone:
|
|
default:
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
bool SendStream(int sock, const uint8_t* data, size_t len) {
|
|
size_t sent = 0;
|
|
while (sent < len) {
|
|
const int written = send(sock, data + sent, len - sent, 0);
|
|
if (written <= 0) {
|
|
return false;
|
|
}
|
|
sent += static_cast<size_t>(written);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::vector<uint8_t> OpenKnxIpPacket(uint16_t service, const std::vector<uint8_t>& body) {
|
|
KnxIpFrame frame(static_cast<uint16_t>(LEN_KNXIP_HEADER + body.size()));
|
|
frame.serviceTypeIdentifier(service);
|
|
std::copy(body.begin(), body.end(), frame.data() + LEN_KNXIP_HEADER);
|
|
return std::vector<uint8_t>(frame.data(), frame.data() + frame.totalLength());
|
|
}
|
|
|
|
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 IsKnxNetIpSecureService(uint16_t service) {
|
|
switch (service) {
|
|
case kServiceSecureWrapper:
|
|
case kServiceSecureSessionRequest:
|
|
case kServiceSecureSessionResponse:
|
|
case kServiceSecureSessionAuth:
|
|
case kServiceSecureSessionStatus:
|
|
case kServiceSecureGroupSync:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
} // 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.dali_bus_id = static_cast<uint8_t>(std::clamp(
|
|
ObjectIntAny(object, {"daliBusId", "dali_bus_id", "targetDaliBusId",
|
|
"target_dali_bus_id"})
|
|
.value_or(config.dali_bus_id),
|
|
0, 15));
|
|
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.ip_interface_individual_address = static_cast<uint16_t>(std::clamp(
|
|
ObjectIntAny(object, {"ipInterfaceIndividualAddress",
|
|
"ip_interface_individual_address",
|
|
"ipInterfaceAddress",
|
|
"ip_interface_address"})
|
|
.value_or(config.ip_interface_individual_address),
|
|
0, 0xffff));
|
|
config.individual_address = static_cast<uint16_t>(std::clamp(
|
|
ObjectIntAny(object, {"individualAddress",
|
|
"individual_address",
|
|
"knxDaliGatewayIndividualAddress",
|
|
"knx_dali_gateway_individual_address",
|
|
"deviceIndividualAddress",
|
|
"device_individual_address"})
|
|
.value_or(config.individual_address),
|
|
0, 0xffff));
|
|
config.programming_button_gpio = std::clamp(
|
|
ObjectIntAny(object, {"programmingButtonGpio", "programming_button_gpio"})
|
|
.value_or(config.programming_button_gpio),
|
|
-1, 48);
|
|
config.programming_button_active_low =
|
|
ObjectBoolAny(object, {"programmingButtonActiveLow", "programming_button_active_low"})
|
|
.value_or(config.programming_button_active_low);
|
|
config.programming_led_gpio = std::clamp(
|
|
ObjectIntAny(object, {"programmingLedGpio", "programming_led_gpio"})
|
|
.value_or(config.programming_led_gpio),
|
|
-1, 48);
|
|
config.programming_led_active_high =
|
|
ObjectBoolAny(object, {"programmingLedActiveHigh", "programming_led_active_high"})
|
|
.value_or(config.programming_led_active_high);
|
|
|
|
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), -1,
|
|
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.startup_timeout_ms = static_cast<uint32_t>(std::max(
|
|
0, ObjectIntAny(serial, {"startupTimeoutMs", "startup_timeout_ms"})
|
|
.value_or(static_cast<int>(config.tp_uart.startup_timeout_ms))));
|
|
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))));
|
|
config.tp_uart.nine_bit_mode = ObjectBoolAny(
|
|
serial, {"nineBitMode", "nine_bit_mode", "use9BitMode", "use_9_bit_mode"})
|
|
.value_or(config.tp_uart.nine_bit_mode);
|
|
}
|
|
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["daliBusId"] = static_cast<int>(config.dali_bus_id);
|
|
out["udpPort"] = static_cast<int>(config.udp_port);
|
|
out["multicastAddress"] = config.multicast_address;
|
|
out["ipInterfaceIndividualAddress"] =
|
|
static_cast<int>(config.ip_interface_individual_address);
|
|
out["individualAddress"] = static_cast<int>(config.individual_address);
|
|
out["programmingButtonGpio"] = config.programming_button_gpio;
|
|
out["programmingButtonActiveLow"] = config.programming_button_active_low;
|
|
out["programmingLedGpio"] = config.programming_led_gpio;
|
|
out["programmingLedActiveHigh"] = config.programming_led_active_high;
|
|
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["startupTimeoutMs"] = static_cast<int>(config.tp_uart.startup_timeout_ms);
|
|
serial["readTimeoutMs"] = static_cast<int>(config.tp_uart.read_timeout_ms);
|
|
serial["nineBitMode"] = config.tp_uart.nine_bit_mode;
|
|
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));
|
|
}
|
|
|
|
bool GatewayKnxConfigUsesTpUart(const GatewayKnxConfig& config) {
|
|
return config.ip_router_enabled && config.tp_uart.uart_port >= 0;
|
|
}
|
|
|
|
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::kBrightnessRelative:
|
|
return "brightness_relative";
|
|
case GatewayKnxDaliDataType::kColorTemperature:
|
|
return "color_temperature";
|
|
case GatewayKnxDaliDataType::kRgb:
|
|
return "rgb";
|
|
case GatewayKnxDaliDataType::kScene:
|
|
return "scene";
|
|
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});
|
|
}
|
|
if (object_number == kGwReg1AppKoScene) {
|
|
return MakeGwReg1Binding(main_group, object_number, -1, "scene",
|
|
GatewayKnxDaliDataType::kScene,
|
|
GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kNone, -1});
|
|
}
|
|
|
|
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 == kGwReg1KoDimmRelative) {
|
|
return MakeGwReg1Binding(main_group, object_number, channel, "dimm_relative",
|
|
GatewayKnxDaliDataType::kBrightnessRelative, 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 == kGwReg1KoDimmRelative) {
|
|
return MakeGwReg1Binding(main_group, object_number, group, "dimm_relative",
|
|
GatewayKnxDaliDataType::kBrightnessRelative, 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();
|
|
}
|
|
|
|
void GatewayKnxBridge::setRuntimeContext(const openknx::EtsDeviceRuntime* runtime) {
|
|
runtime_ = runtime;
|
|
}
|
|
|
|
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 * 4) + (16 * 4));
|
|
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());
|
|
}
|
|
}
|
|
if (const auto binding = GwReg1BindingForObject(config_.main_group, kGwReg1AppKoScene)) {
|
|
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, kGwReg1KoDimmRelative,
|
|
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, kGwReg1KoDimmRelative,
|
|
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;
|
|
}
|
|
|
|
bool GatewayKnxBridge::matchesGroupAddress(uint16_t group_address) const {
|
|
if (!config_.dali_router_enabled) {
|
|
return false;
|
|
}
|
|
if (config_.ets_database_enabled &&
|
|
ets_bindings_by_group_address_.find(group_address) != ets_bindings_by_group_address_.end()) {
|
|
return true;
|
|
}
|
|
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 false;
|
|
}
|
|
if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) {
|
|
const uint16_t object_number = static_cast<uint16_t>((middle << 8) | sub);
|
|
return GwReg1BindingForObject(config_.main_group, object_number).has_value();
|
|
}
|
|
if (config_.mapping_mode == GatewayKnxMappingMode::kManual) {
|
|
return false;
|
|
}
|
|
return GatewayKnxDaliDataTypeForMiddleGroup(middle).has_value() &&
|
|
GatewayKnxDaliTargetForSubgroup(sub).has_value();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
DaliBridgeResult GatewayKnxBridge::handleGroupObjectWrite(uint16_t group_object_number,
|
|
const uint8_t* data, size_t len) {
|
|
const uint16_t group_address = GwReg1GroupAddressForObject(config_.main_group,
|
|
group_object_number);
|
|
const std::string payload = HexBytes(data, len);
|
|
ESP_LOGI(kTag, "OpenKNX KO write ko=%u derivedGa=%s len=%u payload=%s",
|
|
static_cast<unsigned>(group_object_number),
|
|
GatewayKnxGroupAddressString(group_address).c_str(), static_cast<unsigned>(len),
|
|
payload.c_str());
|
|
if (!config_.dali_router_enabled) {
|
|
return ErrorResult(group_address, "KNX to DALI router disabled");
|
|
}
|
|
const auto binding = GwReg1BindingForObject(config_.main_group, group_object_number);
|
|
if (!binding.has_value()) {
|
|
ESP_LOGW(kTag, "OpenKNX KO write ignored ko=%u: unsupported GW-REG1 object",
|
|
static_cast<unsigned>(group_object_number));
|
|
return IgnoredResult(group_address, group_object_number,
|
|
"unsupported GW-REG1 group object");
|
|
}
|
|
DaliBridgeResult result = executeForDecodedWrite(binding->group_address, binding->data_type,
|
|
binding->target, data, len);
|
|
result.metadata["source"] = "openknx_group_object";
|
|
result.metadata["groupObjectNumber"] = static_cast<int>(group_object_number);
|
|
result.metadata["objectRole"] = binding->object_role;
|
|
if (result.ok) {
|
|
ESP_LOGI(kTag, "OpenKNX KO write routed ko=%u role=%s target=%s",
|
|
static_cast<unsigned>(group_object_number), binding->object_role.c_str(),
|
|
TargetName(binding->target).c_str());
|
|
} else {
|
|
ESP_LOGW(kTag, "OpenKNX KO write failed ko=%u role=%s error=%s",
|
|
static_cast<unsigned>(group_object_number), binding->object_role.c_str(),
|
|
result.error.c_str());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
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 only_new = data[1] == 1;
|
|
const bool randomize = data[2] == 1;
|
|
const bool delete_all = data[3] == 1;
|
|
const bool assign = data[4] == 1;
|
|
ESP_LOGI(kTag, "REG1-Dali scan start onlyNew=%d randomize=%d deleteAll=%d assign=%d",
|
|
only_new, randomize, delete_all, assign);
|
|
|
|
std::array<bool, 64> used_addresses{};
|
|
if (assign && !delete_all) {
|
|
used_addresses = QueryUsedShortAddresses(engine_);
|
|
}
|
|
|
|
const bool initialized = SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF,
|
|
"knx-function-scan-terminate-prev") &&
|
|
SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE,
|
|
only_new ? DALI_CMD_STOP_FADE : DALI_CMD_OFF,
|
|
"knx-function-scan-init") &&
|
|
SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE,
|
|
only_new ? DALI_CMD_STOP_FADE : DALI_CMD_OFF,
|
|
"knx-function-scan-init-repeat");
|
|
if (!initialized) {
|
|
ESP_LOGW(kTag, "REG1-Dali scan failed during initialize");
|
|
commissioning_scan_done_ = true;
|
|
response->clear();
|
|
return true;
|
|
}
|
|
|
|
if (delete_all) {
|
|
const bool removed = SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, 0xff,
|
|
"knx-function-scan-clear-short-dtr") &&
|
|
SendRawExt(engine_, 0xff, DALI_CMD_STORE_DTR_AS_SHORT_ADDRESS,
|
|
"knx-function-scan-clear-short");
|
|
if (!removed) {
|
|
ESP_LOGW(kTag, "REG1-Dali scan failed while clearing short addresses");
|
|
commissioning_scan_done_ = true;
|
|
response->clear();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (randomize) {
|
|
const bool randomized = SendRawExt(engine_, DALI_CMD_SPECIAL_RANDOMIZE, DALI_CMD_OFF,
|
|
"knx-function-scan-randomize") &&
|
|
SendRawExt(engine_, DALI_CMD_SPECIAL_RANDOMIZE, DALI_CMD_OFF,
|
|
"knx-function-scan-randomize-repeat");
|
|
if (!randomized) {
|
|
ESP_LOGW(kTag, "REG1-Dali scan failed while randomizing addresses");
|
|
commissioning_scan_done_ = true;
|
|
response->clear();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
while (true) {
|
|
const auto random_address = FindLowestSelectedRandomAddress(engine_);
|
|
if (!random_address.has_value()) {
|
|
break;
|
|
}
|
|
|
|
GatewayKnxCommissioningBallast ballast;
|
|
ballast.high = static_cast<uint8_t>((random_address.value() >> 16) & 0xff);
|
|
ballast.middle = static_cast<uint8_t>((random_address.value() >> 8) & 0xff);
|
|
ballast.low = static_cast<uint8_t>(random_address.value() & 0xff);
|
|
ballast.short_address = 0xff;
|
|
|
|
if (assign) {
|
|
const auto next_address = NextFreeShortAddress(used_addresses);
|
|
if (!next_address.has_value()) {
|
|
ESP_LOGW(kTag, "REG1-Dali scan has no free short address left for 0x%06x",
|
|
static_cast<unsigned>(random_address.value()));
|
|
break;
|
|
}
|
|
if (!SendRaw(engine_, DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS,
|
|
DaliComm::toCmdAddr(next_address.value()),
|
|
"knx-function-scan-program-short") ||
|
|
!VerifyShortAddress(engine_, next_address.value())) {
|
|
ESP_LOGW(kTag, "REG1-Dali scan failed to program short address %u",
|
|
static_cast<unsigned>(next_address.value()));
|
|
break;
|
|
}
|
|
used_addresses[next_address.value()] = true;
|
|
ballast.short_address = next_address.value();
|
|
} else {
|
|
ballast.short_address = QuerySelectedShortAddress(engine_).value_or(0xff);
|
|
}
|
|
|
|
commissioning_found_ballasts_.push_back(ballast);
|
|
ESP_LOGI(kTag, "REG1-Dali scan found random=0x%02X%02X%02X short=%u",
|
|
ballast.high, ballast.middle, ballast.low,
|
|
static_cast<unsigned>(ballast.short_address));
|
|
|
|
if (!SendRaw(engine_, DALI_CMD_SPECIAL_WITHDRAW, DALI_CMD_OFF,
|
|
"knx-function-scan-withdraw")) {
|
|
ESP_LOGW(kTag, "REG1-Dali scan failed while withdrawing matched device");
|
|
break;
|
|
}
|
|
}
|
|
|
|
SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF,
|
|
"knx-function-scan-terminate");
|
|
commissioning_scan_done_ = true;
|
|
ESP_LOGI(kTag, "REG1-Dali scan completed count=%u",
|
|
static_cast<unsigned>(commissioning_found_ballasts_.size()));
|
|
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;
|
|
}
|
|
|
|
DaliBridgeResult GatewayKnxBridge::executeReg1SceneWrite(uint16_t group_address,
|
|
const uint8_t* data, size_t len) {
|
|
if (runtime_ == nullptr || !runtime_->configured()) {
|
|
return ErrorResult(group_address, "REG1 scene parameters are unavailable");
|
|
}
|
|
if (data == nullptr || len < 1) {
|
|
return ErrorResult(group_address, "missing KNX scene payload");
|
|
}
|
|
|
|
const uint8_t knx_scene = data[0] & kReg1SceneTelegramNumberMask;
|
|
const bool store_scene = (data[0] & kReg1SceneTelegramStoreMask) != 0;
|
|
|
|
DaliBridgeResult result;
|
|
result.ok = true;
|
|
result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
|
|
result.metadata["sourceProtocol"] = "knx";
|
|
result.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address);
|
|
result.metadata["sceneNumber"] = static_cast<int>(knx_scene);
|
|
result.metadata["sceneAction"] = std::string(store_scene ? "store" : "recall");
|
|
|
|
size_t matched_entries = 0;
|
|
for (size_t index = 0; index < kReg1SceneEntryCount; ++index) {
|
|
if (Reg1SceneTypeForEntry(*runtime_, index) == kReg1SceneTypeNone) {
|
|
continue;
|
|
}
|
|
const uint8_t configured_knx_scene = Reg1KnxSceneNumberForEntry(*runtime_, index);
|
|
if (configured_knx_scene == 0 || knx_scene != static_cast<uint8_t>(configured_knx_scene - 1)) {
|
|
continue;
|
|
}
|
|
if (store_scene && !Reg1SceneSaveAllowedForEntry(*runtime_, index)) {
|
|
continue;
|
|
}
|
|
const auto target = Reg1SceneTargetForEntry(*runtime_, index);
|
|
if (!target.has_value()) {
|
|
continue;
|
|
}
|
|
|
|
++matched_entries;
|
|
const uint8_t dali_scene = Reg1DaliSceneNumberForEntry(*runtime_, index);
|
|
if (store_scene) {
|
|
DaliBridgeResult copy_result =
|
|
SendRawExtForTarget(engine_, group_address, target.value(),
|
|
DALI_CMD_STORE_ACTUAL_LEVEL_IN_THE_DTR);
|
|
copy_result.metadata["sceneTableIndex"] = static_cast<int>(index);
|
|
copy_result.metadata["sceneNumber"] = static_cast<int>(dali_scene);
|
|
result.results.emplace_back(copy_result.toJson());
|
|
result.ok = result.ok && copy_result.ok;
|
|
|
|
DaliBridgeResult store_result =
|
|
SendRawExtForTarget(engine_, group_address, target.value(), DALI_CMD_SET_SCENE(dali_scene));
|
|
store_result.metadata["sceneTableIndex"] = static_cast<int>(index);
|
|
store_result.metadata["sceneNumber"] = static_cast<int>(dali_scene);
|
|
result.results.emplace_back(store_result.toJson());
|
|
result.ok = result.ok && store_result.ok;
|
|
} else {
|
|
DaliBridgeResult recall_result =
|
|
SendRawForTarget(engine_, group_address, target.value(), DALI_CMD_GO_TO_SCENE(dali_scene));
|
|
recall_result.metadata["sceneTableIndex"] = static_cast<int>(index);
|
|
recall_result.metadata["sceneNumber"] = static_cast<int>(dali_scene);
|
|
result.results.emplace_back(recall_result.toJson());
|
|
result.ok = result.ok && recall_result.ok;
|
|
}
|
|
}
|
|
|
|
if (matched_entries == 0) {
|
|
result.ok = false;
|
|
result.error = "no configured REG1 scene mapping matched KNX scene";
|
|
return result;
|
|
}
|
|
|
|
result.data = static_cast<int>(matched_entries);
|
|
result.metadata["matchedSceneEntries"] = static_cast<int>(matched_entries);
|
|
if (!result.ok) {
|
|
result.error = "one or more REG1 scene operations 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 &&
|
|
data_type != GatewayKnxDaliDataType::kScene) {
|
|
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::kBrightnessRelative: {
|
|
if (data == nullptr || len < 1) {
|
|
return ErrorResult(group_address, "missing DPT3 relative dimming payload");
|
|
}
|
|
const uint8_t payload = data[0];
|
|
const uint8_t step_code = payload & 0x07;
|
|
const bool dim_up = (payload & 0x10) != 0;
|
|
const uint8_t cmd = step_code == 0
|
|
? kDaliCmdStopFade
|
|
: (dim_up ? kDaliCmdOnStepUp : kDaliCmdStepDownOff);
|
|
DaliBridgeResult result = SendRawForTarget(engine_, group_address, target, cmd);
|
|
result.metadata["knxRelativeStepCode"] = static_cast<int>(step_code);
|
|
result.metadata["knxRelativeDirection"] =
|
|
step_code == 0 ? std::string("stop") : std::string(dim_up ? "up" : "down");
|
|
return result;
|
|
}
|
|
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::kScene:
|
|
return executeReg1SceneWrite(group_address, data, len);
|
|
case GatewayKnxDaliDataType::kUnknown:
|
|
default:
|
|
return ErrorResult(group_address, "unsupported KNX data type");
|
|
}
|
|
}
|
|
|
|
GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge,
|
|
std::string openknx_namespace)
|
|
: bridge_(bridge),
|
|
openknx_namespace_(std::move(openknx_namespace)) {
|
|
openknx_lock_ = xSemaphoreCreateMutex();
|
|
startup_semaphore_ = xSemaphoreCreateBinary();
|
|
}
|
|
|
|
GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() {
|
|
stop();
|
|
if (startup_semaphore_ != nullptr) {
|
|
vSemaphoreDelete(startup_semaphore_);
|
|
startup_semaphore_ = nullptr;
|
|
}
|
|
if (openknx_lock_ != nullptr) {
|
|
vSemaphoreDelete(openknx_lock_);
|
|
openknx_lock_ = nullptr;
|
|
}
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; }
|
|
|
|
void GatewayKnxTpIpRouter::setCommissioningOnly(bool enabled) {
|
|
commissioning_only_ = enabled;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::setGroupWriteHandler(GroupWriteHandler handler) {
|
|
group_write_handler_ = std::move(handler);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::setGroupObjectWriteHandler(GroupObjectWriteHandler handler) {
|
|
group_object_write_handler_ = std::move(handler);
|
|
}
|
|
|
|
const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; }
|
|
|
|
bool GatewayKnxTpIpRouter::tpUartOnline() const { return tp_uart_online_; }
|
|
|
|
bool GatewayKnxTpIpRouter::programmingMode() {
|
|
if (openknx_lock_ == nullptr) {
|
|
return false;
|
|
}
|
|
SemaphoreGuard guard(openknx_lock_);
|
|
return ets_device_ != nullptr && ets_device_->programmingMode();
|
|
}
|
|
|
|
esp_err_t GatewayKnxTpIpRouter::setProgrammingMode(bool enabled) {
|
|
if (openknx_lock_ == nullptr) {
|
|
last_error_ = "KNX runtime lock is unavailable";
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
SemaphoreGuard guard(openknx_lock_);
|
|
if (ets_device_ == nullptr) {
|
|
last_error_ = "KNX OpenKNX runtime is unavailable";
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
ets_device_->setProgrammingMode(enabled);
|
|
setProgrammingLed(enabled);
|
|
ESP_LOGI(kTag, "KNX programming mode %s namespace=%s",
|
|
enabled ? "enabled" : "disabled", openknx_namespace_.c_str());
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t GatewayKnxTpIpRouter::toggleProgrammingMode() {
|
|
return setProgrammingMode(!programmingMode());
|
|
}
|
|
|
|
esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) {
|
|
if (started_ || task_handle_ != nullptr) {
|
|
return ESP_OK;
|
|
}
|
|
if (openknx_lock_ == nullptr || startup_semaphore_ == nullptr) {
|
|
last_error_ = "failed to allocate KNX runtime synchronization primitives";
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
if (!config_.ip_router_enabled) {
|
|
last_error_ = "KNXnet/IP router is disabled in config";
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
stop_requested_ = false;
|
|
last_error_.clear();
|
|
int log_tp_uart_tx_pin = -1;
|
|
int log_tp_uart_rx_pin = -1;
|
|
if (config_.tp_uart.uart_port >= 0 && config_.tp_uart.uart_port < SOC_UART_NUM) {
|
|
const uart_port_t log_uart_port = static_cast<uart_port_t>(config_.tp_uart.uart_port);
|
|
ResolveUartIoPin(log_uart_port, config_.tp_uart.tx_pin, SOC_UART_TX_PIN_IDX,
|
|
&log_tp_uart_tx_pin);
|
|
ResolveUartIoPin(log_uart_port, config_.tp_uart.rx_pin, SOC_UART_RX_PIN_IDX,
|
|
&log_tp_uart_rx_pin);
|
|
}
|
|
ESP_LOGI(kTag,
|
|
"starting KNXnet/IP router namespace=%s udp=%u tunnel=%d multicast=%d group=%s "
|
|
"tpUart=%d tx=%s rx=%s nineBit=%d commissioningOnly=%d",
|
|
openknx_namespace_.c_str(), static_cast<unsigned>(config_.udp_port),
|
|
config_.tunnel_enabled, config_.multicast_enabled,
|
|
config_.multicast_address.c_str(), config_.tp_uart.uart_port,
|
|
UartPinDescription(config_.tp_uart.tx_pin, log_tp_uart_tx_pin).c_str(),
|
|
UartPinDescription(config_.tp_uart.rx_pin, log_tp_uart_rx_pin).c_str(),
|
|
config_.tp_uart.nine_bit_mode, commissioning_only_);
|
|
if (!configureSocket()) {
|
|
return ESP_FAIL;
|
|
}
|
|
while (xSemaphoreTake(startup_semaphore_, 0) == pdTRUE) {
|
|
}
|
|
startup_result_ = ESP_ERR_TIMEOUT;
|
|
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;
|
|
}
|
|
if (xSemaphoreTake(startup_semaphore_, pdMS_TO_TICKS(10000)) != pdTRUE) {
|
|
last_error_ = "timed out starting KNXnet/IP OpenKNX runtime";
|
|
stop_requested_ = true;
|
|
closeSockets();
|
|
return ESP_ERR_TIMEOUT;
|
|
}
|
|
return startup_result_;
|
|
}
|
|
|
|
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_; }
|
|
|
|
bool GatewayKnxTpIpRouter::publishDaliStatus(const GatewayKnxDaliTarget& target,
|
|
uint8_t actual_level) {
|
|
if (!started_ || !config_.ip_router_enabled || !shouldRouteDaliApplicationFrames()) {
|
|
return false;
|
|
}
|
|
uint16_t switch_object = 0;
|
|
uint16_t dimm_object = 0;
|
|
if (target.kind == GatewayKnxDaliTargetKind::kShortAddress) {
|
|
if (target.address < 0 || target.address > 63) {
|
|
return false;
|
|
}
|
|
const uint16_t base = kGwReg1AdrKoOffset +
|
|
kGwReg1AdrKoBlockSize * static_cast<uint16_t>(target.address);
|
|
switch_object = base + kGwReg1KoSwitchState;
|
|
dimm_object = base + kGwReg1KoDimmState;
|
|
} else if (target.kind == GatewayKnxDaliTargetKind::kGroup) {
|
|
if (target.address < 0 || target.address > 15) {
|
|
return false;
|
|
}
|
|
const uint16_t base = kGwReg1GrpKoOffset +
|
|
kGwReg1GrpKoBlockSize * static_cast<uint16_t>(target.address);
|
|
switch_object = base + kGwReg1KoSwitchState;
|
|
dimm_object = base + kGwReg1KoDimmState;
|
|
} else if (target.kind == GatewayKnxDaliTargetKind::kBroadcast) {
|
|
switch_object = kGwReg1AppKoBroadcastSwitch;
|
|
dimm_object = kGwReg1AppKoBroadcastDimm;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
const uint8_t switch_value = actual_level > 0 ? 1 : 0;
|
|
const uint8_t dimm_value = DaliArcLevelToDpt5(actual_level);
|
|
bool emitted = emitOpenKnxGroupValue(switch_object, &switch_value, 1);
|
|
emitted = emitOpenKnxGroupValue(dimm_object, &dimm_value, 1) || emitted;
|
|
return emitted;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::TaskEntry(void* arg) {
|
|
static_cast<GatewayKnxTpIpRouter*>(arg)->taskLoop();
|
|
}
|
|
|
|
esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
|
|
{
|
|
SemaphoreGuard guard(openknx_lock_);
|
|
auto tp_uart_interface = createOpenKnxTpUartInterface();
|
|
if (GatewayKnxConfigUsesTpUart(config_) && tp_uart_interface == nullptr && !last_error_.empty()) {
|
|
return ESP_FAIL;
|
|
}
|
|
ets_device_ = std::make_unique<openknx::EtsDeviceRuntime>(openknx_namespace_,
|
|
config_.individual_address,
|
|
effectiveTunnelAddress(),
|
|
std::move(tp_uart_interface));
|
|
bridge_.setRuntimeContext(ets_device_.get());
|
|
knx_ip_parameters_ = std::make_unique<IpParameterObject>(
|
|
ets_device_->deviceObject(), ets_device_->platform());
|
|
openknx_configured_.store(ets_device_->configured());
|
|
ESP_LOGI(kTag,
|
|
"OpenKNX runtime namespace=%s configured=%d ipInterface=0x%04x "
|
|
"device=0x%04x tunnelClient=0x%04x commissioningOnly=%d",
|
|
openknx_namespace_.c_str(), ets_device_->configured(),
|
|
effectiveIpInterfaceIndividualAddress(), ets_device_->individualAddress(),
|
|
ets_device_->tunnelClientAddress(), commissioning_only_);
|
|
ets_device_->setFunctionPropertyHandlers(
|
|
[this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (!shouldRouteDaliApplicationFrames()) {
|
|
return false;
|
|
}
|
|
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) {
|
|
if (!shouldRouteDaliApplicationFrames()) {
|
|
return false;
|
|
}
|
|
return bridge_.handleFunctionPropertyState(object_index, property_id, data, len,
|
|
response);
|
|
});
|
|
ets_device_->setGroupWriteHandler(
|
|
[this](uint16_t group_address, const uint8_t* data, size_t len) {
|
|
if (!shouldRouteDaliApplicationFrames()) {
|
|
return;
|
|
}
|
|
const DaliBridgeResult result = group_write_handler_
|
|
? group_write_handler_(group_address, data, len)
|
|
: bridge_.handleGroupWrite(group_address, data, len);
|
|
if (!result.ok && !result.error.empty()) {
|
|
ESP_LOGD(kTag, "secure KNX group write not routed to DALI: %s", result.error.c_str());
|
|
}
|
|
});
|
|
ets_device_->setGroupObjectWriteHandler(
|
|
[this](uint16_t group_object_number, const uint8_t* data, size_t len) {
|
|
if (!shouldRouteDaliApplicationFrames()) {
|
|
return IgnoredResult(
|
|
GwReg1GroupAddressForObject(config_.main_group, group_object_number),
|
|
group_object_number,
|
|
"routing blocked by commissioning-only state");
|
|
}
|
|
const DaliBridgeResult result = group_object_write_handler_
|
|
? group_object_write_handler_(group_object_number,
|
|
data, len)
|
|
: bridge_.handleGroupObjectWrite(group_object_number,
|
|
data, len);
|
|
const bool ignored = getObjectBool(result.metadata, "ignored").value_or(false);
|
|
if (ignored) {
|
|
const auto reason = getObjectString(result.metadata, "reason").value_or("ignored");
|
|
ESP_LOGW(kTag, "OpenKNX group object %u accepted by ETS but ignored: %s",
|
|
static_cast<unsigned>(group_object_number), reason.c_str());
|
|
} else if (!result.ok && !result.error.empty()) {
|
|
ESP_LOGW(kTag, "OpenKNX group object %u not routed to DALI: %s",
|
|
static_cast<unsigned>(group_object_number), result.error.c_str());
|
|
}
|
|
return result;
|
|
});
|
|
ets_device_->setBusFrameSender([this](const uint8_t* data, size_t len) {
|
|
sendTunnelIndication(data, len);
|
|
sendRoutingIndication(data, len);
|
|
});
|
|
syncOpenKnxConfigFromDevice();
|
|
}
|
|
if (!configureTpUart()) {
|
|
last_error_ = last_error_.empty() ? "failed to configure KNX TP-UART" : last_error_;
|
|
return ESP_FAIL;
|
|
}
|
|
if (!configureProgrammingGpio()) {
|
|
last_error_ = last_error_.empty() ? "failed to configure KNX programming GPIO" : last_error_;
|
|
return ESP_FAIL;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::taskLoop() {
|
|
startup_result_ = initializeRuntime();
|
|
if (startup_result_ == ESP_OK) {
|
|
started_ = true;
|
|
}
|
|
if (startup_semaphore_ != nullptr) {
|
|
xSemaphoreGive(startup_semaphore_);
|
|
}
|
|
if (startup_result_ != ESP_OK || stop_requested_) {
|
|
finishTask();
|
|
return;
|
|
}
|
|
std::array<uint8_t, 768> buffer{};
|
|
auto run_maintenance = [this]() {
|
|
{
|
|
SemaphoreGuard guard(openknx_lock_);
|
|
if (ets_device_ != nullptr) {
|
|
pollProgrammingButton();
|
|
ets_device_->loop();
|
|
tp_uart_online_ = ets_device_->tpUartOnline();
|
|
updateProgrammingLed();
|
|
}
|
|
}
|
|
};
|
|
while (!stop_requested_) {
|
|
const TickType_t now = xTaskGetTickCount();
|
|
if (network_refresh_tick_ == 0 ||
|
|
now - network_refresh_tick_ >= pdMS_TO_TICKS(1000)) {
|
|
refreshNetworkInterfaces(false);
|
|
pruneStaleTunnelClients();
|
|
network_refresh_tick_ = now;
|
|
}
|
|
|
|
fd_set read_fds;
|
|
FD_ZERO(&read_fds);
|
|
int max_fd = -1;
|
|
if (udp_sock_ >= 0) {
|
|
FD_SET(udp_sock_, &read_fds);
|
|
max_fd = std::max(max_fd, udp_sock_);
|
|
}
|
|
if (tcp_sock_ >= 0) {
|
|
FD_SET(tcp_sock_, &read_fds);
|
|
max_fd = std::max(max_fd, tcp_sock_);
|
|
}
|
|
for (const auto& client : tcp_clients_) {
|
|
if (client.sock >= 0) {
|
|
FD_SET(client.sock, &read_fds);
|
|
max_fd = std::max(max_fd, client.sock);
|
|
}
|
|
}
|
|
|
|
timeval timeout{};
|
|
timeout.tv_sec = 0;
|
|
timeout.tv_usec = 20000;
|
|
const int selected = max_fd >= 0 ? select(max_fd + 1, &read_fds, nullptr, nullptr, &timeout)
|
|
: 0;
|
|
if (selected < 0) {
|
|
ESP_LOGW(kTag, "KNXnet/IP socket select failed: errno=%d (%s)", errno,
|
|
std::strerror(errno));
|
|
run_maintenance();
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
continue;
|
|
}
|
|
if (selected == 0) {
|
|
run_maintenance();
|
|
continue;
|
|
}
|
|
|
|
if (tcp_sock_ >= 0 && FD_ISSET(tcp_sock_, &read_fds)) {
|
|
handleTcpAccept();
|
|
}
|
|
for (auto& client : tcp_clients_) {
|
|
if (client.sock >= 0 && FD_ISSET(client.sock, &read_fds)) {
|
|
handleTcpClient(client);
|
|
}
|
|
}
|
|
sockaddr_in remote{};
|
|
socklen_t remote_len = sizeof(remote);
|
|
if (udp_sock_ >= 0 && FD_ISSET(udp_sock_, &read_fds)) {
|
|
const int received = recvfrom(udp_sock_, buffer.data(), buffer.size(), 0,
|
|
reinterpret_cast<sockaddr*>(&remote), &remote_len);
|
|
if (received > 0) {
|
|
handleUdpDatagram(buffer.data(), static_cast<size_t>(received), remote);
|
|
}
|
|
}
|
|
run_maintenance();
|
|
}
|
|
finishTask();
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::finishTask() {
|
|
closeSockets();
|
|
{
|
|
SemaphoreGuard guard(openknx_lock_);
|
|
setProgrammingLed(false);
|
|
knx_ip_parameters_.reset();
|
|
bridge_.setRuntimeContext(nullptr);
|
|
ets_device_.reset();
|
|
openknx_configured_.store(false);
|
|
}
|
|
started_ = false;
|
|
task_handle_ = nullptr;
|
|
vTaskDelete(nullptr);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::pollProgrammingButton() {
|
|
if (config_.programming_button_gpio < 0 || ets_device_ == nullptr) {
|
|
return;
|
|
}
|
|
const int level = gpio_get_level(static_cast<gpio_num_t>(config_.programming_button_gpio));
|
|
const bool pressed = config_.programming_button_active_low ? level == 0 : level != 0;
|
|
const TickType_t now = xTaskGetTickCount();
|
|
if (pressed && !programming_button_last_pressed_ &&
|
|
now - programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) {
|
|
ets_device_->toggleProgrammingMode();
|
|
ESP_LOGI(kTag, "KNX programming mode %s namespace=%s",
|
|
ets_device_->programmingMode() ? "enabled" : "disabled",
|
|
openknx_namespace_.c_str());
|
|
programming_button_last_toggle_tick_ = now;
|
|
}
|
|
programming_button_last_pressed_ = pressed;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::updateProgrammingLed() {
|
|
if (config_.programming_led_gpio < 0 || ets_device_ == nullptr) {
|
|
return;
|
|
}
|
|
const bool programming_mode = ets_device_->programmingMode();
|
|
if (programming_mode == programming_led_state_) {
|
|
return;
|
|
}
|
|
setProgrammingLed(programming_mode);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::setProgrammingLed(bool on) {
|
|
if (config_.programming_led_gpio < 0) {
|
|
programming_led_state_ = on;
|
|
return;
|
|
}
|
|
const bool level = config_.programming_led_active_high ? on : !on;
|
|
gpio_set_level(static_cast<gpio_num_t>(config_.programming_led_gpio), level ? 1 : 0);
|
|
programming_led_state_ = on;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::closeSockets() {
|
|
if (udp_sock_ >= 0) {
|
|
shutdown(udp_sock_, SHUT_RDWR);
|
|
close(udp_sock_);
|
|
udp_sock_ = -1;
|
|
}
|
|
if (tcp_sock_ >= 0) {
|
|
shutdown(tcp_sock_, SHUT_RDWR);
|
|
close(tcp_sock_);
|
|
tcp_sock_ = -1;
|
|
}
|
|
for (auto& client : tcp_clients_) {
|
|
closeTcpClient(client);
|
|
}
|
|
active_tcp_sock_ = -1;
|
|
multicast_joined_interfaces_.clear();
|
|
network_refresh_tick_ = 0;
|
|
for (auto& client : tunnel_clients_) {
|
|
resetTunnelClient(client);
|
|
}
|
|
last_tunnel_channel_id_ = 0;
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::configureSocket() {
|
|
udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
|
if (udp_sock_ < 0) {
|
|
last_error_ = ErrnoDetail("failed to create KNXnet/IP UDP socket", errno);
|
|
ESP_LOGE(kTag, "%s", last_error_.c_str());
|
|
return false;
|
|
}
|
|
int broadcast = 1;
|
|
if (setsockopt(udp_sock_, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) {
|
|
ESP_LOGW(kTag, "failed to enable broadcast for KNX UDP port %u: errno=%d (%s)",
|
|
static_cast<unsigned>(config_.udp_port), errno, std::strerror(errno));
|
|
}
|
|
|
|
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) {
|
|
const int saved_errno = errno;
|
|
last_error_ = ErrnoDetail("failed to bind KNXnet/IP UDP socket on port " +
|
|
std::to_string(config_.udp_port),
|
|
saved_errno);
|
|
ESP_LOGE(kTag, "%s", last_error_.c_str());
|
|
closeSockets();
|
|
return false;
|
|
}
|
|
|
|
timeval timeout{};
|
|
timeout.tv_sec = 0;
|
|
timeout.tv_usec = 20000;
|
|
if (setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
|
|
ESP_LOGW(kTag, "failed to set KNX UDP receive timeout on port %u: errno=%d (%s)",
|
|
static_cast<unsigned>(config_.udp_port), errno, std::strerror(errno));
|
|
}
|
|
|
|
if (config_.multicast_enabled) {
|
|
uint8_t multicast_loop = 0;
|
|
if (setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &multicast_loop,
|
|
sizeof(multicast_loop)) < 0) {
|
|
ESP_LOGW(kTag, "failed to disable KNX multicast loopback for %s: errno=%d (%s)",
|
|
config_.multicast_address.c_str(), errno, std::strerror(errno));
|
|
}
|
|
refreshNetworkInterfaces(true);
|
|
}
|
|
|
|
tcp_sock_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
if (tcp_sock_ < 0) {
|
|
last_error_ = ErrnoDetail("failed to create KNXnet/IP TCP socket", errno);
|
|
ESP_LOGE(kTag, "%s", last_error_.c_str());
|
|
closeSockets();
|
|
return false;
|
|
}
|
|
int reuse = 1;
|
|
if (setsockopt(tcp_sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
|
|
ESP_LOGW(kTag, "failed to enable TCP reuse for KNX port %u: errno=%d (%s)",
|
|
static_cast<unsigned>(config_.udp_port), errno, std::strerror(errno));
|
|
}
|
|
sockaddr_in tcp_bind_addr{};
|
|
tcp_bind_addr.sin_family = AF_INET;
|
|
tcp_bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
|
tcp_bind_addr.sin_port = htons(config_.udp_port);
|
|
if (bind(tcp_sock_, reinterpret_cast<sockaddr*>(&tcp_bind_addr), sizeof(tcp_bind_addr)) < 0) {
|
|
const int saved_errno = errno;
|
|
last_error_ = ErrnoDetail("failed to bind KNXnet/IP TCP socket on port " +
|
|
std::to_string(config_.udp_port),
|
|
saved_errno);
|
|
ESP_LOGE(kTag, "%s", last_error_.c_str());
|
|
closeSockets();
|
|
return false;
|
|
}
|
|
if (listen(tcp_sock_, static_cast<int>(kMaxTcpClients)) < 0) {
|
|
const int saved_errno = errno;
|
|
last_error_ = ErrnoDetail("failed to listen on KNXnet/IP TCP port " +
|
|
std::to_string(config_.udp_port),
|
|
saved_errno);
|
|
ESP_LOGE(kTag, "%s", last_error_.c_str());
|
|
closeSockets();
|
|
return false;
|
|
}
|
|
ESP_LOGI(kTag, "KNXnet/IP listening on UDP/TCP port %u",
|
|
static_cast<unsigned>(config_.udp_port));
|
|
return true;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleTcpAccept() {
|
|
sockaddr_in remote{};
|
|
socklen_t remote_len = sizeof(remote);
|
|
const int client_sock = accept(tcp_sock_, reinterpret_cast<sockaddr*>(&remote), &remote_len);
|
|
if (client_sock < 0) {
|
|
ESP_LOGW(kTag, "failed to accept KNXnet/IP TCP client: errno=%d (%s)", errno,
|
|
std::strerror(errno));
|
|
return;
|
|
}
|
|
TcpClient* slot = nullptr;
|
|
for (auto& client : tcp_clients_) {
|
|
if (client.sock < 0) {
|
|
slot = &client;
|
|
break;
|
|
}
|
|
}
|
|
if (slot == nullptr) {
|
|
ESP_LOGW(kTag, "reject KNXnet/IP TCP client from %s: no free TCP slots",
|
|
EndpointString(remote).c_str());
|
|
close(client_sock);
|
|
return;
|
|
}
|
|
slot->sock = client_sock;
|
|
slot->remote = remote;
|
|
slot->rx_buffer.clear();
|
|
slot->last_activity_tick = xTaskGetTickCount();
|
|
ESP_LOGI(kTag, "accepted KNXnet/IP TCP client from %s", EndpointString(remote).c_str());
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleTcpClient(TcpClient& client) {
|
|
if (client.sock < 0) {
|
|
return;
|
|
}
|
|
std::array<uint8_t, 512> buffer{};
|
|
const int received = recv(client.sock, buffer.data(), buffer.size(), 0);
|
|
if (received <= 0) {
|
|
ESP_LOGI(kTag, "closed KNXnet/IP TCP client from %s", EndpointString(client.remote).c_str());
|
|
closeTcpClient(client);
|
|
return;
|
|
}
|
|
client.last_activity_tick = xTaskGetTickCount();
|
|
client.rx_buffer.insert(client.rx_buffer.end(), buffer.begin(), buffer.begin() + received);
|
|
while (client.rx_buffer.size() >= 6) {
|
|
uint16_t service = 0;
|
|
uint16_t total_len = 0;
|
|
if (!ParseKnxNetIpHeader(client.rx_buffer.data(), client.rx_buffer.size(), &service,
|
|
&total_len)) {
|
|
ESP_LOGW(kTag, "invalid KNXnet/IP TCP packet from %s; closing stream",
|
|
EndpointString(client.remote).c_str());
|
|
closeTcpClient(client);
|
|
return;
|
|
}
|
|
if (client.rx_buffer.size() < total_len) {
|
|
return;
|
|
}
|
|
std::vector<uint8_t> packet(client.rx_buffer.begin(), client.rx_buffer.begin() + total_len);
|
|
client.rx_buffer.erase(client.rx_buffer.begin(), client.rx_buffer.begin() + total_len);
|
|
active_tcp_sock_ = client.sock;
|
|
handleUdpDatagram(packet.data(), packet.size(), client.remote);
|
|
active_tcp_sock_ = -1;
|
|
if (client.sock < 0) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::closeTcpClient(TcpClient& client) {
|
|
if (client.sock < 0) {
|
|
client.rx_buffer.clear();
|
|
return;
|
|
}
|
|
const int sock = client.sock;
|
|
for (auto& tunnel : tunnel_clients_) {
|
|
if (tunnel.connected && tunnel.tcp_sock == sock) {
|
|
resetTunnelClient(tunnel);
|
|
}
|
|
}
|
|
if (active_tcp_sock_ == sock) {
|
|
active_tcp_sock_ = -1;
|
|
}
|
|
shutdown(sock, SHUT_RDWR);
|
|
close(sock);
|
|
client.sock = -1;
|
|
client.rx_buffer.clear();
|
|
client.last_activity_tick = 0;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::refreshNetworkInterfaces(bool force_log) {
|
|
if (!config_.multicast_enabled || udp_sock_ < 0) {
|
|
return;
|
|
}
|
|
const auto netifs = ActiveKnxNetifs();
|
|
if (netifs.empty()) {
|
|
SemaphoreGuard guard(openknx_lock_);
|
|
if (ets_device_ != nullptr) {
|
|
ets_device_->setNetworkInterface(nullptr);
|
|
}
|
|
if (force_log) {
|
|
ESP_LOGW(kTag, "KNX multicast group %s not joined yet: no IPv4 interface is up",
|
|
config_.multicast_address.c_str());
|
|
}
|
|
return;
|
|
}
|
|
|
|
{
|
|
SemaphoreGuard guard(openknx_lock_);
|
|
if (ets_device_ != nullptr) {
|
|
ets_device_->setNetworkInterface(netifs.front().netif);
|
|
}
|
|
}
|
|
|
|
const uint32_t multicast_address = inet_addr(config_.multicast_address.c_str());
|
|
for (const auto& netif : netifs) {
|
|
if (std::find(multicast_joined_interfaces_.begin(), multicast_joined_interfaces_.end(),
|
|
netif.address) != multicast_joined_interfaces_.end()) {
|
|
continue;
|
|
}
|
|
ip_mreq mreq{};
|
|
mreq.imr_multiaddr.s_addr = multicast_address;
|
|
mreq.imr_interface.s_addr = netif.address;
|
|
if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
|
|
ESP_LOGW(kTag,
|
|
"failed to join KNX multicast group %s on %s %s UDP port %u: errno=%d (%s)",
|
|
config_.multicast_address.c_str(), netif.key, Ipv4String(netif.address).c_str(),
|
|
static_cast<unsigned>(config_.udp_port), errno, std::strerror(errno));
|
|
continue;
|
|
}
|
|
multicast_joined_interfaces_.push_back(netif.address);
|
|
ESP_LOGI(kTag, "joined KNX multicast group %s on %s %s UDP port %u",
|
|
config_.multicast_address.c_str(), netif.key, Ipv4String(netif.address).c_str(),
|
|
static_cast<unsigned>(config_.udp_port));
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<openknx::TpuartUartInterface>
|
|
GatewayKnxTpIpRouter::createOpenKnxTpUartInterface() {
|
|
if (!GatewayKnxConfigUsesTpUart(config_)) {
|
|
tp_uart_port_ = -1;
|
|
tp_uart_tx_pin_ = -1;
|
|
tp_uart_rx_pin_ = -1;
|
|
tp_uart_online_ = false;
|
|
ESP_LOGI(kTag, "KNX TP-UART disabled by UART port; KNXnet/IP uses IP-only runtime");
|
|
return nullptr;
|
|
}
|
|
const auto& serial = config_.tp_uart;
|
|
if (serial.uart_port < 0 || serial.uart_port > 2) {
|
|
last_error_ = "invalid KNX TP-UART port " + std::to_string(serial.uart_port);
|
|
ESP_LOGE(kTag, "%s", last_error_.c_str());
|
|
return nullptr;
|
|
}
|
|
const uart_port_t uart_port = static_cast<uart_port_t>(serial.uart_port);
|
|
int tx_pin = UART_PIN_NO_CHANGE;
|
|
int rx_pin = UART_PIN_NO_CHANGE;
|
|
const bool tx_pin_ok = ResolveUartIoPin(uart_port, serial.tx_pin, SOC_UART_TX_PIN_IDX, &tx_pin);
|
|
const bool rx_pin_ok = ResolveUartIoPin(uart_port, serial.rx_pin, SOC_UART_RX_PIN_IDX, &rx_pin);
|
|
if (!tx_pin_ok || !rx_pin_ok) {
|
|
last_error_ = "KNX TP-UART UART" + std::to_string(serial.uart_port) +
|
|
" has no ESP-IDF default " + (!tx_pin_ok ? std::string("TX") : std::string("")) +
|
|
(!tx_pin_ok && !rx_pin_ok ? "/" : "") +
|
|
(!rx_pin_ok ? std::string("RX") : std::string("")) +
|
|
" pin; configure explicit txPin/rxPin values";
|
|
ESP_LOGE(kTag, "%s", last_error_.c_str());
|
|
return nullptr;
|
|
}
|
|
tp_uart_port_ = serial.uart_port;
|
|
tp_uart_tx_pin_ = tx_pin;
|
|
tp_uart_rx_pin_ = rx_pin;
|
|
return std::make_unique<openknx::TpuartUartInterface>(
|
|
uart_port, serial.tx_pin, serial.rx_pin, serial.rx_buffer_size,
|
|
serial.tx_buffer_size, serial.nine_bit_mode);
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::configureTpUart() {
|
|
if (tp_uart_port_ < 0) {
|
|
return true;
|
|
}
|
|
if (ets_device_ == nullptr || !ets_device_->hasTpUart()) {
|
|
last_error_ = "KNX TP-UART interface is unavailable in OpenKNX runtime";
|
|
ESP_LOGE(kTag, "%s", last_error_.c_str());
|
|
return false;
|
|
}
|
|
const TickType_t startup_timeout_ticks =
|
|
pdMS_TO_TICKS(config_.tp_uart.startup_timeout_ms);
|
|
const TickType_t retry_poll_ticks = std::max<TickType_t>(1, pdMS_TO_TICKS(20));
|
|
const TickType_t startup_begin_tick = xTaskGetTickCount();
|
|
tp_uart_online_ = ets_device_->enableTpUart(true);
|
|
while (!tp_uart_online_ && startup_timeout_ticks > 0 &&
|
|
(xTaskGetTickCount() - startup_begin_tick) < startup_timeout_ticks) {
|
|
vTaskDelay(retry_poll_ticks);
|
|
ets_device_->loop();
|
|
tp_uart_online_ = ets_device_->tpUartOnline();
|
|
}
|
|
if (!tp_uart_online_) {
|
|
last_error_ = "OpenKNX failed to initialize KNX TP-UART uart=" +
|
|
std::to_string(config_.tp_uart.uart_port) + " tx=" +
|
|
UartPinDescription(config_.tp_uart.tx_pin, tp_uart_tx_pin_) + " rx=" +
|
|
UartPinDescription(config_.tp_uart.rx_pin, tp_uart_rx_pin_);
|
|
const bool configured = ets_device_ != nullptr && ets_device_->configured();
|
|
ESP_LOGW(kTag,
|
|
"%s; continuing KNXnet/IP in %s IP mode while TP-UART stays offline",
|
|
last_error_.c_str(), configured ? "configured" : "commissioning-only");
|
|
tp_uart_port_ = -1;
|
|
tp_uart_online_ = false;
|
|
return true;
|
|
}
|
|
const TickType_t startup_elapsed_ticks = xTaskGetTickCount() - startup_begin_tick;
|
|
if (startup_elapsed_ticks > 0) {
|
|
ESP_LOGI(kTag, "KNX TP-UART startup settled after %lu ms",
|
|
static_cast<unsigned long>(pdTICKS_TO_MS(startup_elapsed_ticks)));
|
|
}
|
|
ESP_LOGI(kTag, "KNX TP-UART online uart=%d tx=%s rx=%s baud=%u nineBit=%d",
|
|
config_.tp_uart.uart_port,
|
|
UartPinDescription(config_.tp_uart.tx_pin, tp_uart_tx_pin_).c_str(),
|
|
UartPinDescription(config_.tp_uart.rx_pin, tp_uart_rx_pin_).c_str(),
|
|
static_cast<unsigned>(config_.tp_uart.baudrate), config_.tp_uart.nine_bit_mode);
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::configureProgrammingGpio() {
|
|
programming_button_last_pressed_ = false;
|
|
programming_button_last_toggle_tick_ = 0;
|
|
programming_led_state_ = false;
|
|
|
|
if (config_.programming_button_gpio >= 0) {
|
|
gpio_config_t button_config{};
|
|
button_config.pin_bit_mask = 1ULL << static_cast<uint32_t>(config_.programming_button_gpio);
|
|
button_config.mode = GPIO_MODE_INPUT;
|
|
button_config.pull_up_en = config_.programming_button_active_low ? GPIO_PULLUP_ENABLE
|
|
: GPIO_PULLUP_DISABLE;
|
|
button_config.pull_down_en = config_.programming_button_active_low ? GPIO_PULLDOWN_DISABLE
|
|
: GPIO_PULLDOWN_ENABLE;
|
|
button_config.intr_type = GPIO_INTR_DISABLE;
|
|
const esp_err_t err = gpio_config(&button_config);
|
|
if (err != ESP_OK) {
|
|
last_error_ = EspErrDetail("failed to configure KNX programming button GPIO" +
|
|
std::to_string(config_.programming_button_gpio),
|
|
err);
|
|
ESP_LOGE(kTag, "%s", last_error_.c_str());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (config_.programming_led_gpio >= 0) {
|
|
gpio_config_t led_config{};
|
|
led_config.pin_bit_mask = 1ULL << static_cast<uint32_t>(config_.programming_led_gpio);
|
|
led_config.mode = GPIO_MODE_OUTPUT;
|
|
led_config.pull_up_en = GPIO_PULLUP_DISABLE;
|
|
led_config.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
|
led_config.intr_type = GPIO_INTR_DISABLE;
|
|
const esp_err_t err = gpio_config(&led_config);
|
|
if (err != ESP_OK) {
|
|
last_error_ = EspErrDetail("failed to configure KNX programming LED GPIO" +
|
|
std::to_string(config_.programming_led_gpio),
|
|
err);
|
|
ESP_LOGE(kTag, "%s", last_error_.c_str());
|
|
return false;
|
|
}
|
|
setProgrammingLed(false);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
if (IsKnxNetIpSecureService(service)) {
|
|
handleSecureService(service, body, body_len, remote);
|
|
return;
|
|
}
|
|
switch (service) {
|
|
case kServiceSearchRequest:
|
|
case kServiceSearchRequestExt:
|
|
handleSearchRequest(service, data, total_len, remote);
|
|
break;
|
|
case kServiceDescriptionRequest:
|
|
handleDescriptionRequest(data, total_len, remote);
|
|
break;
|
|
case kServiceDeviceConfigurationRequest:
|
|
handleDeviceConfigurationRequest(data, total_len, remote);
|
|
break;
|
|
case kServiceDeviceConfigurationAck:
|
|
case kServiceTunnellingAck:
|
|
if (body_len >= 4) {
|
|
ESP_LOGD(kTag, "rx KNXnet/IP ack service=0x%04x channel=%u seq=%u status=0x%02x from %s",
|
|
static_cast<unsigned>(service), static_cast<unsigned>(body[1]),
|
|
static_cast<unsigned>(body[2]), static_cast<unsigned>(body[3]),
|
|
EndpointString(remote).c_str());
|
|
TunnelClient* client = findTunnelClient(body[1]);
|
|
if (client != nullptr) {
|
|
client->last_activity_tick = xTaskGetTickCount();
|
|
if (service == kServiceTunnellingAck && body[3] == kKnxNoError &&
|
|
!client->last_tunnel_confirmation_packet.empty() &&
|
|
client->last_tunnel_confirmation_sequence == body[2]) {
|
|
client->last_tunnel_confirmation_packet.clear();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case kServiceRoutingIndication:
|
|
if (config_.multicast_enabled) {
|
|
handleRoutingIndication(data, total_len);
|
|
}
|
|
break;
|
|
case kServiceTunnellingRequest:
|
|
if (config_.tunnel_enabled) {
|
|
handleTunnellingRequest(data, total_len, remote);
|
|
}
|
|
break;
|
|
case kServiceConnectRequest:
|
|
if (config_.tunnel_enabled) {
|
|
handleConnectRequest(data, total_len, remote);
|
|
}
|
|
break;
|
|
case kServiceConnectionStateRequest:
|
|
handleConnectionStateRequest(data, total_len, remote);
|
|
break;
|
|
case kServiceDisconnectRequest:
|
|
handleDisconnectRequest(data, total_len, remote);
|
|
break;
|
|
default:
|
|
ESP_LOGD(kTag, "ignore KNXnet/IP service=0x%04x len=%u from %s",
|
|
static_cast<unsigned>(service), static_cast<unsigned>(body_len),
|
|
EndpointString(remote).c_str());
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleSearchRequest(uint16_t service, const uint8_t* packet_data,
|
|
size_t len, const sockaddr_in& remote) {
|
|
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_IPHPAI) {
|
|
ESP_LOGW(kTag, "invalid KNXnet/IP search request from %s len=%u",
|
|
EndpointString(remote).c_str(), static_cast<unsigned>(len));
|
|
return;
|
|
}
|
|
std::vector<uint8_t> request_packet(packet_data, packet_data + len);
|
|
KnxIpSearchRequest request(request_packet.data(), static_cast<uint16_t>(request_packet.size()));
|
|
auto& request_hpai = request.hpai();
|
|
if (OpenKnxHpaiUsesUnsupportedProtocol(request_hpai, currentTransportAllowsTcpHpai())) {
|
|
ESP_LOGW(kTag, "ignore KNXnet/IP search request from %s: unsupported HPAI protocol",
|
|
EndpointString(remote).c_str());
|
|
return;
|
|
}
|
|
sockaddr_in response_remote = EndpointFromOpenKnxHpai(request_hpai, remote);
|
|
selectOpenKnxNetworkInterface(response_remote);
|
|
|
|
const auto hpai = localHpaiForRemote(response_remote, currentTransportAllowsTcpHpai());
|
|
if (!hpai.has_value()) {
|
|
ESP_LOGW(kTag, "cannot send KNXnet/IP search response to %s: no active IPv4 interface",
|
|
EndpointString(response_remote).c_str());
|
|
return;
|
|
}
|
|
std::vector<uint8_t> body_resp;
|
|
body_resp.insert(body_resp.end(), hpai->begin(), hpai->end());
|
|
auto dev_dib = buildDeviceInfoDib(response_remote);
|
|
body_resp.insert(body_resp.end(), dev_dib.begin(), dev_dib.end());
|
|
auto svc_dib = buildSupportedServiceDib();
|
|
body_resp.insert(body_resp.end(), svc_dib.begin(), svc_dib.end());
|
|
if (service == kServiceSearchRequestExt) {
|
|
auto ext_dib = buildExtendedDeviceInfoDib();
|
|
body_resp.insert(body_resp.end(), ext_dib.begin(), ext_dib.end());
|
|
auto ip_dib = buildIpConfigDib(response_remote, false);
|
|
body_resp.insert(body_resp.end(), ip_dib.begin(), ip_dib.end());
|
|
auto current_ip_dib = buildIpConfigDib(response_remote, true);
|
|
body_resp.insert(body_resp.end(), current_ip_dib.begin(), current_ip_dib.end());
|
|
auto addresses_dib = buildKnxAddressesDib();
|
|
body_resp.insert(body_resp.end(), addresses_dib.begin(), addresses_dib.end());
|
|
auto tunneling_dib = buildTunnelingInfoDib();
|
|
body_resp.insert(body_resp.end(), tunneling_dib.begin(), tunneling_dib.end());
|
|
}
|
|
const auto response_packet = OpenKnxIpPacket(
|
|
service == kServiceSearchRequestExt ? kServiceSearchResponseExt : kServiceSearchResponse,
|
|
body_resp);
|
|
sendPacket(response_packet, response_remote);
|
|
ESP_LOGI(kTag, "sent KNXnet/IP search response service=0x%04x namespace=%s mainGroup=%u to %s:%u endpoint=%u.%u.%u.%u:%u",
|
|
static_cast<unsigned>(service), openknx_namespace_.c_str(),
|
|
static_cast<unsigned>(config_.main_group),
|
|
Ipv4String(response_remote.sin_addr.s_addr).c_str(), static_cast<unsigned>(ntohs(response_remote.sin_port)),
|
|
static_cast<unsigned>((*hpai)[2]), static_cast<unsigned>((*hpai)[3]),
|
|
static_cast<unsigned>((*hpai)[4]), static_cast<unsigned>((*hpai)[5]),
|
|
static_cast<unsigned>(config_.udp_port));
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleDescriptionRequest(const uint8_t* packet_data, size_t len,
|
|
const sockaddr_in& remote) {
|
|
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_IPHPAI) {
|
|
ESP_LOGW(kTag, "invalid KNXnet/IP description request from %s len=%u",
|
|
EndpointString(remote).c_str(), static_cast<unsigned>(len));
|
|
return;
|
|
}
|
|
std::vector<uint8_t> request_packet(packet_data, packet_data + len);
|
|
KnxIpDescriptionRequest request(request_packet.data(), static_cast<uint16_t>(request_packet.size()));
|
|
auto& hpai = request.hpaiCtrl();
|
|
if (OpenKnxHpaiUsesUnsupportedProtocol(hpai, currentTransportAllowsTcpHpai())) {
|
|
ESP_LOGW(kTag, "ignore KNXnet/IP description request from %s: unsupported HPAI protocol",
|
|
EndpointString(remote).c_str());
|
|
return;
|
|
}
|
|
const sockaddr_in response_remote = EndpointFromOpenKnxHpai(hpai, remote);
|
|
selectOpenKnxNetworkInterface(response_remote);
|
|
auto device = buildDeviceInfoDib(response_remote);
|
|
auto services = buildSupportedServiceDib();
|
|
std::vector<uint8_t> body_resp;
|
|
auto extended = buildExtendedDeviceInfoDib();
|
|
auto ip_config = buildIpConfigDib(response_remote, false);
|
|
auto current_ip_config = buildIpConfigDib(response_remote, true);
|
|
auto addresses = buildKnxAddressesDib();
|
|
auto tunneling = buildTunnelingInfoDib();
|
|
body_resp.reserve(device.size() + services.size() + extended.size() + ip_config.size() +
|
|
current_ip_config.size() + addresses.size() + tunneling.size());
|
|
body_resp.insert(body_resp.end(), device.begin(), device.end());
|
|
body_resp.insert(body_resp.end(), services.begin(), services.end());
|
|
body_resp.insert(body_resp.end(), extended.begin(), extended.end());
|
|
body_resp.insert(body_resp.end(), ip_config.begin(), ip_config.end());
|
|
body_resp.insert(body_resp.end(), current_ip_config.begin(), current_ip_config.end());
|
|
body_resp.insert(body_resp.end(), addresses.begin(), addresses.end());
|
|
body_resp.insert(body_resp.end(), tunneling.begin(), tunneling.end());
|
|
const auto response_packet = OpenKnxIpPacket(kServiceDescriptionResponse, body_resp);
|
|
sendPacket(response_packet, response_remote);
|
|
ESP_LOGI(kTag, "sent KNXnet/IP description response namespace=%s medium=0x%02x tpOnline=%d to %s:%u",
|
|
openknx_namespace_.c_str(), static_cast<unsigned>(advertisedMedium()),
|
|
tp_uart_online_, Ipv4String(response_remote.sin_addr.s_addr).c_str(),
|
|
static_cast<unsigned>(ntohs(response_remote.sin_port)));
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* packet_data, size_t len) {
|
|
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2) {
|
|
return;
|
|
}
|
|
std::vector<uint8_t> packet(packet_data, packet_data + len);
|
|
KnxIpRoutingIndication routing(packet.data(), static_cast<uint16_t>(packet.size()));
|
|
CemiFrame& frame = routing.frame();
|
|
if (!frame.valid()) {
|
|
ESP_LOGW(kTag, "invalid OpenKNX routing cEMI len=%u", static_cast<unsigned>(len));
|
|
return;
|
|
}
|
|
const uint8_t* cemi = frame.data();
|
|
const size_t cemi_len = frame.dataLength();
|
|
bool consumed_by_local_application = false;
|
|
if (ets_device_ != nullptr && MatchesOpenKnxLocalIndividualAddress(frame, *ets_device_)) {
|
|
std::vector<uint8_t> local_tunnel_frame;
|
|
if (BuildLocalRoutingTunnelFrame(cemi, cemi_len, &local_tunnel_frame)) {
|
|
consumed_by_local_application = handleOpenKnxTunnelFrame(
|
|
local_tunnel_frame.data(), local_tunnel_frame.size(), nullptr,
|
|
kServiceRoutingIndication,
|
|
frame.messageCode() == L_data_ind ? cemi : nullptr,
|
|
frame.messageCode() == L_data_ind ? cemi_len : 0);
|
|
}
|
|
}
|
|
if (consumed_by_local_application) {
|
|
return;
|
|
}
|
|
const bool consumed_by_openknx = handleOpenKnxBusFrame(cemi, cemi_len);
|
|
const bool routed_to_dali = routeOpenKnxGroupWrite(cemi, cemi_len, "KNX routing indication");
|
|
const bool sent_to_tp = transmitOpenKnxTpFrame(cemi, cemi_len);
|
|
if (!consumed_by_openknx && !routed_to_dali && !sent_to_tp) {
|
|
ESP_LOGD(kTag, "KNX routing indication ignored: no OpenKNX/DALI handler matched");
|
|
}
|
|
}
|
|
|
|
GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::findTunnelClient(
|
|
uint8_t channel_id) {
|
|
if (channel_id == 0) {
|
|
return nullptr;
|
|
}
|
|
for (auto& client : tunnel_clients_) {
|
|
if (client.connected && client.channel_id == channel_id) {
|
|
return &client;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::findTunnelClient(
|
|
uint8_t channel_id) const {
|
|
if (channel_id == 0) {
|
|
return nullptr;
|
|
}
|
|
for (const auto& client : tunnel_clients_) {
|
|
if (client.connected && client.channel_id == channel_id) {
|
|
return &client;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::resetTunnelClient(TunnelClient& client) {
|
|
if (client.connected) {
|
|
ESP_LOGI(kTag, "closed KNXnet/IP tunnel channel=%u type=0x%02x data=%s",
|
|
static_cast<unsigned>(client.channel_id),
|
|
static_cast<unsigned>(client.connection_type),
|
|
EndpointString(client.data_remote).c_str());
|
|
}
|
|
client = TunnelClient{};
|
|
}
|
|
|
|
uint8_t GatewayKnxTpIpRouter::nextTunnelChannelId() const {
|
|
uint8_t candidate = last_tunnel_channel_id_;
|
|
for (int attempts = 0; attempts < 255; ++attempts) {
|
|
candidate = static_cast<uint8_t>(candidate + 1);
|
|
if (candidate == 0) {
|
|
candidate = 1;
|
|
}
|
|
if (findTunnelClient(candidate) == nullptr) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddressForSlot(size_t slot) const {
|
|
const uint16_t first = effectiveTunnelAddress();
|
|
const uint16_t line = first & 0xff00;
|
|
uint16_t device = static_cast<uint16_t>((first & 0x00ff) + slot);
|
|
if (device == 0 || device > 0xff) {
|
|
device = static_cast<uint16_t>(1 + slot);
|
|
}
|
|
return static_cast<uint16_t>(line | (device & 0x00ff));
|
|
}
|
|
|
|
GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::allocateTunnelClient(
|
|
const sockaddr_in& control_remote, const sockaddr_in& data_remote, uint8_t connection_type) {
|
|
TunnelClient* free_client = nullptr;
|
|
size_t free_index = 0;
|
|
for (size_t index = 0; index < tunnel_clients_.size(); ++index) {
|
|
auto& client = tunnel_clients_[index];
|
|
const bool same_tcp_stream = active_tcp_sock_ >= 0 && client.tcp_sock == active_tcp_sock_;
|
|
const bool same_udp_endpoints = EndpointEquals(client.control_remote, control_remote) &&
|
|
EndpointEquals(client.data_remote, data_remote);
|
|
if (client.connected && client.connection_type == connection_type &&
|
|
(same_tcp_stream || same_udp_endpoints)) {
|
|
ESP_LOGW(kTag, "replacing existing KNXnet/IP tunnel channel=%u for endpoint %s",
|
|
static_cast<unsigned>(client.channel_id), EndpointString(data_remote).c_str());
|
|
resetTunnelClient(client);
|
|
free_client = &client;
|
|
free_index = index;
|
|
break;
|
|
}
|
|
if (!client.connected && free_client == nullptr) {
|
|
free_client = &client;
|
|
free_index = index;
|
|
}
|
|
}
|
|
if (free_client == nullptr) {
|
|
return nullptr;
|
|
}
|
|
const uint8_t channel_id = nextTunnelChannelId();
|
|
if (channel_id == 0) {
|
|
return nullptr;
|
|
}
|
|
free_client->connected = true;
|
|
free_client->channel_id = channel_id;
|
|
free_client->connection_type = connection_type;
|
|
free_client->received_sequence = 255;
|
|
free_client->send_sequence = 0;
|
|
free_client->individual_address = effectiveTunnelAddressForSlot(free_index);
|
|
free_client->last_activity_tick = xTaskGetTickCount();
|
|
free_client->control_remote = control_remote;
|
|
free_client->data_remote = data_remote;
|
|
free_client->tcp_sock = active_tcp_sock_;
|
|
last_tunnel_channel_id_ = channel_id;
|
|
return free_client;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::pruneStaleTunnelClients() {
|
|
const TickType_t now = xTaskGetTickCount();
|
|
const TickType_t timeout = pdMS_TO_TICKS(120000);
|
|
for (auto& client : tunnel_clients_) {
|
|
if (!client.connected || client.last_activity_tick == 0) {
|
|
continue;
|
|
}
|
|
if (now - client.last_activity_tick > timeout) {
|
|
ESP_LOGW(kTag, "closing stale KNXnet/IP tunnel channel=%u after heartbeat timeout",
|
|
static_cast<unsigned>(client.channel_id));
|
|
resetTunnelClient(client);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* packet_data, size_t len,
|
|
const sockaddr_in& remote) {
|
|
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_CH + 2) {
|
|
ESP_LOGW(kTag, "invalid KNXnet/IP tunnelling request from %s len=%u",
|
|
EndpointString(remote).c_str(), static_cast<unsigned>(len));
|
|
return;
|
|
}
|
|
std::vector<uint8_t> packet(packet_data, packet_data + len);
|
|
KnxIpTunnelingRequest tunneling(packet.data(), static_cast<uint16_t>(packet.size()));
|
|
auto& header = tunneling.connectionHeader();
|
|
if (header.length() != LEN_CH) {
|
|
ESP_LOGW(kTag, "invalid KNXnet/IP tunnelling header from %s len=%u chLen=%u",
|
|
EndpointString(remote).c_str(), static_cast<unsigned>(len),
|
|
static_cast<unsigned>(header.length()));
|
|
return;
|
|
}
|
|
const uint8_t channel_id = header.channelId();
|
|
const uint8_t sequence = header.sequenceCounter();
|
|
TunnelClient* client = findTunnelClient(channel_id);
|
|
if (client == nullptr) {
|
|
ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u from %s: no connection",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
|
|
EndpointString(remote).c_str());
|
|
sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote);
|
|
return;
|
|
}
|
|
const bool same_tcp_stream = client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock;
|
|
if (!same_tcp_stream && !EndpointEquals(remote, client->data_remote)) {
|
|
ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u from %s: expected data endpoint %s",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
|
|
EndpointString(remote).c_str(), EndpointString(client->data_remote).c_str());
|
|
sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote);
|
|
return;
|
|
}
|
|
CemiFrame& frame = tunneling.frame();
|
|
if (!frame.valid()) {
|
|
ESP_LOGW(kTag, "invalid OpenKNX tunnel cEMI channel=%u seq=%u from %s",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
|
|
EndpointString(remote).c_str());
|
|
return;
|
|
}
|
|
if (frame.messageCode() == L_data_req && frame.sourceAddress() == 0) {
|
|
frame.sourceAddress(client->individual_address);
|
|
}
|
|
const uint8_t* cemi = frame.data();
|
|
const size_t cemi_len = frame.dataLength();
|
|
const std::vector<uint8_t> current_cemi(cemi, cemi + cemi_len);
|
|
const bool duplicate_sequence = sequence == client->received_sequence;
|
|
const bool duplicate_payload = duplicate_sequence && client->last_received_cemi == current_cemi;
|
|
if (duplicate_payload) {
|
|
ESP_LOGD(kTag, "duplicate KNXnet/IP tunnelling request channel=%u seq=%u",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence));
|
|
sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote);
|
|
if (!client->last_tunnel_confirmation_packet.empty()) {
|
|
if (sendPacketToTunnelClient(*client, client->last_tunnel_confirmation_packet)) {
|
|
ESP_LOGI(kTag,
|
|
"resent cached KNXnet/IP tunnel confirmation channel=%u confirmSeq=%u after duplicate req seq=%u to %s",
|
|
static_cast<unsigned>(channel_id),
|
|
static_cast<unsigned>(client->last_tunnel_confirmation_sequence),
|
|
static_cast<unsigned>(sequence), EndpointString(client->data_remote).c_str());
|
|
} else {
|
|
ESP_LOGW(kTag,
|
|
"failed to resend cached KNXnet/IP tunnel confirmation channel=%u confirmSeq=%u to %s",
|
|
static_cast<unsigned>(channel_id),
|
|
static_cast<unsigned>(client->last_tunnel_confirmation_sequence),
|
|
EndpointString(client->data_remote).c_str());
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (duplicate_sequence) {
|
|
ESP_LOGW(kTag,
|
|
"accept KNXnet/IP tunnelling request channel=%u with repeated seq=%u because cEMI payload changed",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence));
|
|
} else if (static_cast<uint8_t>(sequence - 1) != client->received_sequence) {
|
|
ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u expected=%u from %s",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
|
|
static_cast<unsigned>(static_cast<uint8_t>(client->received_sequence + 1)),
|
|
EndpointString(remote).c_str());
|
|
sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote);
|
|
return;
|
|
}
|
|
client->received_sequence = sequence;
|
|
client->last_received_cemi = current_cemi;
|
|
client->last_activity_tick = xTaskGetTickCount();
|
|
sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote);
|
|
ESP_LOGI(kTag, "rx KNXnet/IP tunnelling request channel=%u seq=%u cemiLen=%u from %s",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
|
|
static_cast<unsigned>(cemi_len), EndpointString(remote).c_str());
|
|
|
|
const bool consumed_by_openknx = handleOpenKnxTunnelFrame(
|
|
cemi, cemi_len, client, kServiceTunnellingRequest);
|
|
const bool routed_to_dali = routeOpenKnxGroupWrite(cemi, cemi_len, "KNX tunnel frame");
|
|
if (!consumed_by_openknx && routed_to_dali) {
|
|
std::vector<uint8_t> tunnel_confirmation;
|
|
if (BuildTunnelConfirmationFrame(cemi, cemi_len, &tunnel_confirmation)) {
|
|
sendCemiFrameToClient(*client, kServiceTunnellingRequest, tunnel_confirmation.data(),
|
|
tunnel_confirmation.size());
|
|
}
|
|
}
|
|
if (consumed_by_openknx || routed_to_dali) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleDeviceConfigurationRequest(const uint8_t* packet_data, size_t len,
|
|
const sockaddr_in& remote) {
|
|
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + LEN_CH + 2) {
|
|
ESP_LOGW(kTag, "invalid KNXnet/IP device-configuration request from %s len=%u",
|
|
EndpointString(remote).c_str(), static_cast<unsigned>(len));
|
|
return;
|
|
}
|
|
std::vector<uint8_t> packet(packet_data, packet_data + len);
|
|
KnxIpConfigRequest config_request(packet.data(), static_cast<uint16_t>(packet.size()));
|
|
auto& header = config_request.connectionHeader();
|
|
if (header.length() != LEN_CH) {
|
|
ESP_LOGW(kTag, "invalid KNXnet/IP device-configuration header from %s len=%u chLen=%u",
|
|
EndpointString(remote).c_str(), static_cast<unsigned>(len),
|
|
static_cast<unsigned>(header.length()));
|
|
return;
|
|
}
|
|
const uint8_t channel_id = header.channelId();
|
|
const uint8_t sequence = header.sequenceCounter();
|
|
TunnelClient* client = findTunnelClient(channel_id);
|
|
if (client == nullptr) {
|
|
ESP_LOGW(kTag, "reject KNXnet/IP device-configuration request channel=%u seq=%u from %s: no connection",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
|
|
EndpointString(remote).c_str());
|
|
sendDeviceConfigurationAck(channel_id, sequence, kKnxErrorConnectionId, remote);
|
|
return;
|
|
}
|
|
const bool same_tcp_stream = client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock;
|
|
if (!same_tcp_stream && !EndpointEquals(remote, client->data_remote)) {
|
|
ESP_LOGW(kTag, "reject KNXnet/IP device-configuration request channel=%u seq=%u from %s: expected data endpoint %s",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
|
|
EndpointString(remote).c_str(), EndpointString(client->data_remote).c_str());
|
|
sendDeviceConfigurationAck(channel_id, sequence, kKnxErrorConnectionId, remote);
|
|
return;
|
|
}
|
|
client->last_activity_tick = xTaskGetTickCount();
|
|
sendDeviceConfigurationAck(channel_id, sequence, kKnxNoError, client->data_remote);
|
|
CemiFrame& frame = config_request.frame();
|
|
if (!frame.valid()) {
|
|
ESP_LOGW(kTag, "invalid OpenKNX device-configuration cEMI channel=%u seq=%u from %s",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
|
|
EndpointString(remote).c_str());
|
|
return;
|
|
}
|
|
const uint8_t* cemi = frame.data();
|
|
const size_t cemi_len = frame.dataLength();
|
|
ESP_LOGI(kTag, "rx KNXnet/IP device-configuration request channel=%u seq=%u cemiLen=%u from %s",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
|
|
static_cast<unsigned>(cemi_len), EndpointString(remote).c_str());
|
|
if (!handleOpenKnxTunnelFrame(cemi, cemi_len, client, kServiceDeviceConfigurationRequest)) {
|
|
ESP_LOGW(kTag, "KNXnet/IP device-configuration cEMI was not consumed by OpenKNX cemiLen=%u",
|
|
static_cast<unsigned>(cemi_len));
|
|
}
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* packet_data, size_t len,
|
|
const sockaddr_in& remote) {
|
|
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 * LEN_IPHPAI + 2) {
|
|
ESP_LOGW(kTag, "invalid KNXnet/IP connect request from %s len=%u",
|
|
EndpointString(remote).c_str(), static_cast<unsigned>(len));
|
|
return;
|
|
}
|
|
std::vector<uint8_t> packet(packet_data, packet_data + len);
|
|
KnxIpConnectRequest request(packet.data(), static_cast<uint16_t>(packet.size()));
|
|
auto& control_hpai = request.hpaiCtrl();
|
|
auto& data_hpai = request.hpaiData();
|
|
if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai()) ||
|
|
OpenKnxHpaiUsesUnsupportedProtocol(data_hpai, currentTransportAllowsTcpHpai())) {
|
|
ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: unsupported HPAI protocol",
|
|
EndpointString(remote).c_str());
|
|
sendConnectResponse(0, kKnxErrorConnectionType, remote, kKnxConnectionTypeTunnel, 0);
|
|
return;
|
|
}
|
|
sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote);
|
|
sockaddr_in data_remote = EndpointFromOpenKnxHpai(data_hpai, remote);
|
|
selectOpenKnxNetworkInterface(control_remote);
|
|
auto& cri = request.cri();
|
|
const uint8_t cri_length = cri.length();
|
|
const uint8_t connection_type = static_cast<uint8_t>(cri.type());
|
|
if (cri_length < 2 || kKnxNetIpHeaderSize + 2 * LEN_IPHPAI + cri_length > len) {
|
|
ESP_LOGW(kTag, "invalid KNXnet/IP connect CRI from %s len=%u criLen=%u",
|
|
EndpointString(remote).c_str(), static_cast<unsigned>(len),
|
|
static_cast<unsigned>(cri_length));
|
|
sendConnectResponse(0, kKnxErrorConnectionType, control_remote, kKnxConnectionTypeTunnel, 0);
|
|
return;
|
|
}
|
|
if (connection_type != kKnxConnectionTypeTunnel &&
|
|
connection_type != kKnxConnectionTypeDeviceManagement) {
|
|
ESP_LOGW(kTag, "reject KNXnet/IP connect from %s unsupported type=0x%02x",
|
|
EndpointString(remote).c_str(), static_cast<unsigned>(connection_type));
|
|
sendConnectResponse(0, kKnxErrorConnectionType, control_remote, connection_type, 0);
|
|
return;
|
|
}
|
|
if (connection_type == kKnxConnectionTypeTunnel &&
|
|
(cri_length < 4 || cri.layer() != kKnxTunnelLayerLink)) {
|
|
ESP_LOGW(kTag, "reject KNXnet/IP tunnel connect from %s unsupported layer=0x%02x",
|
|
EndpointString(remote).c_str(),
|
|
static_cast<unsigned>(cri_length >= 3 ? cri.layer() : 0));
|
|
sendConnectResponse(0, kKnxErrorTunnellingLayer, control_remote, connection_type, 0);
|
|
return;
|
|
}
|
|
if (!SelectKnxNetifForRemote(control_remote).has_value()) {
|
|
ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: no active IPv4 interface for response",
|
|
EndpointString(remote).c_str());
|
|
sendConnectResponse(0, kKnxErrorConnectionType, control_remote, connection_type, 0);
|
|
return;
|
|
}
|
|
TunnelClient* client = allocateTunnelClient(control_remote, data_remote, connection_type);
|
|
if (client == nullptr) {
|
|
ESP_LOGW(kTag, "reject KNXnet/IP connect from %s: no free tunnel client slots",
|
|
EndpointString(remote).c_str());
|
|
sendConnectResponse(0, kKnxErrorNoMoreConnections, control_remote, connection_type, 0);
|
|
return;
|
|
}
|
|
ESP_LOGI(kTag,
|
|
"accepted KNXnet/IP connect namespace=%s channel=%u type=0x%02x tunnelPa=0x%04x ctrl=%s data=%s remote=%s active=%u/%u",
|
|
openknx_namespace_.c_str(), static_cast<unsigned>(client->channel_id),
|
|
static_cast<unsigned>(connection_type), static_cast<unsigned>(client->individual_address),
|
|
EndpointString(control_remote).c_str(), EndpointString(data_remote).c_str(),
|
|
EndpointString(remote).c_str(),
|
|
static_cast<unsigned>(std::count_if(tunnel_clients_.begin(), tunnel_clients_.end(),
|
|
[](const TunnelClient& item) {
|
|
return item.connected;
|
|
})),
|
|
static_cast<unsigned>(tunnel_clients_.size()));
|
|
sendConnectResponse(client->channel_id, kKnxNoError, control_remote, connection_type,
|
|
client->individual_address);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* packet_data, size_t len,
|
|
const sockaddr_in& remote) {
|
|
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 + LEN_IPHPAI) {
|
|
return;
|
|
}
|
|
std::vector<uint8_t> packet(packet_data, packet_data + len);
|
|
KnxIpStateRequest request(packet.data(), static_cast<uint16_t>(packet.size()));
|
|
auto& control_hpai = request.hpaiCtrl();
|
|
if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai())) {
|
|
ESP_LOGW(kTag,
|
|
"reject KNXnet/IP connection-state request from %s: unsupported HPAI protocol",
|
|
EndpointString(remote).c_str());
|
|
return;
|
|
}
|
|
const uint8_t channel_id = request.channelId();
|
|
const sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote);
|
|
TunnelClient* client = findTunnelClient(channel_id);
|
|
const bool endpoint_matches = client != nullptr &&
|
|
((client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock) ||
|
|
EndpointEquals(control_remote, client->control_remote));
|
|
const uint8_t status = endpoint_matches ? kKnxNoError : kKnxErrorConnectionId;
|
|
if (client != nullptr) {
|
|
if (endpoint_matches) {
|
|
client->last_activity_tick = xTaskGetTickCount();
|
|
}
|
|
}
|
|
ESP_LOGI(kTag, "rx KNXnet/IP connection-state request channel=%u status=0x%02x from %s ctrl=%s expected=%s",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(status),
|
|
EndpointString(remote).c_str(), EndpointString(control_remote).c_str(),
|
|
client == nullptr ? "none" : EndpointString(client->control_remote).c_str());
|
|
sendConnectionStateResponse(
|
|
channel_id, status, control_remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* packet_data, size_t len,
|
|
const sockaddr_in& remote) {
|
|
if (packet_data == nullptr || len < kKnxNetIpHeaderSize + 2 + LEN_IPHPAI) {
|
|
return;
|
|
}
|
|
std::vector<uint8_t> packet(packet_data, packet_data + len);
|
|
KnxIpDisconnectRequest request(packet.data(), static_cast<uint16_t>(packet.size()));
|
|
auto& control_hpai = request.hpaiCtrl();
|
|
if (OpenKnxHpaiUsesUnsupportedProtocol(control_hpai, currentTransportAllowsTcpHpai())) {
|
|
ESP_LOGW(kTag, "reject KNXnet/IP disconnect request from %s: unsupported HPAI protocol",
|
|
EndpointString(remote).c_str());
|
|
return;
|
|
}
|
|
const uint8_t channel_id = request.channelId();
|
|
const sockaddr_in control_remote = EndpointFromOpenKnxHpai(control_hpai, remote);
|
|
TunnelClient* client = findTunnelClient(channel_id);
|
|
const bool endpoint_matches = client != nullptr &&
|
|
((client->tcp_sock >= 0 && active_tcp_sock_ == client->tcp_sock) ||
|
|
EndpointEquals(control_remote, client->control_remote));
|
|
const uint8_t status = endpoint_matches ? kKnxNoError : kKnxErrorConnectionId;
|
|
const std::string expected = client == nullptr ? "none" : EndpointString(client->control_remote);
|
|
if (status == kKnxNoError) {
|
|
resetTunnelClient(*client);
|
|
}
|
|
ESP_LOGI(kTag, "rx KNXnet/IP disconnect request channel=%u status=0x%02x from %s ctrl=%s expected=%s",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(status),
|
|
EndpointString(remote).c_str(), EndpointString(control_remote).c_str(),
|
|
expected.c_str());
|
|
sendDisconnectResponse(channel_id, status, control_remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::handleSecureService(uint16_t service, const uint8_t* body,
|
|
size_t len, const sockaddr_in& remote) {
|
|
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
|
switch (service) {
|
|
case kServiceSecureSessionRequest:
|
|
case kServiceSecureSessionAuth:
|
|
ESP_LOGW(kTag, "KNXnet/IP Secure service 0x%04x rejected: secure sessions are not provisioned", service);
|
|
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
|
|
break;
|
|
case kServiceSecureWrapper:
|
|
ESP_LOGW(kTag, "KNXnet/IP Secure wrapper rejected: no authenticated secure session");
|
|
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
|
|
break;
|
|
case kServiceSecureGroupSync:
|
|
ESP_LOGD(kTag, "KNXnet/IP Secure group sync ignored until secure routing is provisioned");
|
|
break;
|
|
default:
|
|
ESP_LOGD(kTag, "KNXnet/IP Secure service 0x%04x ignored", service);
|
|
break;
|
|
}
|
|
#else
|
|
(void)service;
|
|
(void)body;
|
|
(void)len;
|
|
(void)remote;
|
|
#endif
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequence,
|
|
uint8_t status, const sockaddr_in& remote) {
|
|
sendConnectionHeaderAck(kServiceTunnellingAck, channel_id, sequence, status, remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendDeviceConfigurationAck(uint8_t channel_id, uint8_t sequence,
|
|
uint8_t status,
|
|
const sockaddr_in& remote) {
|
|
sendConnectionHeaderAck(kServiceDeviceConfigurationAck, channel_id, sequence, status, remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendConnectionHeaderAck(uint16_t service, uint8_t channel_id,
|
|
uint8_t sequence, uint8_t status,
|
|
const sockaddr_in& remote) {
|
|
KnxIpTunnelingAck ack;
|
|
ack.serviceTypeIdentifier(service);
|
|
ack.connectionHeader().length(LEN_CH);
|
|
ack.connectionHeader().channelId(channel_id);
|
|
ack.connectionHeader().sequenceCounter(sequence);
|
|
ack.connectionHeader().status(status);
|
|
const std::vector<uint8_t> packet(ack.data(), ack.data() + ack.totalLength());
|
|
sendPacket(packet, remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockaddr_in& remote) {
|
|
const std::vector<uint8_t> body{status, 0x00};
|
|
const auto packet = OpenKnxIpPacket(kServiceSecureSessionStatus, body);
|
|
sendPacket(packet, remote);
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::sendPacket(const std::vector<uint8_t>& packet,
|
|
const sockaddr_in& remote) const {
|
|
if (packet.empty()) {
|
|
return false;
|
|
}
|
|
if (active_tcp_sock_ >= 0) {
|
|
return SendStream(active_tcp_sock_, packet.data(), packet.size());
|
|
}
|
|
return udp_sock_ >= 0 && SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::sendPacketToTunnelClient(
|
|
const TunnelClient& client, const std::vector<uint8_t>& packet) const {
|
|
if (packet.empty()) {
|
|
return false;
|
|
}
|
|
if (client.tcp_sock >= 0) {
|
|
return SendStream(client.tcp_sock, packet.data(), packet.size());
|
|
}
|
|
return udp_sock_ >= 0 && SendAll(udp_sock_, packet.data(), packet.size(), client.data_remote);
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::currentTransportAllowsTcpHpai() const {
|
|
return active_tcp_sock_ >= 0;
|
|
}
|
|
|
|
std::optional<std::array<uint8_t, 8>> GatewayKnxTpIpRouter::localHpaiForRemote(
|
|
const sockaddr_in& remote, bool tcp) const {
|
|
std::array<uint8_t, 8> hpai{};
|
|
hpai[0] = 0x08;
|
|
hpai[1] = tcp ? kKnxHpaiIpv4Tcp : kKnxHpaiIpv4Udp;
|
|
if (tcp) {
|
|
return hpai;
|
|
}
|
|
const auto netif = SelectKnxNetifForRemote(remote);
|
|
if (!netif.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
WriteIp(hpai.data() + 2, netif->address);
|
|
WriteBe16(hpai.data() + 6, config_.udp_port);
|
|
return hpai;
|
|
}
|
|
|
|
std::vector<uint8_t> GatewayKnxTpIpRouter::buildOpenKnxSearchResponse(
|
|
const sockaddr_in& remote) const {
|
|
// Use OpenKNX's proven DIB construction via KnxIpSearchResponse.
|
|
// Requires ets_device_ to be initialized (DeviceObject + Platform).
|
|
if (ets_device_ == nullptr || knx_ip_parameters_ == nullptr) {
|
|
ESP_LOGW(kTag, "OpenKNX search response unavailable; falling back to hand-rolled DIBs");
|
|
return {};
|
|
}
|
|
KnxIpSearchResponse response(*knx_ip_parameters_, ets_device_->deviceObject());
|
|
return std::vector<uint8_t>(response.data(), response.data() + response.totalLength());
|
|
}
|
|
|
|
std::vector<uint8_t> GatewayKnxTpIpRouter::buildOpenKnxDescriptionResponse(
|
|
const sockaddr_in& remote) const {
|
|
if (ets_device_ == nullptr || knx_ip_parameters_ == nullptr) {
|
|
ESP_LOGW(kTag, "OpenKNX description response unavailable; falling back to hand-rolled DIBs");
|
|
return {};
|
|
}
|
|
KnxIpDescriptionResponse response(*knx_ip_parameters_, ets_device_->deviceObject());
|
|
return std::vector<uint8_t>(response.data(), response.data() + response.totalLength());
|
|
}
|
|
|
|
std::vector<uint8_t> GatewayKnxTpIpRouter::buildDeviceInfoDib(
|
|
const sockaddr_in& remote) const {
|
|
std::vector<uint8_t> dib(54, 0);
|
|
dib[0] = static_cast<uint8_t>(dib.size());
|
|
dib[1] = kKnxDibDeviceInfo;
|
|
dib[2] = advertisedMedium();
|
|
dib[3] = 0;
|
|
WriteBe16(dib.data() + 4, effectiveIpInterfaceIndividualAddress());
|
|
WriteBe16(dib.data() + 6, 0);
|
|
|
|
uint8_t mac[6]{};
|
|
if (ReadBaseMac(mac)) {
|
|
dib[8] = static_cast<uint8_t>((knx_internal::kReg1DaliManufacturerId >> 8) & 0xff);
|
|
dib[9] = static_cast<uint8_t>(knx_internal::kReg1DaliManufacturerId & 0xff);
|
|
std::memcpy(dib.data() + 10, mac + 2, 4);
|
|
std::memcpy(dib.data() + 18, mac, 6);
|
|
}
|
|
|
|
WriteIp(dib.data() + 14, inet_addr(config_.multicast_address.c_str()));
|
|
char friendly[31]{};
|
|
std::snprintf(friendly, sizeof(friendly), "DALI GW MG%u %s",
|
|
static_cast<unsigned>(config_.main_group), openknx_namespace_.c_str());
|
|
std::memcpy(dib.data() + 24, friendly, std::min<size_t>(30, std::strlen(friendly)));
|
|
(void)remote;
|
|
return dib;
|
|
}
|
|
|
|
std::vector<uint8_t> GatewayKnxTpIpRouter::buildExtendedDeviceInfoDib() const {
|
|
std::vector<uint8_t> dib(8, 0);
|
|
dib[0] = static_cast<uint8_t>(dib.size());
|
|
dib[1] = kKnxDibExtendedDeviceInfo;
|
|
dib[2] = 0x01;
|
|
dib[3] = 0x00;
|
|
WriteBe16(dib.data() + 4, 254);
|
|
WriteBe16(dib.data() + 6,
|
|
advertisedMedium() == kKnxMediumIp ? kKnxIpOnlyDeviceDescriptor
|
|
: kKnxTpIpInterfaceDeviceDescriptor);
|
|
return dib;
|
|
}
|
|
|
|
std::vector<uint8_t> GatewayKnxTpIpRouter::buildIpConfigDib(const sockaddr_in& remote,
|
|
bool current) const {
|
|
const auto netif = SelectKnxNetifForRemote(remote);
|
|
const uint32_t address = netif.has_value() ? netif->address : htonl(INADDR_ANY);
|
|
const uint32_t netmask = netif.has_value() ? netif->netmask : htonl(INADDR_ANY);
|
|
const uint32_t gateway = netif.has_value() ? netif->gateway : htonl(INADDR_ANY);
|
|
std::vector<uint8_t> dib(current ? 20 : 16, 0);
|
|
dib[0] = static_cast<uint8_t>(dib.size());
|
|
dib[1] = current ? kKnxDibCurrentIpConfig : kKnxDibIpConfig;
|
|
WriteIp(dib.data() + 2, address);
|
|
WriteIp(dib.data() + 6, netmask);
|
|
WriteIp(dib.data() + 10, gateway);
|
|
if (current) {
|
|
WriteIp(dib.data() + 14, htonl(INADDR_ANY));
|
|
dib[18] = kKnxIpAssignmentManual;
|
|
dib[19] = 0x00;
|
|
} else {
|
|
dib[14] = kKnxIpCapabilityManual;
|
|
dib[15] = kKnxIpAssignmentManual;
|
|
}
|
|
return dib;
|
|
}
|
|
|
|
std::vector<uint8_t> GatewayKnxTpIpRouter::buildKnxAddressesDib() const {
|
|
std::vector<uint8_t> dib(4 + kMaxTunnelClients * 2U, 0);
|
|
dib[0] = static_cast<uint8_t>(dib.size());
|
|
dib[1] = kKnxDibKnxAddresses;
|
|
WriteBe16(dib.data() + 2, effectiveIpInterfaceIndividualAddress());
|
|
size_t offset = 4;
|
|
for (size_t slot = 0; slot < kMaxTunnelClients; ++slot) {
|
|
WriteBe16(dib.data() + offset, effectiveTunnelAddressForSlot(slot));
|
|
offset += 2;
|
|
}
|
|
return dib;
|
|
}
|
|
|
|
std::vector<uint8_t> GatewayKnxTpIpRouter::buildTunnelingInfoDib() const {
|
|
std::vector<uint8_t> dib(4 + kMaxTunnelClients * 4U, 0);
|
|
dib[0] = static_cast<uint8_t>(dib.size());
|
|
dib[1] = kKnxDibTunnellingInfo;
|
|
WriteBe16(dib.data() + 2, 254);
|
|
size_t offset = 4;
|
|
for (size_t slot = 0; slot < kMaxTunnelClients; ++slot) {
|
|
const uint16_t address = effectiveTunnelAddressForSlot(slot);
|
|
bool used = false;
|
|
for (const auto& client : tunnel_clients_) {
|
|
if (client.connected && client.individual_address == address) {
|
|
used = true;
|
|
break;
|
|
}
|
|
}
|
|
uint16_t flags = 0xffff;
|
|
if (used) {
|
|
flags = static_cast<uint16_t>(flags & ~0x0001U);
|
|
flags = static_cast<uint16_t>(flags & ~0x0004U);
|
|
}
|
|
WriteBe16(dib.data() + offset, address);
|
|
WriteBe16(dib.data() + offset + 2, flags);
|
|
offset += 4;
|
|
}
|
|
return dib;
|
|
}
|
|
|
|
std::vector<uint8_t> GatewayKnxTpIpRouter::buildSupportedServiceDib() const {
|
|
std::vector<std::pair<uint8_t, uint8_t>> services{
|
|
{kKnxServiceFamilyCore, 2},
|
|
{kKnxServiceFamilyDeviceManagement, 1},
|
|
};
|
|
if (config_.tunnel_enabled) {
|
|
services.emplace_back(kKnxServiceFamilyTunnelling, 1);
|
|
}
|
|
if (config_.multicast_enabled) {
|
|
services.emplace_back(kKnxServiceFamilyRouting, 1);
|
|
}
|
|
std::vector<uint8_t> dib(2 + services.size() * 2U, 0);
|
|
dib[0] = static_cast<uint8_t>(dib.size());
|
|
dib[1] = kKnxDibSupportedServices;
|
|
size_t offset = 2;
|
|
for (const auto& service : services) {
|
|
dib[offset++] = service.first;
|
|
dib[offset++] = service.second;
|
|
}
|
|
return dib;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) {
|
|
if (data == nullptr || len == 0) {
|
|
return;
|
|
}
|
|
for (auto& client : tunnel_clients_) {
|
|
if (client.connected) {
|
|
sendTunnelIndicationToClient(client, data, len);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data,
|
|
size_t len) {
|
|
sendCemiFrameToClient(client, kServiceTunnellingRequest, data, len);
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t service,
|
|
const uint8_t* data, size_t len) {
|
|
if (!client.connected || data == nullptr || len == 0) {
|
|
return false;
|
|
}
|
|
std::vector<uint8_t> frame_data(data, data + len);
|
|
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
|
|
if (!frame.valid()) {
|
|
ESP_LOGW(kTag, "not sending invalid OpenKNX cEMI service=0x%04x len=%u to %s",
|
|
static_cast<unsigned>(service), static_cast<unsigned>(len),
|
|
EndpointString(client.data_remote).c_str());
|
|
return false;
|
|
}
|
|
KnxIpTunnelingRequest request(frame);
|
|
request.serviceTypeIdentifier(service);
|
|
request.connectionHeader().length(LEN_CH);
|
|
request.connectionHeader().channelId(client.channel_id);
|
|
const auto message_code = CemiMessageCode(data, len);
|
|
const uint8_t send_sequence = client.send_sequence++;
|
|
request.connectionHeader().sequenceCounter(send_sequence);
|
|
request.connectionHeader().status(kKnxNoError);
|
|
const std::vector<uint8_t> packet(request.data(), request.data() + request.totalLength());
|
|
if (!sendPacketToTunnelClient(client, packet)) {
|
|
ESP_LOGW(kTag, "failed to send KNXnet/IP cEMI service=0x%04x channel=%u seq=%u to %s",
|
|
static_cast<unsigned>(service), static_cast<unsigned>(client.channel_id),
|
|
static_cast<unsigned>(request.connectionHeader().sequenceCounter()),
|
|
EndpointString(client.data_remote).c_str());
|
|
return false;
|
|
}
|
|
if (service == kServiceTunnellingRequest && message_code.has_value() &&
|
|
message_code.value() == L_data_con) {
|
|
client.last_tunnel_confirmation_sequence = send_sequence;
|
|
client.last_tunnel_confirmation_packet = packet;
|
|
}
|
|
ESP_LOGI(kTag, "sent KNXnet/IP cEMI service=0x%04x channel=%u seq=%u cemi=0x%02x len=%u to %s",
|
|
static_cast<unsigned>(service), static_cast<unsigned>(client.channel_id),
|
|
static_cast<unsigned>(request.connectionHeader().sequenceCounter()), static_cast<unsigned>(data[0]),
|
|
static_cast<unsigned>(len), EndpointString(client.data_remote).c_str());
|
|
return true;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
|
|
const sockaddr_in& remote) {
|
|
KnxIpStateResponse response(channel_id, status);
|
|
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
|
|
sendPacket(packet, remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendDisconnectResponse(uint8_t channel_id, uint8_t status,
|
|
const sockaddr_in& remote) {
|
|
KnxIpDisconnectResponse response(channel_id, status);
|
|
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
|
|
sendPacket(packet, remote);
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t status,
|
|
const sockaddr_in& remote,
|
|
uint8_t connection_type,
|
|
uint16_t tunnel_address) {
|
|
if (status != kKnxNoError) {
|
|
KnxIpConnectResponse response(channel_id, status);
|
|
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
|
|
sendPacket(packet, remote);
|
|
ESP_LOGI(kTag, "sent KNXnet/IP connect error channel=%u status=0x%02x to %s",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(status),
|
|
EndpointString(remote).c_str());
|
|
return;
|
|
}
|
|
const auto netif = SelectKnxNetifForRemote(remote);
|
|
if (!netif.has_value() || knx_ip_parameters_ == nullptr) {
|
|
ESP_LOGW(kTag, "cannot accept KNXnet/IP connect from %s: no active IPv4 interface",
|
|
EndpointString(remote).c_str());
|
|
KnxIpConnectResponse response(channel_id, kKnxErrorConnectionType);
|
|
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
|
|
sendPacket(packet, remote);
|
|
return;
|
|
}
|
|
KnxIpConnectResponse response(*knx_ip_parameters_, tunnel_address, config_.udp_port,
|
|
channel_id, connection_type);
|
|
const bool tcp = currentTransportAllowsTcpHpai();
|
|
const uint32_t endpoint_address = ntohl(netif->address);
|
|
response.controlEndpoint().code(tcp ? IPV4_TCP : IPV4_UDP);
|
|
response.controlEndpoint().ipAddress(tcp ? 0 : endpoint_address);
|
|
response.controlEndpoint().ipPortNumber(tcp ? 0 : config_.udp_port);
|
|
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
|
|
sendPacket(packet, remote);
|
|
std::string endpoint_string;
|
|
if (tcp) {
|
|
endpoint_string = "0.0.0.0:0 (TCP HPAI)";
|
|
} else {
|
|
sockaddr_in local_endpoint{};
|
|
local_endpoint.sin_family = AF_INET;
|
|
local_endpoint.sin_port = htons(config_.udp_port);
|
|
local_endpoint.sin_addr.s_addr = netif->address;
|
|
endpoint_string = EndpointString(local_endpoint);
|
|
}
|
|
ESP_LOGI(kTag, "sent KNXnet/IP connect response channel=%u type=0x%02x to %s endpoint=%s",
|
|
static_cast<unsigned>(channel_id), static_cast<unsigned>(connection_type),
|
|
EndpointString(remote).c_str(), endpoint_string.c_str());
|
|
}
|
|
|
|
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());
|
|
std::vector<uint8_t> frame_data(data, data + len);
|
|
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
|
|
if (!frame.valid()) {
|
|
ESP_LOGW(kTag, "not sending invalid OpenKNX routing cEMI len=%u",
|
|
static_cast<unsigned>(len));
|
|
return;
|
|
}
|
|
KnxIpRoutingIndication routing(frame);
|
|
const std::vector<uint8_t> packet(routing.data(), routing.data() + routing.totalLength());
|
|
const auto netifs = ActiveKnxNetifs();
|
|
if (netifs.empty()) {
|
|
SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
|
return;
|
|
}
|
|
for (const auto& netif : netifs) {
|
|
in_addr multicast_interface{};
|
|
multicast_interface.s_addr = netif.address;
|
|
if (setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_IF, &multicast_interface,
|
|
sizeof(multicast_interface)) < 0) {
|
|
ESP_LOGW(kTag, "failed to select KNX multicast interface %s %s: errno=%d (%s)",
|
|
netif.key, Ipv4String(netif.address).c_str(), errno, std::strerror(errno));
|
|
continue;
|
|
}
|
|
SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
|
}
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::selectOpenKnxNetworkInterface(const sockaddr_in& remote) {
|
|
const auto netif = SelectKnxNetifForRemote(remote);
|
|
SemaphoreGuard guard(openknx_lock_);
|
|
if (ets_device_ != nullptr) {
|
|
ets_device_->setNetworkInterface(netif.has_value() ? netif->netif : nullptr);
|
|
}
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len,
|
|
TunnelClient* response_client,
|
|
uint16_t response_service,
|
|
const uint8_t* suppress_routing_echo,
|
|
size_t suppress_routing_echo_len) {
|
|
SemaphoreGuard guard(openknx_lock_);
|
|
if (ets_device_ == nullptr) {
|
|
return false;
|
|
}
|
|
std::vector<uint8_t> tunnel_confirmation;
|
|
const bool needs_tunnel_confirmation =
|
|
response_client != nullptr && response_client->connected &&
|
|
response_service == kServiceTunnellingRequest &&
|
|
BuildTunnelConfirmationFrame(data, len, &tunnel_confirmation);
|
|
bool sent_tunnel_confirmation = false;
|
|
const bool consumed = ets_device_->handleTunnelFrame(
|
|
data, len,
|
|
[this, response_client, response_service, needs_tunnel_confirmation,
|
|
&tunnel_confirmation, &sent_tunnel_confirmation,
|
|
suppress_routing_echo, suppress_routing_echo_len](const uint8_t* response,
|
|
size_t response_len) {
|
|
if (response == nullptr || response_len == 0) {
|
|
return;
|
|
}
|
|
const bool routing_context =
|
|
response_client == nullptr && response_service == kServiceRoutingIndication;
|
|
const auto message_code = CemiMessageCode(response, response_len);
|
|
if (routing_context && suppress_routing_echo != nullptr &&
|
|
IsLocalRoutingEchoIndication(response, response_len, suppress_routing_echo,
|
|
suppress_routing_echo_len)) {
|
|
return;
|
|
}
|
|
if (needs_tunnel_confirmation && !sent_tunnel_confirmation &&
|
|
message_code.has_value() && message_code.value() != L_data_con) {
|
|
sent_tunnel_confirmation = sendCemiFrameToClient(
|
|
*response_client, kServiceTunnellingRequest,
|
|
tunnel_confirmation.data(), tunnel_confirmation.size());
|
|
}
|
|
|
|
const uint16_t service = KnxIpServiceForCemi(response, response_len, response_service);
|
|
if (service == kServiceDeviceConfigurationRequest) {
|
|
if (response_client != nullptr && response_client->connected) {
|
|
sendCemiFrameToClient(*response_client, service, response, response_len);
|
|
} else if (routing_context) {
|
|
sendRoutingIndication(response, response_len);
|
|
}
|
|
return;
|
|
}
|
|
if (message_code.has_value() && message_code.value() == L_data_con) {
|
|
if (routing_context) {
|
|
return;
|
|
}
|
|
if (response_client != nullptr && response_client->connected) {
|
|
sent_tunnel_confirmation =
|
|
sendCemiFrameToClient(*response_client, service, response, response_len) ||
|
|
sent_tunnel_confirmation;
|
|
}
|
|
return;
|
|
}
|
|
if (routing_context) {
|
|
sendRoutingIndication(response, response_len);
|
|
return;
|
|
}
|
|
sendTunnelIndication(response, response_len);
|
|
});
|
|
if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) {
|
|
sendCemiFrameToClient(*response_client, kServiceTunnellingRequest,
|
|
tunnel_confirmation.data(), tunnel_confirmation.size());
|
|
}
|
|
syncOpenKnxConfigFromDevice();
|
|
return consumed;
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::transmitOpenKnxTpFrame(const uint8_t* data, size_t len) {
|
|
SemaphoreGuard guard(openknx_lock_);
|
|
if (ets_device_ == nullptr) {
|
|
return false;
|
|
}
|
|
const bool sent = ets_device_->transmitTpFrame(data, len);
|
|
tp_uart_online_ = ets_device_->tpUartOnline();
|
|
return sent;
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::handleOpenKnxBusFrame(const uint8_t* data, size_t len) {
|
|
SemaphoreGuard guard(openknx_lock_);
|
|
if (ets_device_ == nullptr) {
|
|
return false;
|
|
}
|
|
const bool consumed = ets_device_->handleBusFrame(data, len);
|
|
syncOpenKnxConfigFromDevice();
|
|
return consumed;
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::routeOpenKnxGroupWrite(const uint8_t* data, size_t len,
|
|
const char* context) {
|
|
const auto decoded = DecodeOpenKnxGroupWrite(data, len);
|
|
if (!decoded.has_value()) {
|
|
return false;
|
|
}
|
|
if (!shouldRouteDaliApplicationFrames()) {
|
|
return true;
|
|
}
|
|
const DaliBridgeResult result = group_write_handler_
|
|
? group_write_handler_(decoded->group_address,
|
|
decoded->data.data(),
|
|
decoded->data.size())
|
|
: bridge_.handleGroupWrite(decoded->group_address,
|
|
decoded->data.data(),
|
|
decoded->data.size());
|
|
if (!result.ok && !result.error.empty()) {
|
|
ESP_LOGD(kTag, "%s not routed to DALI: %s", context == nullptr ? "KNX group write" : context,
|
|
result.error.c_str());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::emitOpenKnxGroupValue(uint16_t group_object_number,
|
|
const uint8_t* data, size_t len) {
|
|
SemaphoreGuard guard(openknx_lock_);
|
|
if (ets_device_ == nullptr) {
|
|
return false;
|
|
}
|
|
const bool emitted = ets_device_->emitGroupValue(
|
|
group_object_number, data, len, [this](const uint8_t* frame_data, size_t frame_len) {
|
|
sendRoutingIndication(frame_data, frame_len);
|
|
sendTunnelIndication(frame_data, frame_len);
|
|
if (ets_device_ != nullptr) {
|
|
const bool sent_to_tp = ets_device_->transmitTpFrame(frame_data, frame_len);
|
|
tp_uart_online_ = sent_to_tp || ets_device_->tpUartOnline();
|
|
}
|
|
});
|
|
syncOpenKnxConfigFromDevice();
|
|
return emitted;
|
|
}
|
|
|
|
bool GatewayKnxTpIpRouter::shouldRouteDaliApplicationFrames() const {
|
|
if (!commissioning_only_) {
|
|
return true;
|
|
}
|
|
return openknx_configured_.load();
|
|
}
|
|
|
|
uint8_t GatewayKnxTpIpRouter::advertisedMedium() const {
|
|
return (config_.tunnel_enabled || tp_uart_online_) ? kKnxMediumTp1 : kKnxMediumIp;
|
|
}
|
|
|
|
void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() {
|
|
if (ets_device_ == nullptr) {
|
|
return;
|
|
}
|
|
const auto snapshot = ets_device_->snapshot();
|
|
openknx_configured_.store(snapshot.configured);
|
|
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::effectiveIpInterfaceIndividualAddress() const {
|
|
if (config_.ip_interface_individual_address != 0 &&
|
|
config_.ip_interface_individual_address != 0xffff) {
|
|
return config_.ip_interface_individual_address;
|
|
}
|
|
return 0xff01;
|
|
}
|
|
|
|
uint16_t GatewayKnxTpIpRouter::effectiveKnxDeviceIndividualAddress() 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 {
|
|
const uint16_t interface_address = effectiveIpInterfaceIndividualAddress();
|
|
uint16_t device = static_cast<uint16_t>((interface_address & 0x00ff) + 1);
|
|
if (device == 0 || device > 0xff) {
|
|
device = 1;
|
|
}
|
|
uint16_t address = static_cast<uint16_t>((interface_address & 0xff00) | device);
|
|
if (address == 0xffff) {
|
|
address = static_cast<uint16_t>((interface_address & 0xff00) | 0x0001);
|
|
}
|
|
return address;
|
|
}
|
|
|
|
} // namespace gateway
|