Add OpenKNX IDF component with TPUart integration

- Created CMakeLists.txt for the OpenKNX IDF component, ensuring dependencies on OpenKNX and TPUart submodules.
- Implemented Arduino compatibility header for basic functions like millis, delay, pinMode, and digitalRead.
- Developed EspIdfPlatform class for network interface management and multicast communication.
- Added EtsMemoryLoader for loading ETS memory snapshots and managing associations.
- Introduced TpuartUartInterface for UART communication with methods for reading, writing, and managing callbacks.
- Implemented arduino_compat.cpp for Arduino-like functionality on ESP-IDF.
- Created source files for platform and memory loader implementations.
- Updated submodules for knx, knx_dali_gw, and tpuart.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-11 07:05:40 +08:00
parent 1b8753636f
commit 70367f53ca
20 changed files with 1359 additions and 20 deletions
@@ -0,0 +1,180 @@
#include "Arduino.h"
#include "driver/gpio.h"
#include "esp_err.h"
#include "esp_rom_sys.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <array>
#include <cstdio>
namespace {
std::array<voidFuncPtr, GPIO_NUM_MAX> g_gpio_callbacks{};
bool g_isr_service_installed = false;
void IRAM_ATTR gpioIsrThunk(void* arg) {
const auto pin = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(arg));
if (pin < g_gpio_callbacks.size() && g_gpio_callbacks[pin] != nullptr) {
g_gpio_callbacks[pin]();
}
}
gpio_int_type_t toGpioInterrupt(uint32_t mode) {
switch (mode) {
case RISING:
return GPIO_INTR_POSEDGE;
case FALLING:
return GPIO_INTR_NEGEDGE;
case CHANGE:
return GPIO_INTR_ANYEDGE;
default:
return GPIO_INTR_DISABLE;
}
}
void printUnsigned(unsigned long long value, int base) {
if (base == HEX) {
std::printf("%llX", value);
} else {
std::printf("%llu", value);
}
}
void printSigned(long long value, int base) {
if (base == HEX) {
std::printf("%llX", static_cast<unsigned long long>(value));
} else {
std::printf("%lld", value);
}
}
} // namespace
uint32_t millis() { return static_cast<uint32_t>(esp_timer_get_time() / 1000ULL); }
uint32_t micros() { return static_cast<uint32_t>(esp_timer_get_time()); }
void delay(uint32_t millis) { vTaskDelay(pdMS_TO_TICKS(millis)); }
void delayMicroseconds(unsigned int howLong) { esp_rom_delay_us(howLong); }
void pinMode(uint32_t pin, uint32_t mode) {
if (pin >= GPIO_NUM_MAX) {
return;
}
gpio_config_t config{};
config.pin_bit_mask = 1ULL << pin;
config.mode = mode == OUTPUT ? GPIO_MODE_OUTPUT : GPIO_MODE_INPUT;
config.pull_up_en = mode == INPUT_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE;
config.pull_down_en = mode == INPUT_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE;
config.intr_type = GPIO_INTR_DISABLE;
gpio_config(&config);
}
void digitalWrite(uint32_t pin, uint32_t value) {
if (pin < GPIO_NUM_MAX) {
gpio_set_level(static_cast<gpio_num_t>(pin), value == LOW ? 0 : 1);
}
}
uint32_t digitalRead(uint32_t pin) {
if (pin >= GPIO_NUM_MAX) {
return LOW;
}
return gpio_get_level(static_cast<gpio_num_t>(pin)) == 0 ? LOW : HIGH;
}
void attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode) {
if (pin >= GPIO_NUM_MAX) {
return;
}
if (!g_isr_service_installed) {
const esp_err_t err = gpio_install_isr_service(ESP_INTR_FLAG_IRAM);
g_isr_service_installed = err == ESP_OK || err == ESP_ERR_INVALID_STATE;
}
if (!g_isr_service_installed) {
return;
}
gpio_set_intr_type(static_cast<gpio_num_t>(pin), toGpioInterrupt(mode));
gpio_isr_handler_remove(static_cast<gpio_num_t>(pin));
g_gpio_callbacks[pin] = callback;
if (callback != nullptr) {
gpio_isr_handler_add(static_cast<gpio_num_t>(pin), gpioIsrThunk,
reinterpret_cast<void*>(static_cast<uintptr_t>(pin)));
}
}
void print(const char value[]) { std::printf("%s", value == nullptr ? "" : value); }
void print(char value) { std::printf("%c", value); }
void print(unsigned char value, int base) { printUnsigned(value, base); }
void print(int value, int base) { printSigned(value, base); }
void print(unsigned int value, int base) { printUnsigned(value, base); }
void print(long value, int base) { printSigned(value, base); }
void print(unsigned long value, int base) { printUnsigned(value, base); }
void print(long long value, int base) { printSigned(value, base); }
void print(unsigned long long value, int base) { printUnsigned(value, base); }
void print(double value) { std::printf("%f", value); }
void println(const char value[]) {
print(value);
std::printf("\n");
}
void println(char value) {
print(value);
std::printf("\n");
}
void println(unsigned char value, int base) {
print(value, base);
std::printf("\n");
}
void println(int value, int base) {
print(value, base);
std::printf("\n");
}
void println(unsigned int value, int base) {
print(value, base);
std::printf("\n");
}
void println(long value, int base) {
print(value, base);
std::printf("\n");
}
void println(unsigned long value, int base) {
print(value, base);
std::printf("\n");
}
void println(long long value, int base) {
print(value, base);
std::printf("\n");
}
void println(unsigned long long value, int base) {
print(value, base);
std::printf("\n");
}
void println(double value) {
print(value);
std::printf("\n");
}
void println(void) { std::printf("\n"); }
@@ -0,0 +1,273 @@
#include "openknx_idf/esp_idf_platform.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "lwip/inet.h"
#include "nvs.h"
#include "nvs_flash.h"
#include <algorithm>
#include <cerrno>
#include <cstring>
#include <unistd.h>
namespace gateway::openknx {
namespace {
constexpr const char* kTag = "openknx_idf";
constexpr const char* kEepromKey = "eeprom";
esp_netif_t* findDefaultNetif() {
if (auto* sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF")) {
return sta;
}
if (auto* eth = esp_netif_get_handle_from_ifkey("ETH_DEF")) {
return eth;
}
return nullptr;
}
bool ensureNvsReady() {
const esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
if (nvs_flash_erase() != ESP_OK) {
return false;
}
return nvs_flash_init() == ESP_OK;
}
return err == ESP_OK || err == ESP_ERR_INVALID_STATE;
}
} // namespace
EspIdfPlatform::EspIdfPlatform(TPUart::Interface::Abstract* interface,
const char* nvs_namespace)
: nvs_namespace_(nvs_namespace == nullptr ? "openknx" : nvs_namespace) {
this->interface(interface);
}
EspIdfPlatform::~EspIdfPlatform() { closeMultiCast(); }
void EspIdfPlatform::networkInterface(esp_netif_t* netif) { netif_ = netif; }
esp_netif_t* EspIdfPlatform::networkInterface() const { return netif_; }
esp_netif_t* EspIdfPlatform::effectiveNetif() const {
return netif_ == nullptr ? findDefaultNetif() : netif_;
}
uint32_t EspIdfPlatform::currentIpAddress() {
esp_netif_ip_info_t ip_info{};
esp_netif_t* netif = effectiveNetif();
if (netif == nullptr || esp_netif_get_ip_info(netif, &ip_info) != ESP_OK) {
return 0;
}
return ip_info.ip.addr;
}
uint32_t EspIdfPlatform::currentSubnetMask() {
esp_netif_ip_info_t ip_info{};
esp_netif_t* netif = effectiveNetif();
if (netif == nullptr || esp_netif_get_ip_info(netif, &ip_info) != ESP_OK) {
return 0;
}
return ip_info.netmask.addr;
}
uint32_t EspIdfPlatform::currentDefaultGateway() {
esp_netif_ip_info_t ip_info{};
esp_netif_t* netif = effectiveNetif();
if (netif == nullptr || esp_netif_get_ip_info(netif, &ip_info) != ESP_OK) {
return 0;
}
return ip_info.gw.addr;
}
void EspIdfPlatform::macAddress(uint8_t* data) {
if (data == nullptr) {
return;
}
if (esp_read_mac(data, ESP_MAC_WIFI_STA) != ESP_OK) {
std::memset(data, 0, 6);
}
}
uint32_t EspIdfPlatform::uniqueSerialNumber() {
uint8_t mac[6]{};
macAddress(mac);
return (static_cast<uint32_t>(mac[0]) << 24) | (static_cast<uint32_t>(mac[1]) << 16) |
(static_cast<uint32_t>(mac[4]) << 8) | mac[5];
}
void EspIdfPlatform::restart() { esp_restart(); }
void EspIdfPlatform::fatalError() {
ESP_LOGE(kTag, "OpenKNX fatal error");
while (true) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void EspIdfPlatform::setupMultiCast(uint32_t addr, uint16_t port) {
closeMultiCast();
udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (udp_sock_ < 0) {
ESP_LOGE(kTag, "failed to create UDP socket: errno=%d", errno);
return;
}
int reuse = 1;
setsockopt(udp_sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
sockaddr_in bind_addr{};
bind_addr.sin_family = AF_INET;
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind_addr.sin_port = htons(port);
if (bind(udp_sock_, reinterpret_cast<sockaddr*>(&bind_addr), sizeof(bind_addr)) < 0) {
ESP_LOGE(kTag, "failed to bind UDP socket: errno=%d", errno);
closeMultiCast();
return;
}
timeval timeout{};
timeout.tv_sec = 0;
timeout.tv_usec = 1000;
setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
ip_mreq mreq{};
mreq.imr_multiaddr.s_addr = htonl(addr);
mreq.imr_interface.s_addr = currentIpAddress();
if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
ESP_LOGW(kTag, "failed to join KNX multicast group: errno=%d", errno);
}
uint8_t loop = 0;
setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop));
multicast_remote_ = {};
multicast_remote_.sin_family = AF_INET;
multicast_remote_.sin_addr.s_addr = htonl(addr);
multicast_remote_.sin_port = htons(port);
}
void EspIdfPlatform::closeMultiCast() {
if (udp_sock_ >= 0) {
shutdown(udp_sock_, SHUT_RDWR);
close(udp_sock_);
udp_sock_ = -1;
}
has_last_remote_ = false;
}
bool EspIdfPlatform::sendBytesMultiCast(uint8_t* buffer, uint16_t len) {
if (udp_sock_ < 0 || buffer == nullptr || len == 0) {
return false;
}
const int sent = sendto(udp_sock_, buffer, len, 0, reinterpret_cast<sockaddr*>(&multicast_remote_),
sizeof(multicast_remote_));
return sent == len;
}
int EspIdfPlatform::readBytesMultiCast(uint8_t* buffer, uint16_t maxLen) {
uint32_t src_addr = 0;
uint16_t src_port = 0;
return readBytesMultiCast(buffer, maxLen, src_addr, src_port);
}
int EspIdfPlatform::readBytesMultiCast(uint8_t* buffer, uint16_t maxLen, uint32_t& src_addr,
uint16_t& src_port) {
if (udp_sock_ < 0 || buffer == nullptr || maxLen == 0) {
return 0;
}
sockaddr_in remote{};
socklen_t remote_len = sizeof(remote);
const int len = recvfrom(udp_sock_, buffer, maxLen, 0, reinterpret_cast<sockaddr*>(&remote),
&remote_len);
if (len <= 0) {
return 0;
}
last_remote_ = remote;
has_last_remote_ = true;
src_addr = ntohl(remote.sin_addr.s_addr);
src_port = ntohs(remote.sin_port);
return len;
}
bool EspIdfPlatform::sendBytesUniCast(uint32_t addr, uint16_t port, uint8_t* buffer,
uint16_t len) {
if (udp_sock_ < 0 || buffer == nullptr || len == 0) {
return false;
}
sockaddr_in remote{};
if (addr == 0 && port == 0 && has_last_remote_) {
remote = last_remote_;
} else {
remote.sin_family = AF_INET;
remote.sin_addr.s_addr = htonl(addr);
remote.sin_port = htons(port);
}
const int sent = sendto(udp_sock_, buffer, len, 0, reinterpret_cast<sockaddr*>(&remote),
sizeof(remote));
return sent == len;
}
void EspIdfPlatform::loadEeprom(size_t size) {
if (eeprom_loaded_ && eeprom_.size() == size) {
return;
}
eeprom_.assign(size, 0xff);
eeprom_loaded_ = true;
if (!ensureNvsReady()) {
ESP_LOGW(kTag, "NVS is not ready for OpenKNX EEPROM load");
return;
}
nvs_handle_t handle = 0;
if (nvs_open(nvs_namespace_.c_str(), NVS_READONLY, &handle) != ESP_OK) {
return;
}
size_t stored_size = 0;
if (nvs_get_blob(handle, kEepromKey, nullptr, &stored_size) == ESP_OK && stored_size > 0) {
std::vector<uint8_t> stored(stored_size);
if (nvs_get_blob(handle, kEepromKey, stored.data(), &stored_size) == ESP_OK) {
std::memcpy(eeprom_.data(), stored.data(), std::min(eeprom_.size(), stored.size()));
}
}
nvs_close(handle);
}
uint8_t* EspIdfPlatform::getEepromBuffer(uint32_t size) {
loadEeprom(size);
return eeprom_.data();
}
void EspIdfPlatform::commitToEeprom() {
if (eeprom_.empty()) {
return;
}
if (!ensureNvsReady()) {
ESP_LOGW(kTag, "NVS is not ready for OpenKNX EEPROM commit");
return;
}
nvs_handle_t handle = 0;
esp_err_t err = nvs_open(nvs_namespace_.c_str(), NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to open OpenKNX NVS namespace: %s", esp_err_to_name(err));
return;
}
err = nvs_set_blob(handle, kEepromKey, eeprom_.data(), eeprom_.size());
if (err == ESP_OK) {
err = nvs_commit(handle);
}
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to commit OpenKNX EEPROM: %s", esp_err_to_name(err));
}
nvs_close(handle);
}
} // namespace gateway::openknx
@@ -0,0 +1,50 @@
#include "openknx_idf/ets_memory_loader.h"
#include "openknx_idf/esp_idf_platform.h"
#include "knx/bau07B0.h"
#include <algorithm>
namespace gateway::openknx {
namespace {
void CollectAssociation(uint16_t group_address, uint16_t group_object_number,
void* context) {
auto* associations = static_cast<std::vector<EtsAssociation>*>(context);
if (associations == nullptr) {
return;
}
associations->push_back(EtsAssociation{group_address, group_object_number});
}
} // namespace
EtsMemorySnapshot LoadEtsMemorySnapshot(const std::string& nvs_namespace) {
EspIdfPlatform platform(nullptr, nvs_namespace.c_str());
Bau07B0 device(platform);
device.deviceObject().manufacturerId(0xfa);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
device.readMemory();
EtsMemorySnapshot snapshot;
snapshot.configured = device.configured();
device.forEachEtsAssociation(CollectAssociation, &snapshot.associations);
std::sort(snapshot.associations.begin(), snapshot.associations.end(),
[](const EtsAssociation& lhs, const EtsAssociation& rhs) {
if (lhs.group_address != rhs.group_address) {
return lhs.group_address < rhs.group_address;
}
return lhs.group_object_number < rhs.group_object_number;
});
snapshot.associations.erase(
std::unique(snapshot.associations.begin(), snapshot.associations.end(),
[](const EtsAssociation& lhs, const EtsAssociation& rhs) {
return lhs.group_address == rhs.group_address &&
lhs.group_object_number == rhs.group_object_number;
}),
snapshot.associations.end());
return snapshot;
}
} // namespace gateway::openknx
@@ -0,0 +1,114 @@
#include "openknx_idf/tpuart_uart_interface.h"
#include "esp_log.h"
#include <utility>
namespace gateway::openknx {
namespace {
constexpr const char* kTag = "openknx_tpuart";
} // namespace
TpuartUartInterface::TpuartUartInterface(uart_port_t uart_port, int tx_pin, int rx_pin,
size_t rx_buffer_size, size_t tx_buffer_size)
: uart_port_(uart_port),
tx_pin_(tx_pin),
rx_pin_(rx_pin),
rx_buffer_size_(rx_buffer_size),
tx_buffer_size_(tx_buffer_size) {}
TpuartUartInterface::~TpuartUartInterface() { end(); }
void TpuartUartInterface::begin(int baud) {
if (_running) {
end();
}
uart_config_t config{};
config.baud_rate = baud;
config.data_bits = UART_DATA_8_BITS;
config.parity = UART_PARITY_EVEN;
config.stop_bits = UART_STOP_BITS_1;
config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
config.source_clk = UART_SCLK_DEFAULT;
esp_err_t err = uart_param_config(uart_port_, &config);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to configure UART%d: %s", uart_port_, esp_err_to_name(err));
return;
}
err = uart_set_pin(uart_port_, tx_pin_ < 0 ? UART_PIN_NO_CHANGE : tx_pin_,
rx_pin_ < 0 ? UART_PIN_NO_CHANGE : rx_pin_, UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to route UART%d pins: %s", uart_port_, esp_err_to_name(err));
return;
}
err = uart_driver_install(uart_port_, rx_buffer_size_, tx_buffer_size_, 0, nullptr, 0);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to install UART%d driver: %s", uart_port_, esp_err_to_name(err));
return;
}
uart_set_rx_full_threshold(uart_port_, 1);
_running = true;
}
void TpuartUartInterface::end() {
if (!_running) {
return;
}
_running = false;
uart_driver_delete(uart_port_);
}
bool TpuartUartInterface::available() {
if (!_running) {
return false;
}
size_t len = 0;
return uart_get_buffered_data_len(uart_port_, &len) == ESP_OK && len > 0;
}
bool TpuartUartInterface::availableForWrite() {
if (!_running) {
return false;
}
size_t len = 0;
return uart_get_tx_buffer_free_size(uart_port_, &len) == ESP_OK && len > 0;
}
bool TpuartUartInterface::write(char value) {
if (!_running) {
return false;
}
return uart_write_bytes(uart_port_, &value, 1) == 1;
}
int TpuartUartInterface::read() {
if (!_running) {
return -1;
}
uint8_t value = 0;
return uart_read_bytes(uart_port_, &value, 1, 0) == 1 ? value : -1;
}
bool TpuartUartInterface::overflow() { return overflow_.exchange(false); }
void TpuartUartInterface::flush() {
if (_running) {
uart_flush(uart_port_);
}
}
bool TpuartUartInterface::hasCallback() { return false; }
void TpuartUartInterface::registerCallback(std::function<bool()> callback) {
callback_ = std::move(callback);
}
} // namespace gateway::openknx