Files
gateway/components/gateway_knx/src/gateway_knx_private.hpp
T
Tony 2b8ef31263 Add GatewayKnxTpIpRouter implementation for handling KNXnet/IP services
- Implemented handleUdpDatagram to process incoming UDP datagrams and route them to appropriate handlers based on service type.
- Added methods for handling various KNXnet/IP requests including search, description, tunneling, device configuration, connection state, and disconnect requests.
- Introduced TunnelClient management for handling multiple tunnel connections, including allocation, resetting, and pruning stale clients.
- Implemented secure service handling with appropriate logging for unsupported secure sessions.
- Enhanced logging for better traceability of incoming requests and responses.

Signed-off-by: Tony <tonylu@tony-cloud.com>
2026-05-21 14:12:46 +08:00

1171 lines
42 KiB
C++

#pragma once
#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/interface_object.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>
#if defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-function"
#endif
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;
constexpr uint32_t kCommissioningScanTaskStackSize = 8192;
constexpr uint16_t kGroupObjectTableObjectType = OT_GRP_OBJ_TABLE;
constexpr uint8_t kPidGoDiagnostics = 0x42;
constexpr uint8_t kGoDiagnosticsReservedByte = 0x00;
constexpr uint8_t kGoDiagnosticsGroupWriteService = 0x01;
constexpr uint8_t kGoDiagnosticsCompactPayloadFlag = 0x80;
struct DecodedGroupWrite {
uint16_t group_address{0};
std::vector<uint8_t> data;
};
struct DecodedGoDiagnosticsGroupWrite {
uint16_t group_address{0};
const uint8_t* payload{nullptr};
size_t payload_len{0};
};
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;
}
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));
}
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;
}
std::optional<DecodedGoDiagnosticsGroupWrite> DecodeGoDiagnosticsGroupWrite(
const uint8_t* data, size_t len) {
if (data == nullptr || len < 5) {
return std::nullopt;
}
if (data[0] != kGoDiagnosticsReservedByte || data[1] != kGoDiagnosticsGroupWriteService) {
return std::nullopt;
}
const uint8_t encoded_length = data[2];
size_t payload_len = 0;
if ((encoded_length & kGoDiagnosticsCompactPayloadFlag) != 0) {
payload_len = static_cast<size_t>(encoded_length & ~kGoDiagnosticsCompactPayloadFlag);
if (payload_len == 0 || len != payload_len + 5) {
return std::nullopt;
}
} else {
const size_t expanded_length = encoded_length;
if (expanded_length < 2 || len != expanded_length + 3) {
return std::nullopt;
}
payload_len = expanded_length - 2;
}
DecodedGoDiagnosticsGroupWrite out;
out.group_address = ReadBe16(data + 3);
out.payload = data + 5;
out.payload_len = payload_len;
return out;
}
bool IsOpenKnxGroupValueWrite(const uint8_t* data, size_t len) {
return DecodeOpenKnxGroupWrite(data, len).has_value();
}
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()) {
ESP_LOGD(kTag, "DALI query failed seq=%s addr=0x%02x cmd=0x%02x error=%s",
sequence == nullptr ? "" : sequence, static_cast<unsigned>(addr),
static_cast<unsigned>(cmd),
result.error.empty() ? "no response" : result.error.c_str());
return std::nullopt;
}
ESP_LOGD(kTag, "DALI query result seq=%s addr=0x%02x cmd=0x%02x data=0x%02x",
sequence == nullptr ? "" : sequence, static_cast<unsigned>(addr),
static_cast<unsigned>(cmd),
static_cast<unsigned>(result.data.value() & 0xff));
return result.data.value();
}
bool DaliQueryResultHasStatus(const DaliBridgeResult& result, const char* status) {
if (status == nullptr) return false;
const auto it = result.metadata.find("queryStatus");
if (it == result.metadata.end()) return false;
const auto value = it->second.asString();
return value.has_value() && value.value() == status;
}
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);
}
uint8_t GoDiagnosticsReturnCode(const DaliBridgeResult& result) {
if (result.ok) {
return ReturnCodes::Success;
}
if (getObjectBool(result.metadata, "ignored").value_or(false)) {
return ReturnCodes::TemporarilyNotAvailable;
}
const std::string& error = result.error;
if (error.find("unmapped") != std::string::npos ||
error.find("does not match gateway config") != std::string::npos) {
return ReturnCodes::AddressVoid;
}
if (error.find("disabled") != std::string::npos ||
error.find("commissioning-only") != std::string::npos ||
error.find("not configured") != std::string::npos) {
return ReturnCodes::TemporarilyNotAvailable;
}
return ReturnCodes::GenericError;
}
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 result = ExecuteRaw(engine, BridgeOperation::query, DALI_CMD_SPECIAL_COMPARE,
DALI_CMD_OFF, sequence);
if (result.ok && result.data.has_value()) {
return true;
}
if (DaliQueryResultHasStatus(result, "noResponse") ||
DaliQueryResultHasStatus(result, "timeout")) {
ESP_LOGD(kTag, "DALI compare no match seq=%s search=0x%06x status=%s",
sequence == nullptr ? "" : sequence, static_cast<unsigned>(search_address),
DaliQueryResultHasStatus(result, "timeout") ? "timeout" : "noResponse");
return false;
}
if (!result.error.empty()) {
ESP_LOGW(kTag, "DALI compare failed seq=%s search=0x%06x error=%s",
sequence == nullptr ? "" : sequence, static_cast<unsigned>(search_address),
result.error.c_str());
}
if (!result.ok || !result.data.has_value()) {
return std::nullopt;
}
return true;
}
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
} // namespace gateway
#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif