diff --git a/README.md b/README.md index c1c7bfc..362ee9d 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ The native rewrite now wires a shared `gateway_core` bootstrap component, a mult KNX Data Secure and KNXnet/IP Secure support are controlled by `GATEWAY_KNX_DATA_SECURE_SUPPORTED` and `GATEWAY_KNX_IP_SECURE_SUPPORTED`. The current KNXnet/IP Secure flag reserves and reports secure service capability, while runtime secure-session transport is still reported as not implemented until that path is wired. The gateway derives its KNX serial identity from the ESP base MAC, and the development factory setup key is deterministically derived from that KNX serial so the same board keeps the same FDSK across NVS erases. -The KNXnet/IP tunnel can start from the built-in default configuration before any ETS download. KNX TP-UART is enabled only when `GATEWAY_KNX_TP_UART_PORT` is `0`, `1`, or `2`; set that UART port to `-1` for IP-only operation. UART TX/RX GPIO values of `-1` mean use the ESP-IDF default IO routing for that UART, not disabled. Non-UART GPIO options use `-1` as disabled, including the KNX programming button, KNX programming LED, setup AP button, Wi-Fi reset button, and status LED. +The KNXnet/IP tunnel can start from the built-in default configuration before any ETS download. KNX TP-UART is enabled only when `GATEWAY_KNX_TP_UART_PORT` is `0`, `1`, or `2`; set that UART port to `-1` for IP-only operation. UART TX/RX GPIO values of `-1` mean use the ESP-IDF target default pins for that UART, not disabled. `GATEWAY_KNX_TP_UART_9BIT_MODE` enables the NCN5120/OpenKNX-style 9-bit host frame on the wire, represented on ESP-IDF as 8 data bits plus even parity. Non-UART GPIO options use `-1` as disabled, including the KNX programming button, KNX programming LED, setup AP button, Wi-Fi reset button, and status LED. When no KNX bridge config or ETS application data has been downloaded, the KNXnet/IP router starts in commissioning mode: OpenKNX receives tunnel programming traffic from ETS, while DALI group routing and REG1-Dali function-property actions stay inactive until ETS reports a configured application. diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index 8f3605d..e470564 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -772,6 +772,16 @@ config GATEWAY_KNX_TP_BAUDRATE range 1200 921600 default 19200 +config GATEWAY_KNX_TP_UART_9BIT_MODE + bool "KNX TP UART 9-bit mode" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + default y + help + Enable the NCN5120/OpenKNX-style 9-bit UART frame on the wire. ESP-IDF + exposes this as 8 data bits plus even parity, matching the TP-UART host + mode commonly described as 19200 baud 9-bit UART. Disable only for + hardware wired for 8N1 host UART mode. + config GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE int "KNX/IP bridge task stack bytes" depends on GATEWAY_KNX_BRIDGE_SUPPORTED diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index d299e93..f3b6b69 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -886,6 +886,11 @@ extern "C" void app_main(void) { default_knx.tp_uart.tx_pin = CONFIG_GATEWAY_KNX_TP_TX_PIN; default_knx.tp_uart.rx_pin = CONFIG_GATEWAY_KNX_TP_RX_PIN; default_knx.tp_uart.baudrate = static_cast(CONFIG_GATEWAY_KNX_TP_BAUDRATE); + #ifdef CONFIG_GATEWAY_KNX_TP_UART_9BIT_MODE + default_knx.tp_uart.nine_bit_mode = true; + #else + default_knx.tp_uart.nine_bit_mode = false; + #endif bridge_config.default_knx_config = default_knx; } bridge_config.knx_task_stack_size = diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index 44bb17d..9e992d3 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -2062,12 +2062,14 @@ struct GatewayBridgeService::ChannelRuntime { const bool commissioning_only = !knx_config.has_value(); ESP_LOGI(kTag, "gateway=%u KNX/IP start config namespace=%s storedConfig=%d udp=%u tunnel=%d " - "multicast=%d multicastGroup=%s mainGroup=%u tpUart=%d tx=%d rx=%d individual=0x%04x", + "multicast=%d multicastGroup=%s mainGroup=%u tpUart=%d tx=%d rx=%d nineBit=%d " + "individual=0x%04x", channel.gateway_id, openKnxNamespace().c_str(), !commissioning_only, static_cast(runtime_config.udp_port), runtime_config.tunnel_enabled, runtime_config.multicast_enabled, runtime_config.multicast_address.c_str(), static_cast(runtime_config.main_group), runtime_config.tp_uart.uart_port, runtime_config.tp_uart.tx_pin, runtime_config.tp_uart.rx_pin, + runtime_config.tp_uart.nine_bit_mode, runtime_config.individual_address); knx->setConfig(runtime_config); knx_router->setConfig(runtime_config); @@ -2304,6 +2306,7 @@ struct GatewayBridgeService::ChannelRuntime { cJSON_AddNumberToObject(serial_json, "txPin", effective_knx->tp_uart.tx_pin); cJSON_AddNumberToObject(serial_json, "rxPin", effective_knx->tp_uart.rx_pin); cJSON_AddNumberToObject(serial_json, "baudrate", effective_knx->tp_uart.baudrate); + cJSON_AddBoolToObject(serial_json, "nineBitMode", effective_knx->tp_uart.nine_bit_mode); cJSON_AddItemToObject(knx_json, "tpUart", serial_json); } } diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index dad35e9..df0b30f 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -39,6 +39,7 @@ struct GatewayKnxTpUartConfig { size_t rx_buffer_size{1024}; size_t tx_buffer_size{1024}; uint32_t read_timeout_ms{20}; + bool nine_bit_mode{true}; }; enum class GatewayKnxMappingMode : uint8_t { @@ -348,6 +349,8 @@ class GatewayKnxTpIpRouter { int tcp_sock_{-1}; int active_tcp_sock_{-1}; int tp_uart_port_{-1}; + int tp_uart_tx_pin_{-1}; + int tp_uart_rx_pin_{-1}; std::vector multicast_joined_interfaces_; TickType_t network_refresh_tick_{0}; std::array tcp_clients_{}; diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index c84578d..24ad844 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -9,6 +9,7 @@ #include "lwip/inet.h" #include "lwip/sockets.h" #include "openknx_idf/ets_device_runtime.h" +#include "soc/uart_periph.h" #include #include @@ -173,6 +174,38 @@ 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]{}; @@ -909,6 +942,9 @@ std::optional GatewayKnxConfigFromValue(const DaliValue* value config.tp_uart.read_timeout_ms = static_cast(std::max( 1, ObjectIntAny(serial, {"readTimeoutMs", "read_timeout_ms"}) .value_or(static_cast(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; } @@ -939,6 +975,7 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) { serial["rxBufferSize"] = static_cast(config.tp_uart.rx_buffer_size); serial["txBufferSize"] = static_cast(config.tp_uart.tx_buffer_size); serial["readTimeoutMs"] = static_cast(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()); @@ -1911,13 +1948,24 @@ esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task } 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(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=%d rx=%d commissioningOnly=%d", + "tpUart=%d tx=%s rx=%s nineBit=%d commissioningOnly=%d", openknx_namespace_.c_str(), static_cast(config_.udp_port), config_.tunnel_enabled, config_.multicast_enabled, config_.multicast_address.c_str(), config_.tp_uart.uart_port, - config_.tp_uart.tx_pin, config_.tp_uart.rx_pin, commissioning_only_); + 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; } @@ -2447,11 +2495,24 @@ bool GatewayKnxTpIpRouter::configureTpUart() { uart_config_t uart_config{}; uart_config.baud_rate = static_cast(serial.baudrate); uart_config.data_bits = UART_DATA_8_BITS; - uart_config.parity = UART_PARITY_EVEN; + uart_config.parity = serial.nine_bit_mode ? UART_PARITY_EVEN : UART_PARITY_DISABLE; uart_config.stop_bits = UART_STOP_BITS_1; uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; uart_config.source_clk = UART_SCLK_DEFAULT; const uart_port_t uart_port = static_cast(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 false; + } esp_err_t err = uart_param_config(uart_port, &uart_config); if (err != ESP_OK) { last_error_ = EspErrDetail("failed to configure KNX TP-UART parameters on UART" + @@ -2460,14 +2521,12 @@ bool GatewayKnxTpIpRouter::configureTpUart() { ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } - err = uart_set_pin(uart_port, serial.tx_pin < 0 ? UART_PIN_NO_CHANGE : serial.tx_pin, - serial.rx_pin < 0 ? UART_PIN_NO_CHANGE : serial.rx_pin, - UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + err = uart_set_pin(uart_port, tx_pin, rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); if (err != ESP_OK) { last_error_ = EspErrDetail("failed to configure KNX TP-UART pins uart=" + std::to_string(serial.uart_port) + " tx=" + - std::to_string(serial.tx_pin) + " rx=" + - std::to_string(serial.rx_pin), + UartPinDescription(serial.tx_pin, tx_pin) + " rx=" + + UartPinDescription(serial.rx_pin, rx_pin), err); ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; @@ -2482,6 +2541,8 @@ bool GatewayKnxTpIpRouter::configureTpUart() { return false; } tp_uart_port_ = serial.uart_port; + tp_uart_tx_pin_ = tx_pin; + tp_uart_rx_pin_ = rx_pin; if (!initializeTpUart()) { if (ets_device_ != nullptr && !ets_device_->configured()) { ESP_LOGW(kTag, @@ -2496,8 +2557,11 @@ bool GatewayKnxTpIpRouter::configureTpUart() { ESP_LOGE(kTag, "%s", last_error_.c_str()); return false; } - ESP_LOGI(kTag, "KNX TP-UART online uart=%d tx=%d rx=%d baud=%u", serial.uart_port, - serial.tx_pin, serial.rx_pin, static_cast(serial.baudrate)); + ESP_LOGI(kTag, "KNX TP-UART online uart=%d tx=%s rx=%s baud=%u nineBit=%d", + serial.uart_port, + UartPinDescription(serial.tx_pin, tp_uart_tx_pin_).c_str(), + UartPinDescription(serial.rx_pin, tp_uart_rx_pin_).c_str(), + static_cast(serial.baudrate), serial.nine_bit_mode); return true; } @@ -2600,8 +2664,8 @@ bool GatewayKnxTpIpRouter::initializeTpUart() { last_error_ = (saw_reset ? "timed out waiting for KNX TP-UART state indication" : "timed out waiting for KNX TP-UART reset indication") + std::string(" uart=") + std::to_string(config_.tp_uart.uart_port) + - " tx=" + std::to_string(config_.tp_uart.tx_pin) + - " rx=" + std::to_string(config_.tp_uart.rx_pin) + + " tx=" + UartPinDescription(config_.tp_uart.tx_pin, tp_uart_tx_pin_) + + " rx=" + UartPinDescription(config_.tp_uart.rx_pin, tp_uart_rx_pin_) + " timeoutMs=1500"; return false; } diff --git a/components/openknx_idf/include/openknx_idf/tpuart_uart_interface.h b/components/openknx_idf/include/openknx_idf/tpuart_uart_interface.h index 788683a..f8ad9a3 100644 --- a/components/openknx_idf/include/openknx_idf/tpuart_uart_interface.h +++ b/components/openknx_idf/include/openknx_idf/tpuart_uart_interface.h @@ -14,7 +14,8 @@ namespace gateway::openknx { class TpuartUartInterface : public TPUart::Interface::Abstract { public: TpuartUartInterface(uart_port_t uart_port, int tx_pin, int rx_pin, - size_t rx_buffer_size = 512, size_t tx_buffer_size = 512); + size_t rx_buffer_size = 512, size_t tx_buffer_size = 512, + bool nine_bit_mode = true); ~TpuartUartInterface(); void begin(int baud) override; @@ -34,6 +35,7 @@ class TpuartUartInterface : public TPUart::Interface::Abstract { int rx_pin_; size_t rx_buffer_size_; size_t tx_buffer_size_; + bool nine_bit_mode_; std::atomic_bool overflow_{false}; std::function callback_; }; diff --git a/components/openknx_idf/src/ets_device_runtime.cpp b/components/openknx_idf/src/ets_device_runtime.cpp index 3668698..fdf51a1 100644 --- a/components/openknx_idf/src/ets_device_runtime.cpp +++ b/components/openknx_idf/src/ets_device_runtime.cpp @@ -80,6 +80,7 @@ EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace, ? tunnel_client_address : DefaultTunnelClientAddress( device_.deviceObject().individualAddress())); + server->deviceAddressPropertiesTargetClient(false); server->tunnelFrameCallback(&EtsDeviceRuntime::EmitTunnelFrame, this); } device_.functionPropertyCallback(&EtsDeviceRuntime::HandleFunctionPropertyCommand); diff --git a/components/openknx_idf/src/tpuart_uart_interface.cpp b/components/openknx_idf/src/tpuart_uart_interface.cpp index 4920a53..300400b 100644 --- a/components/openknx_idf/src/tpuart_uart_interface.cpp +++ b/components/openknx_idf/src/tpuart_uart_interface.cpp @@ -1,6 +1,7 @@ #include "openknx_idf/tpuart_uart_interface.h" #include "esp_log.h" +#include "soc/uart_periph.h" #include @@ -9,15 +10,39 @@ namespace { constexpr const char* kTag = "openknx_tpuart"; +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; +} + } // namespace TpuartUartInterface::TpuartUartInterface(uart_port_t uart_port, int tx_pin, int rx_pin, - size_t rx_buffer_size, size_t tx_buffer_size) + size_t rx_buffer_size, size_t tx_buffer_size, + bool nine_bit_mode) : uart_port_(uart_port), tx_pin_(tx_pin), rx_pin_(rx_pin), rx_buffer_size_(rx_buffer_size), - tx_buffer_size_(tx_buffer_size) {} + tx_buffer_size_(tx_buffer_size), + nine_bit_mode_(nine_bit_mode) {} TpuartUartInterface::~TpuartUartInterface() { end(); } @@ -29,22 +54,30 @@ void TpuartUartInterface::begin(int baud) { uart_config_t config{}; config.baud_rate = baud; config.data_bits = UART_DATA_8_BITS; - config.parity = UART_PARITY_EVEN; + config.parity = nine_bit_mode_ ? UART_PARITY_EVEN : UART_PARITY_DISABLE; config.stop_bits = UART_STOP_BITS_1; config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; config.source_clk = UART_SCLK_DEFAULT; + int tx_pin = UART_PIN_NO_CHANGE; + int rx_pin = UART_PIN_NO_CHANGE; + if (!ResolveUartIoPin(uart_port_, tx_pin_, SOC_UART_TX_PIN_IDX, &tx_pin) || + !ResolveUartIoPin(uart_port_, rx_pin_, SOC_UART_RX_PIN_IDX, &rx_pin)) { + ESP_LOGE(kTag, "UART%d has no ESP-IDF default TX/RX pin; configure explicit pins", + uart_port_); + return; + } + 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); + err = uart_set_pin(uart_port_, tx_pin, 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)); + ESP_LOGE(kTag, "failed to route UART%d pins tx=%d rx=%d: %s", uart_port_, tx_pin, + rx_pin, esp_err_to_name(err)); return; } diff --git a/knx b/knx index 5d7d6e5..346b704 160000 --- a/knx +++ b/knx @@ -1 +1 @@ -Subproject commit 5d7d6e573bacc8217487843013afd72f8bfdd52c +Subproject commit 346b704cbec32ff1dca3fbba7ec507c07b846eea