From 3288f83401558a67f9439ee4eae16ab6e957002a Mon Sep 17 00:00:00 2001 From: Ondrej Kosta Date: Wed, 24 May 2023 15:56:24 +0200 Subject: [PATCH] feat(network/examples): extended LwIP bridge example Extended LwIP bridge example to support WiFi AP interface and DHCP Server https://github.com/espressif/esp-idf/issues/5697 --- components/esp_netif/CMakeLists.txt | 12 +- .../esp_netif/include/esp_netif_br_glue.h | 15 +- .../esp_netif/include/esp_netif_defaults.h | 22 +- components/esp_netif/lwip/esp_netif_br_glue.c | 330 ++++++++++++------ components/esp_netif/lwip/netif/wlanif.c | 2 +- components/lwip/apps/dhcpserver/dhcpserver.c | 6 +- examples/network/bridge/README.md | 43 +-- examples/network/bridge/docs/network_1.drawio | 71 ++++ examples/network/bridge/docs/network_1.png | Bin 0 -> 19742 bytes examples/network/bridge/docs/network_2.drawio | 73 ++++ examples/network/bridge/docs/network_2.png | Bin 0 -> 20431 bytes .../network/bridge/main/Kconfig.projbuild | 45 +++ .../network/bridge/main/bridge_example_main.c | 62 +++- 13 files changed, 545 insertions(+), 136 deletions(-) create mode 100644 examples/network/bridge/docs/network_1.drawio create mode 100644 examples/network/bridge/docs/network_1.png create mode 100644 examples/network/bridge/docs/network_2.drawio create mode 100644 examples/network/bridge/docs/network_2.png create mode 100644 examples/network/bridge/main/Kconfig.projbuild diff --git a/components/esp_netif/CMakeLists.txt b/components/esp_netif/CMakeLists.txt index 091d07daa7..ceb1d30a5c 100644 --- a/components/esp_netif/CMakeLists.txt +++ b/components/esp_netif/CMakeLists.txt @@ -51,7 +51,17 @@ idf_component_register(SRCS "${srcs}" LDFRAGMENTS linker.lf) if(CONFIG_ESP_NETIF_L2_TAP OR CONFIG_ESP_NETIF_BRIDGE_EN) - idf_component_optional_requires(PRIVATE esp_eth vfs) + set(optional_requires "") + if(CONFIG_ESP_NETIF_L2_TAP) + list(APPEND optional_requires "vfs") + endif() + + if(CONFIG_ESP_NETIF_BRIDGE_EN) + list(APPEND optional_requires "esp_wifi") + endif() + + list(APPEND optional_requires "esp_eth") + idf_component_optional_requires(PRIVATE "${optional_requires}") endif() diff --git a/components/esp_netif/include/esp_netif_br_glue.h b/components/esp_netif/include/esp_netif_br_glue.h index 95b1c0a8bc..ff30c31f11 100644 --- a/components/esp_netif/include/esp_netif_br_glue.h +++ b/components/esp_netif/include/esp_netif_br_glue.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -28,14 +28,23 @@ typedef struct esp_netif_br_glue_t* esp_netif_br_glue_handle_t; esp_netif_br_glue_handle_t esp_netif_br_glue_new(void); /** - * @brief Add a port to the bridge netif glue + * @brief Add Ethernet port to the bridge netif glue * * @param netif_br_glue bridge netif glue - * @param esp_netif_port port netif + * @param esp_netif_port Ethernet port netif * @return - ESP_OK on success */ esp_err_t esp_netif_br_glue_add_port(esp_netif_br_glue_handle_t netif_br_glue, esp_netif_t *esp_netif_port); +/** + * @brief Add WiFi port to the bridge netif glue + * + * @param netif_br_glue bridge netif glue + * @param esp_netif_port WiFi port netif + * @return - ESP_OK on success + */ +esp_err_t esp_netif_br_glue_add_wifi_port(esp_netif_br_glue_handle_t netif_br_glue, esp_netif_t *esp_netif_port); + /** * @brief Delete netif glue of bridge * diff --git a/components/esp_netif/include/esp_netif_defaults.h b/components/esp_netif/include/esp_netif_defaults.h index 46cf10eeb4..414b6c389b 100644 --- a/components/esp_netif/include/esp_netif_defaults.h +++ b/components/esp_netif/include/esp_netif_defaults.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -104,9 +104,6 @@ extern "C" { } #endif /* CONFIG_PPP_SUPPORT */ - - - #define ESP_NETIF_INHERENT_DEFAULT_BR() \ { \ .flags = (esp_netif_flags_t)(ESP_NETIF_DHCP_CLIENT | ESP_NETIF_DEFAULT_ARP_FLAGS | ESP_NETIF_FLAG_EVENT_IP_MODIFIED | ESP_NETIF_FLAG_IS_BRIDGE), \ @@ -114,8 +111,21 @@ extern "C" { ESP_COMPILER_DESIGNATED_INIT_AGGREGATE_TYPE_EMPTY(ip_info) \ .get_ip_event = IP_EVENT_ETH_GOT_IP, \ .lost_ip_event = IP_EVENT_ETH_LOST_IP, \ - .if_key = "BR", \ - .if_desc = "br", \ + .if_key = "BR0", \ + .if_desc = "br0", \ + .route_prio = 70, \ + .bridge_info = NULL \ + } + +#define ESP_NETIF_INHERENT_DEFAULT_BR_DHCPS() \ + { \ + .flags = (esp_netif_flags_t)(ESP_NETIF_DHCP_SERVER | ESP_NETIF_FLAG_IS_BRIDGE), \ + ESP_COMPILER_DESIGNATED_INIT_AGGREGATE_TYPE_EMPTY(mac) \ + .ip_info = &_g_esp_netif_soft_ap_ip, \ + .get_ip_event = 0, \ + .lost_ip_event = 0, \ + .if_key = "BR1", \ + .if_desc = "br1", \ .route_prio = 70, \ .bridge_info = NULL \ } diff --git a/components/esp_netif/lwip/esp_netif_br_glue.c b/components/esp_netif/lwip/esp_netif_br_glue.c index ebe70947a6..c26b3e1ece 100644 --- a/components/esp_netif/lwip/esp_netif_br_glue.c +++ b/components/esp_netif/lwip/esp_netif_br_glue.c @@ -1,11 +1,13 @@ /* - * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include +#include #include "esp_netif_br_glue.h" #include "esp_eth_driver.h" +#include "esp_wifi.h" #include "esp_event.h" #include "esp_log.h" #include "esp_check.h" @@ -16,16 +18,23 @@ const static char *TAG = "esp_netif_br_glue"; typedef struct esp_netif_br_glue_t esp_netif_br_glue_t; +typedef enum { + START_CTX_HANDLER, + STOP_CTX_HANDLER, + CONNECT_CTX_HANDLER, + DISCONNECT_CTX_HANDLER, + CTX_HANDLERS_END_LIST +} ctx_handl_type_t; + struct esp_netif_br_glue_t { esp_netif_driver_base_t base; bool br_started; esp_netif_t **ports_esp_netifs; + esp_netif_t *wifi_esp_netif; uint8_t port_cnt; - esp_event_handler_instance_t eth_start_ctx_handler; - esp_event_handler_instance_t eth_stop_ctx_handler; - esp_event_handler_instance_t eth_connect_ctx_handler; - esp_event_handler_instance_t eth_disconnect_ctx_handler; + esp_event_handler_instance_t *eth_ctx_handlers; esp_event_handler_instance_t get_ip_ctx_handler; + esp_event_handler_instance_t *wifi_ctx_handlers; }; static esp_err_t esp_eth_post_attach_br(esp_netif_t *esp_netif, void *args) @@ -43,87 +52,128 @@ static esp_err_t esp_eth_post_attach_br(esp_netif_t *esp_netif, void *args) return ESP_OK; } -static void eth_action_start(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +static void start_br_if_stopped(esp_netif_br_glue_t *netif_glue) { - esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data; - esp_netif_br_glue_t *netif_glue = handler_args; - ESP_LOGD(TAG, "eth_action_start: %p, %p, %d, %p, %p", netif_glue, base, event_id, event_data, *(esp_eth_handle_t *)event_data); - - for (int i = 0; i < netif_glue->port_cnt; i++) { - if (eth_handle == esp_netif_get_io_driver(netif_glue->ports_esp_netifs[i])) { - if (netif_glue->br_started == false) { - esp_netif_action_start(netif_glue->base.netif, base, event_id, event_data); // basically creates lwip_netif br instance - netif_glue->br_started = true; - ESP_LOGD(TAG, "bridge netif %p is started", netif_glue->base.netif); - } - esp_netif_bridge_add_port(netif_glue->base.netif, netif_glue->ports_esp_netifs[i]); - } + if (netif_glue->br_started == false) { + esp_netif_action_start(netif_glue->base.netif, 0, 0, NULL); // basically creates lwip_netif br instance + netif_glue->br_started = true; + ESP_LOGD(TAG, "bridge netif %p is started", netif_glue->base.netif); } } -static void eth_action_stop(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +static void stop_br_if_started(esp_netif_br_glue_t *netif_glue) { - esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data; - esp_netif_br_glue_t *netif_glue = handler_args; - ESP_LOGD(TAG, "eth_action_stop: %p, %p, %d, %p, %p", netif_glue, base, event_id, event_data, *(esp_eth_handle_t *)event_data); - - for (int i = 0; i < netif_glue->port_cnt; i++) { - // if one of the bridge's ports is stopped, we need to stop the bridge too, since port's lwip_netif is removed and so it would become - // an invalid reference in the bridge's internal structure (there is no way how to remove single port from bridge in current LwIP) - if (eth_handle == esp_netif_get_io_driver(netif_glue->ports_esp_netifs[i])) { - if (netif_glue->br_started == true) { - esp_netif_action_stop(netif_glue->base.netif, base, event_id, event_data); // basically removes lwip_netif br - netif_glue->br_started = false; - ESP_LOGD(TAG, "bridge netif %p is stopped", netif_glue->base.netif); - } - } + if (netif_glue->br_started == true) { + esp_netif_action_stop(netif_glue->base.netif, 0, 0, NULL); // basically removes lwip_netif br + netif_glue->br_started = false; + ESP_LOGD(TAG, "bridge netif %p is stopped", netif_glue->base.netif); } } -static void eth_action_connected(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +static bool are_ports_disconnected(esp_netif_br_glue_t *netif_glue) { - esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data; - esp_netif_br_glue_t *netif_glue = handler_args; - ESP_LOGD(TAG, "eth_action_connected: %p, %p, %d, %p, %p", netif_glue, base, event_id, event_data, *(esp_eth_handle_t *)event_data); - - // if bridge interface is already up, do nothing - if (esp_netif_is_netif_up(netif_glue->base.netif) == true) { - return; - } - - for (int i = 0; i < netif_glue->port_cnt; i++) { - if (eth_handle == esp_netif_get_io_driver(netif_glue->ports_esp_netifs[i])) { - esp_netif_action_connected(netif_glue->base.netif, base, event_id, event_data); - ESP_LOGD(TAG, "bridge netif %p is connected", netif_glue->base.netif); + int disc_cnt; + // check Ethernet ports at first + for (disc_cnt = 0; disc_cnt < netif_glue->port_cnt; disc_cnt++) { + if (esp_netif_is_netif_up(netif_glue->ports_esp_netifs[disc_cnt]) == true) { break; } } + + if (disc_cnt >= netif_glue->port_cnt) { + // check WiFi port if is also registered + if (netif_glue->wifi_esp_netif != NULL) { + if (esp_netif_is_netif_up(netif_glue->wifi_esp_netif) == false) { + return true; + } + } else { + return true; + } + } + return false; } -static void eth_action_disconnected(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +static void port_action_start(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { - esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data; esp_netif_br_glue_t *netif_glue = handler_args; - ESP_LOGD(TAG, "eth_action_disconnected: %p, %p, %d, %p, %p", netif_glue, base, event_id, event_data, *(esp_eth_handle_t *)event_data); - for (int i = 0; i < netif_glue->port_cnt; i++) { - // if this is a Ethernet interface associated with bridge, check if other ports are disconnected - if (eth_handle == esp_netif_get_io_driver(netif_glue->ports_esp_netifs[i])) { - int disc_cnt; - for (disc_cnt = 0; disc_cnt < netif_glue->port_cnt; disc_cnt++) { - if (esp_netif_is_netif_up(netif_glue->ports_esp_netifs[disc_cnt]) == true) { - break; - } - } - // if all ports are disconnected, set bridge as disconnected too - if (disc_cnt >= netif_glue->port_cnt) { - esp_netif_action_disconnected(netif_glue->base.netif, base, event_id, event_data); - ESP_LOGD(TAG, "bridge netif %p is disconnected", netif_glue->base.netif); + if (base == WIFI_EVENT) { + ESP_LOGD(TAG, "wifi_action_start: %p, %p, %d, %p", netif_glue, base, event_id, event_data); + start_br_if_stopped(netif_glue); + esp_netif_bridge_add_port(netif_glue->base.netif, netif_glue->wifi_esp_netif); + } else if (base == ETH_EVENT) { + esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data; + ESP_LOGD(TAG, "eth_action_start: %p, %p, %d, %p, %p", netif_glue, base, event_id, event_data, *(esp_eth_handle_t *)event_data); + for (int i = 0; i < netif_glue->port_cnt; i++) { + if (eth_handle == esp_netif_get_io_driver(netif_glue->ports_esp_netifs[i])) { + start_br_if_stopped(netif_glue); + esp_netif_bridge_add_port(netif_glue->base.netif, netif_glue->ports_esp_netifs[i]); } } } } +static void port_action_stop(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + esp_netif_br_glue_t *netif_glue = handler_args; + + // if one of the bridge's ports is stopped, we need to stop the bridge too, since port's lwip_netif is removed and so it would become + // an invalid reference in the bridge's internal structure (there is no way how to remove single port from bridge in current LwIP) + if (base == WIFI_EVENT) { + ESP_LOGD(TAG, "wifi_action_stop: %p, %p, %d, %p", netif_glue, base, event_id, event_data); + stop_br_if_started(netif_glue); + } else if (base == ETH_EVENT) { + esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data; + ESP_LOGD(TAG, "eth_action_stop: %p, %p, %d, %p, %p", netif_glue, base, event_id, event_data, *(esp_eth_handle_t *)event_data); + for (int i = 0; i < netif_glue->port_cnt; i++) { + if (eth_handle == esp_netif_get_io_driver(netif_glue->ports_esp_netifs[i])) { + stop_br_if_started(netif_glue); + } + } + } +} + +static void port_action_connected(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + esp_netif_br_glue_t *netif_glue = handler_args; + + // if bridge interface is already up, do nothing + if (esp_netif_is_netif_up(netif_glue->base.netif) == true) { + ESP_LOGD(TAG, "action_connected, no action bridge is up"); + return; + } + + if (base == WIFI_EVENT) { + ESP_LOGD(TAG, "wifi_action_connected: %p, %p, %d, %p", netif_glue, base, event_id, event_data); + esp_netif_action_connected(netif_glue->base.netif, 0, 0, NULL); + } else if (base == ETH_EVENT) { + esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data; + ESP_LOGD(TAG, "eth_action_connected: %p, %p, %d, %p, %p", netif_glue, base, event_id, event_data, *(esp_eth_handle_t *)event_data); + for (int i = 0; i < netif_glue->port_cnt; i++) { + if (eth_handle == esp_netif_get_io_driver(netif_glue->ports_esp_netifs[i])) { + esp_netif_action_connected(netif_glue->base.netif, 0, 0, NULL); + ESP_LOGD(TAG, "bridge netif %p is connected", netif_glue->base.netif); + break; + } + } + } + if (esp_netif_is_netif_up(netif_glue->base.netif) == true && + (esp_netif_get_flags(netif_glue->base.netif) & ESP_NETIF_DHCP_SERVER) == ESP_NETIF_DHCP_SERVER) { + esp_netif_dhcps_start(netif_glue->base.netif); + } +} + +static void port_action_disconnected(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + esp_netif_br_glue_t *netif_glue = handler_args; + ESP_LOGD(TAG, "action_disconnected: %p, %p, %d, %p", netif_glue, base, event_id, event_data); + // if all ports are disconnected, set bridge as disconnected too + if (are_ports_disconnected(netif_glue)) { + esp_netif_action_disconnected(netif_glue->base.netif, base, event_id, event_data); + ESP_LOGD(TAG, "bridge netif %p is disconnected", netif_glue->base.netif); + } +} + static void br_action_got_ip(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { ip_event_got_ip_t *ip_event = (ip_event_got_ip_t *)event_data; @@ -138,25 +188,22 @@ static esp_err_t esp_netif_br_glue_clear_instance_handlers(esp_netif_br_glue_han { ESP_RETURN_ON_FALSE(esp_netif_br_glue, ESP_ERR_INVALID_ARG, TAG, "esp_netif_br_glue handle can't be null"); - if (esp_netif_br_glue->eth_start_ctx_handler) { - esp_event_handler_instance_unregister(ETH_EVENT, ETHERNET_EVENT_START, esp_netif_br_glue->eth_start_ctx_handler); - esp_netif_br_glue->eth_start_ctx_handler = NULL; + if (esp_netif_br_glue->eth_ctx_handlers[START_CTX_HANDLER]) { + esp_event_handler_instance_unregister(ETH_EVENT, ETHERNET_EVENT_START, esp_netif_br_glue->eth_ctx_handlers[START_CTX_HANDLER]); } - if (esp_netif_br_glue->eth_stop_ctx_handler) { - esp_event_handler_instance_unregister(ETH_EVENT, ETHERNET_EVENT_STOP, esp_netif_br_glue->eth_stop_ctx_handler); - esp_netif_br_glue->eth_stop_ctx_handler = NULL; + if (esp_netif_br_glue->eth_ctx_handlers[STOP_CTX_HANDLER]) { + esp_event_handler_instance_unregister(ETH_EVENT, ETHERNET_EVENT_STOP, esp_netif_br_glue->eth_ctx_handlers[STOP_CTX_HANDLER]); } - if (esp_netif_br_glue->eth_connect_ctx_handler) { - esp_event_handler_instance_unregister(ETH_EVENT, ETHERNET_EVENT_CONNECTED, esp_netif_br_glue->eth_connect_ctx_handler); - esp_netif_br_glue->eth_connect_ctx_handler = NULL; + if (esp_netif_br_glue->eth_ctx_handlers[CONNECT_CTX_HANDLER]) { + esp_event_handler_instance_unregister(ETH_EVENT, ETHERNET_EVENT_CONNECTED, esp_netif_br_glue->eth_ctx_handlers[CONNECT_CTX_HANDLER]); } - if (esp_netif_br_glue->eth_disconnect_ctx_handler) { - esp_event_handler_instance_unregister(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED, esp_netif_br_glue->eth_disconnect_ctx_handler); - esp_netif_br_glue->eth_disconnect_ctx_handler = NULL; + if (esp_netif_br_glue->eth_ctx_handlers[DISCONNECT_CTX_HANDLER]) { + esp_event_handler_instance_unregister(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED, esp_netif_br_glue->eth_ctx_handlers[DISCONNECT_CTX_HANDLER]); } + free(esp_netif_br_glue->eth_ctx_handlers); if (esp_netif_br_glue->get_ip_ctx_handler) { esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_ETH_GOT_IP, esp_netif_br_glue->get_ip_ctx_handler); @@ -168,24 +215,28 @@ static esp_err_t esp_netif_br_glue_clear_instance_handlers(esp_netif_br_glue_han static esp_err_t esp_netif_br_glue_set_instance_handlers(esp_netif_br_glue_handle_t esp_netif_br_glue) { + esp_err_t ret = ESP_OK; ESP_RETURN_ON_FALSE(esp_netif_br_glue, ESP_ERR_INVALID_ARG, TAG, "esp_netif_br_glue handle can't be null"); - esp_err_t ret = esp_event_handler_instance_register(ETH_EVENT, ETHERNET_EVENT_START, eth_action_start, esp_netif_br_glue, &esp_netif_br_glue->eth_start_ctx_handler); + esp_netif_br_glue->eth_ctx_handlers = calloc(CTX_HANDLERS_END_LIST, sizeof(esp_event_handler_instance_t)); + ESP_GOTO_ON_FALSE(esp_netif_br_glue->eth_ctx_handlers, ESP_ERR_NO_MEM, fail, TAG, "no memory for Ethernet event context handlers"); + + ret = esp_event_handler_instance_register(ETH_EVENT, ETHERNET_EVENT_START, port_action_start, esp_netif_br_glue, &esp_netif_br_glue->eth_ctx_handlers[START_CTX_HANDLER]); if (ret != ESP_OK) { goto fail; } - ret = esp_event_handler_instance_register(ETH_EVENT, ETHERNET_EVENT_STOP, eth_action_stop, esp_netif_br_glue, &esp_netif_br_glue->eth_stop_ctx_handler); + ret = esp_event_handler_instance_register(ETH_EVENT, ETHERNET_EVENT_STOP, port_action_stop, esp_netif_br_glue, &esp_netif_br_glue->eth_ctx_handlers[STOP_CTX_HANDLER]); if (ret != ESP_OK) { goto fail; } - ret = esp_event_handler_instance_register(ETH_EVENT, ETHERNET_EVENT_CONNECTED, eth_action_connected, esp_netif_br_glue, &esp_netif_br_glue->eth_connect_ctx_handler); + ret = esp_event_handler_instance_register(ETH_EVENT, ETHERNET_EVENT_CONNECTED, port_action_connected, esp_netif_br_glue, &esp_netif_br_glue->eth_ctx_handlers[CONNECT_CTX_HANDLER]); if (ret != ESP_OK) { goto fail; } - ret = esp_event_handler_instance_register(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED, eth_action_disconnected, esp_netif_br_glue, &esp_netif_br_glue->eth_disconnect_ctx_handler); + ret = esp_event_handler_instance_register(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED, port_action_disconnected, esp_netif_br_glue, &esp_netif_br_glue->eth_ctx_handlers[DISCONNECT_CTX_HANDLER]); if (ret != ESP_OK) { goto fail; } @@ -202,6 +253,103 @@ fail: return ret; } +static esp_err_t esp_netif_br_glue_clear_instance_handlers_wifi(esp_netif_br_glue_handle_t esp_netif_br_glue) +{ + ESP_RETURN_ON_FALSE(esp_netif_br_glue, ESP_ERR_INVALID_ARG, TAG, "esp_netif_br_glue handle can't be null"); + ESP_RETURN_ON_FALSE(esp_netif_br_glue->wifi_ctx_handlers, ESP_ERR_INVALID_STATE, TAG, "WiFi event handlers are empty"); + + if (esp_netif_br_glue->wifi_ctx_handlers[START_CTX_HANDLER]) { + esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_START, esp_netif_br_glue->wifi_ctx_handlers[START_CTX_HANDLER]); + } + + if (esp_netif_br_glue->wifi_ctx_handlers[STOP_CTX_HANDLER]) { + esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_STOP, esp_netif_br_glue->wifi_ctx_handlers[STOP_CTX_HANDLER]); + } + + if (esp_netif_br_glue->wifi_ctx_handlers[CONNECT_CTX_HANDLER]) { + esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, esp_netif_br_glue->wifi_ctx_handlers[CONNECT_CTX_HANDLER]); + } + + if (esp_netif_br_glue->wifi_ctx_handlers[DISCONNECT_CTX_HANDLER]) { + esp_event_handler_instance_unregister(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, esp_netif_br_glue->wifi_ctx_handlers[DISCONNECT_CTX_HANDLER]); + } + free(esp_netif_br_glue->wifi_ctx_handlers); + + return ESP_OK; +} + +static esp_err_t esp_netif_br_glue_set_instance_handlers_wifi(esp_netif_br_glue_handle_t esp_netif_br_glue) +{ + esp_err_t ret= ESP_OK; + + ESP_RETURN_ON_FALSE(esp_netif_br_glue, ESP_ERR_INVALID_ARG, TAG, "esp_netif_br_glue handle can't be null"); + ESP_RETURN_ON_FALSE(esp_netif_br_glue->wifi_esp_netif, ESP_ERR_INVALID_ARG, TAG, "WiFi port esp_netif isn't registered"); + + esp_netif_br_glue->wifi_ctx_handlers = calloc(CTX_HANDLERS_END_LIST, sizeof(esp_event_handler_instance_t)); + ESP_GOTO_ON_FALSE(esp_netif_br_glue->wifi_ctx_handlers, ESP_ERR_NO_MEM, fail, TAG, "no memory for WiFi event context handlers"); + + ret = esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_AP_START, port_action_start, esp_netif_br_glue, &esp_netif_br_glue->wifi_ctx_handlers[START_CTX_HANDLER]); + if (ret != ESP_OK) { + goto fail; + } + + ret = esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_AP_STOP, port_action_stop, esp_netif_br_glue, &esp_netif_br_glue->wifi_ctx_handlers[STOP_CTX_HANDLER]); + if (ret != ESP_OK) { + goto fail; + } + + ret = esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, port_action_connected, esp_netif_br_glue, &esp_netif_br_glue->wifi_ctx_handlers[CONNECT_CTX_HANDLER]); + if (ret != ESP_OK) { + goto fail; + } + + ret = esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, port_action_disconnected, esp_netif_br_glue, &esp_netif_br_glue->wifi_ctx_handlers[DISCONNECT_CTX_HANDLER]); + if (ret != ESP_OK) { + goto fail; + } + + return ESP_OK; +fail: + esp_netif_br_glue_clear_instance_handlers_wifi(esp_netif_br_glue); + return ret; +} + +esp_err_t esp_netif_br_glue_add_port(esp_netif_br_glue_handle_t netif_br_glue, esp_netif_t *esp_netif_port) +{ + if (netif_br_glue->ports_esp_netifs == NULL) { + netif_br_glue->ports_esp_netifs = malloc(sizeof(esp_netif_t *)); + } else { + netif_br_glue->ports_esp_netifs = realloc(netif_br_glue->ports_esp_netifs, (netif_br_glue->port_cnt + 1) * sizeof(esp_netif_t *)); + } + if (!netif_br_glue->ports_esp_netifs) { + ESP_LOGE(TAG, "no memory to add br port"); + return ESP_ERR_NO_MEM; + } + + netif_br_glue->ports_esp_netifs[netif_br_glue->port_cnt] = esp_netif_port; + netif_br_glue->port_cnt++; + + return ESP_OK; +} + +esp_err_t esp_netif_br_glue_add_wifi_port(esp_netif_br_glue_handle_t netif_br_glue, esp_netif_t *esp_netif_port) +{ + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(netif_br_glue->wifi_esp_netif == NULL, ESP_ERR_INVALID_STATE, fail_ret, TAG, "WiFi interface already registered"); + const char *if_desc = esp_netif_get_desc(esp_netif_port); + ESP_GOTO_ON_FALSE(strcmp(if_desc, "ap") == 0, ESP_ERR_INVALID_ARG, fail_ret, TAG, "interface is not WiFi AP"); + + netif_br_glue->wifi_esp_netif = esp_netif_port; + ESP_GOTO_ON_ERROR(esp_netif_br_glue_set_instance_handlers_wifi(netif_br_glue), fail, TAG, "failed to create WiFi event handlers"); + + return ESP_OK; +fail: + netif_br_glue->wifi_esp_netif = NULL; + netif_br_glue->wifi_ctx_handlers = NULL; +fail_ret: + return ret; +} + esp_netif_br_glue_handle_t esp_netif_br_glue_new(void) { esp_netif_br_glue_t *netif_glue = calloc(1, sizeof(esp_netif_br_glue_t)); @@ -220,27 +368,13 @@ esp_netif_br_glue_handle_t esp_netif_br_glue_new(void) return netif_glue; } -esp_err_t esp_netif_br_glue_add_port(esp_netif_br_glue_handle_t netif_br_glue, esp_netif_t *esp_netif_port) -{ - if (netif_br_glue->ports_esp_netifs == NULL) { - netif_br_glue->ports_esp_netifs = malloc(sizeof(esp_netif_t *)); - } else { - netif_br_glue->ports_esp_netifs = realloc(netif_br_glue->ports_esp_netifs, (netif_br_glue->port_cnt + 1) * sizeof(esp_netif_t *)); - } - if (!netif_br_glue->ports_esp_netifs) { - ESP_LOGE(TAG, "no memory to add br port"); - return ESP_ERR_NO_MEM; - } - - netif_br_glue->ports_esp_netifs[netif_br_glue->port_cnt] = esp_netif_port; - netif_br_glue->port_cnt++; - - return ESP_OK; -} - esp_err_t esp_netif_br_glue_del(esp_netif_br_glue_handle_t netif_br_glue) { + stop_br_if_started(netif_br_glue); esp_netif_br_glue_clear_instance_handlers(netif_br_glue); + if (netif_br_glue->wifi_esp_netif != NULL) { + esp_netif_br_glue_clear_instance_handlers_wifi(netif_br_glue); + } free(netif_br_glue->ports_esp_netifs); free(netif_br_glue); netif_br_glue = NULL; diff --git a/components/esp_netif/lwip/netif/wlanif.c b/components/esp_netif/lwip/netif/wlanif.c index d389b4b6c8..f6bdfae4fa 100644 --- a/components/esp_netif/lwip/netif/wlanif.c +++ b/components/esp_netif/lwip/netif/wlanif.c @@ -45,7 +45,7 @@ low_level_init(struct netif *netif) /* device capabilities */ /* don't set NETIF_FLAG_ETHARP if this device is not an ethernet one */ - netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP; + netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET; #if ESP_LWIP #if LWIP_IGMP diff --git a/components/lwip/apps/dhcpserver/dhcpserver.c b/components/lwip/apps/dhcpserver/dhcpserver.c index 1f6b509327..6d8cb6e70f 100644 --- a/components/lwip/apps/dhcpserver/dhcpserver.c +++ b/components/lwip/apps/dhcpserver/dhcpserver.c @@ -604,7 +604,7 @@ static void send_offer(dhcps_t *dhcps, struct dhcps_msg *m, u16_t len) ip_addr_t ip_temp = IPADDR4_INIT(0x0); ip4_addr_set(ip_2_ip4(&ip_temp), &dhcps->broadcast_dhcps); #if DHCPS_DEBUG - SendOffer_err_t = udp_sendto(pcb_dhcps, p, &ip_temp, DHCPS_CLIENT_PORT); + SendOffer_err_t = udp_sendto(dhcps->dhcps_pcb, p, &ip_temp, DHCPS_CLIENT_PORT); DHCPS_LOG("dhcps: send_offer>>udp_sendto result %x\n", SendOffer_err_t); #else udp_sendto(dhcps->dhcps_pcb, p, &ip_temp, DHCPS_CLIENT_PORT); @@ -682,7 +682,7 @@ static void send_nak(dhcps_t *dhcps, struct dhcps_msg *m, u16_t len) ip_addr_t ip_temp = IPADDR4_INIT(0x0); ip4_addr_set(ip_2_ip4(&ip_temp), &dhcps->broadcast_dhcps); #if DHCPS_DEBUG - SendNak_err_t = udp_sendto(pcb_dhcps, p, &ip_temp, DHCPS_CLIENT_PORT); + SendNak_err_t = udp_sendto(dhcps->dhcps_pcb, p, &ip_temp, DHCPS_CLIENT_PORT); DHCPS_LOG("dhcps: send_nak>>udp_sendto result %x\n", SendNak_err_t); #else udp_sendto(dhcps->dhcps_pcb, p, &ip_temp, DHCPS_CLIENT_PORT); @@ -1005,7 +1005,7 @@ POOL_CHECK: #if DHCPS_DEBUG DHCPS_LOG("dhcps: xid changed\n"); - DHCPS_LOG("dhcps: client_address.addr = %x\n", client_address.addr); + DHCPS_LOG("dhcps: client_address.addr = %x\n", dhcps->client_address.addr); #endif return ret; } diff --git a/examples/network/bridge/README.md b/examples/network/bridge/README.md index afedb06fbe..9187315608 100644 --- a/examples/network/bridge/README.md +++ b/examples/network/bridge/README.md @@ -14,34 +14,34 @@ Performance of this type of "software" bridge is limited by the performance of E ## How to use example +The bellow sections demonstrate just two basic bridge configurations. However, note that additional combinations are possible. + +### Example 1 - Ethernet Interfaces, DHCP Client + You need one ESP32 with at least two Ethernet ports and two PCs (or other Ethernet capable devices). Connect the network as shown in figure below, configure PC#1 as DHCP server and PC#2 as DHCP client. -```mermaid -graph TD; - classDef classPing fill:#0000,stroke-width:0px; - esp32["ESP32 w/ 2 bridged
Ethernet ports
(DHCP Client)"]; - pc1["PC#1
(DHCP Server)"]; - pc2["PC#2
(DHCP Client)"]; - ping1["ping"]:::classPing - ping2["ping"]:::classPing - ping3["ping"]:::classPing - esp32 -.- ping1; - ping1 -.- pc1; - esp32 == Eth === pc2; - esp32 == Eth === pc1; - esp32 -.- ping2; - ping2 -.- pc2; - pc1 <-.- ping3; - pc2 <-.- ping3; -``` +![network_1](./docs/network_1.png) The work flow of the example is then as follows: -1. Install the Ethernet ports drivers in ESP32. +1. Install the Ethernet ports in ESP32. 2. Configure bridge. 3. Wait for a DHCP leases in ESP32 and PC#2. 4. If get IP addresses successfully, then you will be able to ping the ESP32 device and PC#2 from PC#1 (and vice versa). +### Example 2 - Ethernet & WiFi AP, DHCP Server + +You need one ESP32 with at least one Ethernet port and WiFi, and two PCs (or other Ethernet/WiFi capable devices). Connect the network as shown in figure below, configure PC#1 and PC#2 as DHCP clients. Enable DHCP server option in example menuconfig. + +![network_2](./docs/network_2.png) + +The work flow of the example is then as follows: + +1. Install the Ethernet ports & WiFi AP in ESP32. +2. Configure bridge. +3. Wait for a DHCP leases in PC#1 and PC#2. +4. If get IP addresses successfully, then you will be able to ping the ESP32 device and PC#2 from PC#1 (and vice versa). + ## Hardware Required To run this example, it's recommended that you have either an official ESP32 Ethernet development board - [ESP32-Ethernet-Kit](https://docs.espressif.com/projects/esp-idf/en/latest/hw-reference/get-started-ethernet-kit.html), or 3rd party ESP32 board as long as it's integrated with a supported Ethernet PHY chips and connected with supported SPI Ethernet modules (for example `DM9051`, `W5500` or `KSZ8851SNL`). Or ESP32(S/C series) board without internal Ethernet interface but connected to multiple SPI Ethernet modules. Note that it is recommended to use multiple SPI Ethernet modules of the same type rather than combination of internal EMAC and SPI module since you don't need to take care of load balancing (internal EMAC has much higher bandwidth than SPI Ethernet modules). @@ -159,8 +159,9 @@ Now you can ping your ESP32 in PC#1 terminal by entering `ping 192.168.20.105` a ## Known Limitations -* Currently only Ethernet interfaces can be bridged using LwIP bridge. -* If you need to stop just one Ethernet interface which is bridged to perform some action like speed/duplex setting, **all remaining interfaces** associated with the bridge need to be stopped as well to the bridge work properly after the interfaces are started again. +* Only Ethernet and WiFi AP interfaces can be bridged using LwIP bridge since ESP WiFi station will only receive packets destined to it due to operating in 3 address system. +* If you need to stop just one network interface which is bridged to perform some action (like speed/duplex setting in Ethernet case), **all remaining interfaces** associated with the bridge need to be stopped as well to the bridge work properly after the interfaces are started again. +* No traffic balancing is implemented between faster and slower interface. ## Troubleshooting diff --git a/examples/network/bridge/docs/network_1.drawio b/examples/network/bridge/docs/network_1.drawio new file mode 100644 index 0000000000..869b2f8b16 --- /dev/null +++ b/examples/network/bridge/docs/network_1.drawio @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/network/bridge/docs/network_1.png b/examples/network/bridge/docs/network_1.png new file mode 100644 index 0000000000000000000000000000000000000000..a81fb435a16b6b90522963533ec6d56a3a2d5411 GIT binary patch literal 19742 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

S!d=KBD>_`qK)Pg9|(%F?)@-X-QP?% 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/network/bridge/docs/network_2.png b/examples/network/bridge/docs/network_2.png new file mode 100644 index 0000000000000000000000000000000000000000..f044052ea33e9bb9de044cfa224126f2e5cd0315 GIT binary patch literal 20431 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 gC5uhxtS