From 9fce68769061457d1e99a369b0e78422ec26474f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=9F=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=D0=BE=D1=88=D0=B5=D0=B2?= Date: Thu, 3 Sep 2026 20:18:59 +0300 Subject: [PATCH] Add Linux TAP --- .gitignore | 34 +++- .vscode/c_cpp_properties.json | 19 --- .vscode/launch.json | 10 -- .vscode/settings.json | 17 -- CMakeLists.txt | 2 + README.md | 65 ++++--- components/arp_monitor/CMakeLists.txt | 3 +- components/arp_monitor/arp_monitor.c | 24 ++- components/arp_monitor/include/arp_monitor.h | 3 +- components/device_list/CMakeLists.txt | 3 +- components/device_list/device_list.c | 7 +- components/lwip/lwip/src/core/ipv4/etharp.c | 11 +- components/ping/ping.c | 23 ++- .../protocol_examples_tapif_io/CMakeLists.txt | 9 + .../Kconfig.projbuild | 63 +++++++ .../protocol_examples_tapif_io/README.md | 161 ++++++++++++++++++ .../include/protocol_examples_common.h | 32 ++++ .../include/tapio.h | 27 +++ .../protocol_examples_tapif_io/linux/tapio.c | 134 +++++++++++++++ .../linux_connect.c | 86 ++++++++++ .../protocol_examples_tapif_io/lwip/tapif.c | 103 +++++++++++ .../protocol_examples_tapif_io/lwip/tapif.h | 25 +++ .../protocol_examples_tapif_io/make_tap_netif | 4 + components/web_server/web_server.c | 10 +- docs/web-interface.png | Bin 0 -> 50365 bytes main/CMakeLists.txt | 21 ++- main/main.c | 28 ++- sdkconfig.defaults | 0 sdkconfig.defaults.linux | 9 + 29 files changed, 815 insertions(+), 118 deletions(-) mode change 100644 => 100755 .gitignore delete mode 100644 .vscode/c_cpp_properties.json delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json create mode 100644 components/protocol_examples_tapif_io/CMakeLists.txt create mode 100644 components/protocol_examples_tapif_io/Kconfig.projbuild create mode 100644 components/protocol_examples_tapif_io/README.md create mode 100644 components/protocol_examples_tapif_io/include/protocol_examples_common.h create mode 100644 components/protocol_examples_tapif_io/include/tapio.h create mode 100644 components/protocol_examples_tapif_io/linux/tapio.c create mode 100644 components/protocol_examples_tapif_io/linux_connect.c create mode 100644 components/protocol_examples_tapif_io/lwip/tapif.c create mode 100644 components/protocol_examples_tapif_io/lwip/tapif.h create mode 100755 components/protocol_examples_tapif_io/make_tap_netif create mode 100644 docs/web-interface.png create mode 100644 sdkconfig.defaults create mode 100644 sdkconfig.defaults.linux diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 659c34b..a131952 --- a/.gitignore +++ b/.gitignore @@ -77,15 +77,41 @@ Desktop.ini *.suo # Visual Studio Solution User Options *.sln.docstates # Visual Studio -# Игнорируем весь lwip +# components/lwip is a complete local ESP-IDF component override and must be +# kept in source archives/repositories. Ignoring it would make a fresh checkout +# impossible to build. + +# Local IDE configuration +.vscode/ + +# Windows download metadata +*:Zone.Identifier + +# Keep only the modified lwIP source file components/lwip/** -# Но разрешаем нужные папки !components/lwip/ !components/lwip/lwip/ !components/lwip/lwip/src/ !components/lwip/lwip/src/core/ !components/lwip/lwip/src/core/ipv4/ +!components/lwip/lwip/src/core/ipv4/etharp.c +# Keep only the modified lwIP source file +components/lwip/** -# И сам файл -!components/lwip/lwip/src/core/ipv4/etharp.c \ No newline at end of file +!components/lwip/ +!components/lwip/lwip/ +!components/lwip/lwip/src/ +!components/lwip/lwip/src/core/ +!components/lwip/lwip/src/core/ipv4/ +!components/lwip/lwip/src/core/ipv4/etharp.c + +# Keep only the modified lwIP source file +components/lwip/** + +!components/lwip/ +!components/lwip/lwip/ +!components/lwip/lwip/src/ +!components/lwip/lwip/src/core/ +!components/lwip/lwip/src/core/ipv4/ +!components/lwip/lwip/src/core/ipv4/etharp.c diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json deleted file mode 100644 index 33cd4d7..0000000 --- a/.vscode/c_cpp_properties.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "configurations": [ - { - "name": "ESP-IDF", - "compilerPath": "C:\\Espressif\\tools\\xtensa-esp-elf\\esp-15.2.0_20251204\\xtensa-esp-elf\\bin\\xtensa-esp32-elf-gcc.exe", - "compileCommands": [ - "${config:idf.buildPath}/compile_commands.json" - ], - "intelliSenseMode": "gcc-x86", - "browse": { - "path": [ - "${workspaceFolder}" - ], - "limitSymbolsToIncludedHeaders": true - } - } - ], - "version": 4 -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 3694ae4..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "gdbtarget", - "request": "attach", - "name": "Eclipse CDT GDB Adapter" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 4d8c984..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "C_Cpp.intelliSenseEngine": "default", - "idf.extensionActivationMode": "always", - "idf.openOcdConfigs": [ - "board/esp32-wrover-kit-3.3v.cfg" - ], - "idf.currentSetup": "C:\\esp\\v6.0.2\\esp-idf", - "idf.customExtraVars": { - "IDF_TARGET": "esp32" - }, - "clangd.path": "C:\\Espressif\\tools\\esp-clang\\esp-20.1.1_20250829\\esp-clang\\bin\\clangd.exe", - "clangd.arguments": [ - "--background-index", - "--query-driver=**", - "--compile-commands-dir=c:\\Users\\Maxpr\\monitoring\\build" - ] -} diff --git a/CMakeLists.txt b/CMakeLists.txt index 157ba0f..46d69b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,5 +2,7 @@ # CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.22) +set(COMPONENTS main) + include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(monitoring) diff --git a/README.md b/README.md index f0b7787..0a9ebac 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,43 @@ -# ARP Monitoring for ESP32 +# ARP Monitoring (ESP-IDF / Linux TAP) -> Практическая работа по анализу и модификации сетевого стека **lwIP** в среде **ESP-IDF**. +Практическая работа по анализу и модификации сетевого стека **lwIP** в +**ESP-IDF**. Проект перехватывает входящие ARP-пакеты, хранит обнаруженные узлы +в оперативной памяти, показывает их во встроенном веб-интерфейсе и позволяет +проверять выбранные адреса с помощью ICMP Echo (Ping). -## Описание - -Проект представляет собой систему мониторинга устройств локальной сети на базе **ESP32**. - -В ходе работы выполняется перехват входящих ARP-пакетов на уровне сетевого стека **lwIP**, извлекается информация об обнаруженных устройствах и формируется динамический список сетевых узлов. Полученные данные отображаются через встроенный HTTP-сервер. - -Дополнительно реализована возможность проверки доступности устройств с помощью **ICMP Echo (Ping)**. - -## Возможности - -* Перехват входящих ARP-пакетов. -* Обнаружение устройств локальной сети. -* Хранение списка устройств в оперативной памяти. -* Веб-интерфейс для просмотра обнаруженных устройств. -* Проверка доступности устройств с помощью Ping. +Рабочая конфигурация проверена на **ESP-IDF v6.0.2** с использованием целевой +платформы Linux и TAP-интерфейса без физической платы ESP32. ## Структура проекта -```text -components/ -│ -├── arp_monitor/ Перехват ARP-пакетов -├── device_list/ Работа со списком устройств -├── ping/ Проверка доступности устройств -├── web_server/ HTTP-сервер и веб-интерфейс -└── lwip/ Изменённый файл etharp.c +| Модуль | Назначение | +| --- | --- | +| `components/lwip` | Локальная копия lwIP; в `etharp_input()` установлен перехват ARP | +| `components/arp_monitor` | Принимает IP/MAC из lwIP через очередь FreeRTOS | +| `components/device_list` | Хранит до 64 найденных устройств в RAM | +| `components/web_server` | Отдаёт HTML-таблицу устройств и обрабатывает `/ping` | +| `components/ping` | Запускает стандартный `esp_ping` для выбранных узлов | +| `components/protocol_examples_tapif_io` | Подключает lwIP к Linux TAP без ESP32 | +| `main/main.c` | Отвечает за инициализацию компонентов | -main/ -└── main.c Точка входа приложения -``` \ No newline at end of file +Путь входящего пакета: + +1. TAP передаёт Ethernet-кадр в lwIP. +2. `ethernet_input()` определяет тип кадра `ETHTYPE_ARP` и вызывает + `etharp_input()` из `components/lwip/lwip/src/core/ipv4/etharp.c`. +3. После разбора ARP-заголовка `etharp_input()` вызывает + `arp_monitor_report()` с IP- и MAC-адресом отправителя. +4. `arp_monitor_report()` копирует данные в неблокирующую очередь FreeRTOS. +5. Задача `arp_monitor_task` обновляет статический массив `device_list` в RAM. +6. HTTP-обработчик читает актуальный список и формирует HTML-таблицу. + +## Веб-интерфейс + +![Веб-интерфейс ARP Monitoring](docs/web-interface.png) + +## Примечание + +Текущая версия полностью проверена на целевой платформе Linux с использованием +TAP-интерфейса без физической платы. Для запуска на ESP32 необходимо +дополнительно реализовать инициализацию Wi-Fi и подключение устройства к +локальной сети. diff --git a/components/arp_monitor/CMakeLists.txt b/components/arp_monitor/CMakeLists.txt index dafc5a5..9ca012b 100644 --- a/components/arp_monitor/CMakeLists.txt +++ b/components/arp_monitor/CMakeLists.txt @@ -3,4 +3,5 @@ idf_component_register( INCLUDE_DIRS "include" REQUIRES device_list -) \ No newline at end of file + lwip +) diff --git a/components/arp_monitor/arp_monitor.c b/components/arp_monitor/arp_monitor.c index b7ce3e7..142e496 100644 --- a/components/arp_monitor/arp_monitor.c +++ b/components/arp_monitor/arp_monitor.c @@ -20,7 +20,7 @@ void arp_monitor_init(void) arp_queue = xQueueCreate(ARP_QUEUE_LENGTH, sizeof(arp_event_t)); if (arp_queue == NULL) { - ESP_LOGE(TAG, "Failde to create ARP queue"); + ESP_LOGE(TAG, "Failed to create ARP queue"); return; } ESP_LOGI(TAG, "ARP queue created"); @@ -46,6 +46,11 @@ void arp_monitor_init(void) bool arp_monitor_post_event(const arp_event_t *event) { + if (event == NULL) + { + return false; + } + if (arp_queue == NULL) { ESP_LOGE(TAG, "ARP queue isn't initialized"); @@ -61,6 +66,21 @@ bool arp_monitor_post_event(const arp_event_t *event) return true; } +void arp_monitor_report(const ip4_addr_t *ip, const struct eth_addr *mac) +{ + if ((ip == NULL) || (mac == NULL)) + { + return; + } + + arp_event_t event = { + .ip = *ip + }; + + memcpy(event.mac, mac->addr, ETH_HWADDR_LEN); + arp_monitor_post_event(&event); +} + static void process_arp_event(const arp_event_t *event) { device_info_t device = { @@ -88,4 +108,4 @@ static void arp_monitor_task(void *arg) process_arp_event(&event); } } -} \ No newline at end of file +} diff --git a/components/arp_monitor/include/arp_monitor.h b/components/arp_monitor/include/arp_monitor.h index 7b5acbc..94a40d5 100644 --- a/components/arp_monitor/include/arp_monitor.h +++ b/components/arp_monitor/include/arp_monitor.h @@ -13,5 +13,6 @@ typedef struct void arp_monitor_init(void); bool arp_monitor_post_event(const arp_event_t *event); +void arp_monitor_report(const ip4_addr_t *ip, const struct eth_addr *mac); -#endif \ No newline at end of file +#endif diff --git a/components/device_list/CMakeLists.txt b/components/device_list/CMakeLists.txt index f8a54b7..c185026 100644 --- a/components/device_list/CMakeLists.txt +++ b/components/device_list/CMakeLists.txt @@ -1,4 +1,5 @@ idf_component_register( SRCS "device_list.c" INCLUDE_DIRS "include" -) \ No newline at end of file + REQUIRES lwip +) diff --git a/components/device_list/device_list.c b/components/device_list/device_list.c index 435fd41..b336e81 100644 --- a/components/device_list/device_list.c +++ b/components/device_list/device_list.c @@ -34,6 +34,11 @@ device_info_t *device_list_find_by_mac(const struct eth_addr *mac) bool device_list_add_or_update(const device_info_t *device) { + if (device == NULL) + { + return false; + } + device_info_t *existing_device = device_list_find_by_mac(&device->mac); if (existing_device != NULL) @@ -73,4 +78,4 @@ const device_info_t *device_list_get_by_index(size_t index) } return &device_list[index]; -} \ No newline at end of file +} diff --git a/components/lwip/lwip/src/core/ipv4/etharp.c b/components/lwip/lwip/src/core/ipv4/etharp.c index cb037d0..32769dd 100644 --- a/components/lwip/lwip/src/core/ipv4/etharp.c +++ b/components/lwip/lwip/src/core/ipv4/etharp.c @@ -57,17 +57,8 @@ #include "netif/ethernet.h" #include -#include "arp_monitor.h" -static inline void arp_monitor_report(const ip4_addr_t *ip, const struct eth_addr *mac) -{ - arp_event_t event; - - event.ip = *ip; - memcpy(event.mac, mac->addr, ETH_HWADDR_LEN); - - arp_monitor_post_event(&event); -} +void arp_monitor_report(const ip4_addr_t *ip, const struct eth_addr *mac); #ifdef LWIP_HOOK_FILENAME #include LWIP_HOOK_FILENAME diff --git a/components/ping/ping.c b/components/ping/ping.c index 167bc0c..b7abea7 100644 --- a/components/ping/ping.c +++ b/components/ping/ping.c @@ -8,7 +8,6 @@ #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" -static const char *TAG = "ping"; typedef struct { @@ -16,7 +15,7 @@ typedef struct SemaphoreHandle_t finished; } ping_context_t; -static bool parse_mac_address(const char *text, struct eth_addr * mac); +static bool parse_mac_address(const char *text, struct eth_addr *mac); static void ping_device(device_info_t *device); void ping_selected_devices(const char *query); static void ping_success_callback(esp_ping_handle_t handle, void *arguments); @@ -25,11 +24,11 @@ static void ping_end_callback(esp_ping_handle_t handle, void *arguments); static bool parse_mac_address(const char *text, struct eth_addr *mac) { - if ((text == NULL) || (mac == NULL)) + if ((text == NULL) || (mac == NULL) || (strlen(text) != 12)) return false; return (sscanf(text, - "%2hhX:%2hhX:%2hhX:%2hhX:%2hhX:%2hhX", + "%2hhX%2hhX%2hhX%2hhX%2hhX%2hhX", &mac->addr[0], &mac->addr[1], &mac->addr[2], @@ -40,6 +39,9 @@ static bool parse_mac_address(const char *text, struct eth_addr *mac) void ping_selected_devices(const char *query) { + if (query == NULL) + return; + const char *current = query; while ((current = strstr(current, "mac=")) != NULL) @@ -47,7 +49,7 @@ void ping_selected_devices(const char *query) struct eth_addr mac; device_info_t *device; - char mac_string[18]; + char mac_string[13]; size_t length; current += 4; @@ -110,10 +112,17 @@ static void ping_device(device_info_t *device) if (esp_ping_new_session(&config, &callbacks, &ping) != ESP_OK) { vSemaphoreDelete(context.finished); + device->active = false; return; } - esp_ping_start(ping); + if (esp_ping_start(ping) != ESP_OK) + { + esp_ping_delete_session(ping); + vSemaphoreDelete(context.finished); + device->active = false; + return; + } xSemaphoreTake(context.finished, portMAX_DELAY); @@ -135,4 +144,4 @@ static void ping_end_callback(esp_ping_handle_t handle, void *args) { ping_context_t *context = (ping_context_t *)args; xSemaphoreGive(context->finished); -} \ No newline at end of file +} diff --git a/components/protocol_examples_tapif_io/CMakeLists.txt b/components/protocol_examples_tapif_io/CMakeLists.txt new file mode 100644 index 0000000..140acc5 --- /dev/null +++ b/components/protocol_examples_tapif_io/CMakeLists.txt @@ -0,0 +1,9 @@ +idf_build_get_property(target IDF_TARGET) + +if(${target} STREQUAL "linux") + idf_component_register(INCLUDE_DIRS include + SRCS linux/tapio.c linux_connect.c lwip/tapif.c + PRIV_REQUIRES esp_netif lwip) +else() + message(FATAL_ERROR "This component is currently only supported for linux target") +endif() diff --git a/components/protocol_examples_tapif_io/Kconfig.projbuild b/components/protocol_examples_tapif_io/Kconfig.projbuild new file mode 100644 index 0000000..ee60079 --- /dev/null +++ b/components/protocol_examples_tapif_io/Kconfig.projbuild @@ -0,0 +1,63 @@ +menu "Example Connection Configuration" + + config EXAMPLE_CONNECT_LWIP_TAPIF + bool "connect using lwip to linux tap interface" + depends on IDF_TARGET_LINUX && ESP_NETIF_TCPIP_LWIP + default y + + if EXAMPLE_CONNECT_LWIP_TAPIF + config EXAMPLE_CONNECT_IPV4 + bool + depends on LWIP_IPV4 + default y + + config EXAMPLE_CONNECT_IPV6 + bool "Obtain IPv6 address" + depends on LWIP_IPV6 + default y + help + Set to true to setup link local address and wait until it's valid + + config EXAMPLE_CONNECT_WAIT_FOR_IP + bool "run DHCP and wait for IP" + default y + + config EXAMPLE_CONNECT_TAPIF_IP_ADDR + string "Static IP address" + default "192.168.5.100" + depends on EXAMPLE_CONNECT_IPV4 && !EXAMPLE_CONNECT_WAIT_FOR_IP + help + Set static IP address. + + config EXAMPLE_CONNECT_TAPIF_NETMASK + string "Static netmask address" + default "255.255.255.0" + depends on EXAMPLE_CONNECT_IPV4 && !EXAMPLE_CONNECT_WAIT_FOR_IP + help + Set static netmask address. + + config EXAMPLE_CONNECT_TAPIF_GW + string "Static gateway address" + default "192.168.5.1" + depends on EXAMPLE_CONNECT_IPV4 && !EXAMPLE_CONNECT_WAIT_FOR_IP + help + Set static gateway address. + + config EXAMPLE_CONNECT_TAPIF_OUT_LOSS + int "Percentage of packets to be dropped on transmission" + default 0 + range 0 100 + help + Set non-zero number simulate packet loss when sending data. + Number represents probability between 0 and 100% + + config EXAMPLE_CONNECT_TAPIF_IN_LOSS + int "Percentage of packets to be dropped on reception" + default 0 + range 0 100 + help + Set non-zero number simulate packet loss when receiving data. + Number represents probability between 0 and 100% + endif + +endmenu diff --git a/components/protocol_examples_tapif_io/README.md b/components/protocol_examples_tapif_io/README.md new file mode 100644 index 0000000..3ac36ea --- /dev/null +++ b/components/protocol_examples_tapif_io/README.md @@ -0,0 +1,161 @@ +# tapif-io Component + +This component implements a tap networking interface that provides connectivity to host network using `tuntap` interface in Linux. +It could be used to route lwip traffic to host side network, typically when working with the **Linux target**. + +## How to use this component + +### Usage of the API + +1) Add the path to this component to as a dependency to the `main` component of your project using the following idf_component.yml file: +```yaml +dependencies: + tapif_io: + path: ${IDF_PATH}/examples/common_components/tapif_io +``` +2) Include lwip and linux side of the configuration +```cpp +#include "esp_netif.h" // esp-netif +#include "tapio.h" // esp-netif's driver side +#include "lwip/tapif.h" // esp-netif's network stack side +``` +3) Configure the esp-netif + a) setup the linux tap I/O config +```cpp + esp_netif_driver_ifconfig_t driver_cfg = { + .handle = tapio_create(), + .transmit = tapio_output, + }; +``` + + b) configure the lwip netif for the tap interface +```cpp + struct esp_netif_netstack_config stack_cfg = { + .lwip = { + .init_fn = lwip_tapif_init, + .input_fn = lwip_tapif_input, + } + }; +``` + + c) configure the esp-netif basic parameters +```cpp + esp_netif_inherent_config_t base_cfg = { + .if_key = "TAP", // unique name of the interface + .flags = ESP_NETIF_FLAG_AUTOUP, // no dhcp client, starts when it's set up + .ip_info = &ip_info, // add static IP info + .route_prio = 100 // priority for setting default gateway + }; +``` + + +4) Initialize and attach the esp_netif to the I/O handle +```cpp + esp_netif_t *tap_netif = esp_netif_new(&cfg); + esp_netif_attach(tap_netif, driver_cfg.handle); +``` + +### Host side networking + +1) Create a new tun/tap interface type named `tap0` + a) You can run the script `./make_tap_netif` + b) Update the IP address of the interface to correspond to the configured static IP in previous step + +2) Start the application and send/receive the packets via `tap0` interface + * it is possible to create server or client test application listening or connecting to this interface. + * it is also possible to route these packets to external network (using routing rules or simply by ip forwarding if using the same subnet) + +#### Common networking/routing examples + +##### Isolated internal connection + +Is useful to experiment with one interface with no intention to connect to internet or external facilities. +Typically, when we want to create a server listening on the `tap0` interface and run a client in lwip, e.g. the default `tcp_client` socket example in IDF. +* Create the tap interface using `./make_tap_netif` and set the IP address **not to overlap** with any other IPv4 network range (e.g. `ip addr add 192.168.5.1/24 dev tap0`) +* Configure the `tapif_io` component to use static address from that range (e.g. `192.168.5.x`) +* Configure the `tcp_client` example to connect to the tap interface IP address (e.g. `192.168.5.1`) +* Execute a tcp server listening on the tap interface and the configured port (e.g. `nc -l 3333`) +* Build and run the `tcp_client` example to send and receive data between the server created in the previous step. + +##### Connecting to the external network using IP forwarding + +This allows using full-featured network facilities of your host network, but a care must be taken to the selected IP addresses to avoid potential conflicts. +* Set the IP address of the `tap0` interface from the range used by your host system's default gateway (e.g. `ip addr add 192.168.0.123/24 dev tap0`, assuming the default netif is `eth0` with IP range of `192.168.0.x` and this address doesn't overlap with any other IP address in this network) +* Configure the `tapif_io` with another address from the same range, e.g. +```text +CONFIG_EXAMPLE_CONNECT_TAPIF_IP_ADDR="192.168.0.100" +CONFIG_EXAMPLE_CONNECT_TAPIF_NETMASK="255.255.255.0" +CONFIG_EXAMPLE_CONNECT_TAPIF_GW="192.168.0.1" +``` +assuming that the default gateway of your host network is configured to `192.168.0.1` +* Build and run the lwip example to interact with the host network, e.g, to send an HTTP request to a publicly available http server (if the server is reachable from your host network) + +(Note, that the IP forwarding must be enabled in the host system: +```bash +echo 1 > /proc/sys/net/ipv4/ip_forward +``` +) + +##### Routing the internal interface to the host network with IP tables + +Uses an isolated interface with routing and NAT-ing between interfaces +* Configure the `tap0` interface address **not to overlap** with any other IPv4 network range. +* Setup `MASQUERADE` target to route network traffic between `tap0` and your default network interface (`eth0` in the below example). +```bash +sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE +sudo iptables -A FORWARD -i eth0 -o tap0 -m state --state RELATED,ESTABLISHED -j ACCEPT +sudo iptables -A FORWARD -i tap0 -o eth0 -j ACCEPT +``` + +##### Using DHCP + +It's also possible to configure the lwip interface to use DHCP client (common setup for most default network interfaces, such as Ethernet or WiFi station) +and set up a DHCP server on the host machine to assign the IP address dynamically. + +This component sets up a DHCP client if `CONFIG_EXAMPLE_CONNECT_WAIT_FOR_IP` is enabled and waits for assigning an IP address. See below the description of DHCP client workflow for tap interface: + +1) **Configure and set the `esp-netif` up** + +* Same as in [API usage](#Usage-of-the-API), but update the base esp-netif config `3c)` to enable DHCP client +```cpp + esp_netif_inherent_config_t base_cfg = { + .if_key = "TAP", + .flags = (esp_netif_flags_t)(ESP_NETIF_DHCP_CLIENT | ESP_NETIF_FLAG_EVENT_IP_MODIFIED | ESP_NETIF_FLAG_AUTOUP), + .route_prio = 100 + }; +``` +* After starting the netif, tell the lwIP that we're connected +```cpp + esp_netif_action_connected(tap_netif, 0, 0, 0); +``` +* Wait for the IP address to be assigned. +This could be implemented using an event handler +```cpp + esp_netif_inherent_config_t base_cfg = { + ... + .get_ip_event = TAP0_GOT_IP, + ... + }; + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, TAP0_GOT_IP, event_handler, NULL)); + // wait for the IP event (e.g. using signalling semaphores from the handler) + // ... + esp_netif_ip_info_t ip_info = {}; + ESP_LOGI("tap-init", "Assigned IP address:"IPSTR ",", IP2STR(&ip_info.ip)); +``` + +2) **Configure forwarding/routing** if needed based on the previous sections. + +3) **Configure the DHCP server on the host machine** + +Example for `isc-dhcp-server` + +```bash +INTERFACES="tap0"; +authoritative; + +subnet 192.168.5.0 netmask 255.255.255.0 { + range 192.168.5.2 192.168.5.200; + option routers 192.168.5.1; + option domain-name-servers 8.8.8.8; +} +``` diff --git a/components/protocol_examples_tapif_io/include/protocol_examples_common.h b/components/protocol_examples_tapif_io/include/protocol_examples_common.h new file mode 100644 index 0000000..3ebfd2a --- /dev/null +++ b/components/protocol_examples_tapif_io/include/protocol_examples_common.h @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* Common functions for protocol examples, to establish tap interface connection + * For linux target + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. + */ +#pragma once +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Configure and connect, wait for IP + * + * @return ESP_OK on successful connection + */ +esp_err_t example_connect(void); + + +#ifdef __cplusplus +} +#endif diff --git a/components/protocol_examples_tapif_io/include/tapio.h b/components/protocol_examples_tapif_io/include/tapio.h new file mode 100644 index 0000000..f646d75 --- /dev/null +++ b/components/protocol_examples_tapif_io/include/tapio.h @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once +#include "esp_err.h" +#include "esp_netif.h" + +/** + * @brief Creates tapio layer as a driver interface to esp-netif + * + * @warning Implemented as singleton, can use only one tapio in the system! + * + * @return pointer to the tapio driver handle + */ +void *tapio_create(void); + +/** + * @brief esp-netif driver I/O output path + * + * @param h Driver's handle + * @param buffer Data to output + * @param len Data size + * @return ESP_OK on success + */ +esp_err_t tapio_output(void *h, void *buffer, size_t len); diff --git a/components/protocol_examples_tapif_io/linux/tapio.c b/components/protocol_examples_tapif_io/linux/tapio.c new file mode 100644 index 0000000..4d46f9f --- /dev/null +++ b/components/protocol_examples_tapif_io/linux/tapio.c @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include "esp_err.h" +#include "esp_log.h" +#include "esp_netif.h" + +// Use linux system sockets to connect to tap interface +#define LWIP_HDR_LINUX_SYS_SOCKETS_H +#include +#include + +#include +#include +#include +#include +#include "errno.h" + +#include +#include + +#define DEVTAP "/dev/net/tun" +#define DEVTAP_NAME "tap0" + + +typedef struct tap_io { + esp_netif_driver_base_t base; + int fd; +} tap_io_t; + +static const char *TAG = "tap-netif"; + +static void tapio_input_task(void *arg) +{ + tap_io_t *io = arg; + fd_set fdset; + int ret; + while (1) { + FD_ZERO(&fdset); + FD_SET(io->fd, &fdset); + + /* Wait for a packet to arrive. */ + ret = select(io->fd + 1, &fdset, NULL, NULL, NULL); + + if (ret == 1) { + /* Handle incoming packet. */ + ssize_t readlen; + char buf[1518]; /* max packet size including VLAN excluding CRC */ + + /* Obtain the size of the packet and put it into the "len" + variable. */ + readlen = read(io->fd, buf, sizeof(buf)); + if (readlen < 0) { + ESP_LOGE(TAG, "Failed to read from tap fd: returned %ld", readlen); + exit(1); + } +#if CONFIG_EXAMPLE_CONNECT_TAPIF_IN_LOSS + if (((double)rand()/(double)RAND_MAX) < ((double)CONFIG_EXAMPLE_CONNECT_TAPIF_IN_LOSS)/100.0) { + ESP_LOGW(TAG, "Simulated packet drop on input"); + continue; + } +#endif + esp_netif_receive(io->base.netif, buf, readlen, NULL); + } else if (ret == -1) { + if (errno == EINTR /* Interrupted system call (used by FreeRTOS simulated interrupts) */) { + vTaskDelay(1); // yield to the FreeRTOS simulator + } else { + ESP_LOGE(TAG, "tapif_thread: select() error(%d), %s", errno, strerror(errno)); + } + } + } +} + +static esp_err_t tapio_start(esp_netif_t *esp_netif, void *arg) +{ + tap_io_t *io = arg; + io->base.netif = esp_netif; + esp_netif_action_start(esp_netif, 0, 0, 0); + + sys_thread_new("tapio_rx", tapio_input_task, io, DEFAULT_THREAD_STACKSIZE, DEFAULT_THREAD_PRIO); + + return ESP_OK; +} + +void *tapio_create(void) +{ + static tap_io_t tap_io = {}; + tap_io.base.post_attach = tapio_start; + tap_io.fd = open(DEVTAP, O_RDWR); + if (tap_io.fd == -1) { + ESP_LOGE(TAG, "Cannot open tap device %s", DEVTAP); + return NULL; + } + + struct ifreq ifr = {}; + memset(&ifr, 0, sizeof(ifr)); + + strncpy(ifr.ifr_name, DEVTAP_NAME, sizeof(ifr.ifr_name)); + ifr.ifr_name[sizeof(ifr.ifr_name)-1] = 0; /* ensure \0 termination */ + ifr.ifr_flags = IFF_TAP|IFF_NO_PI; + if (ioctl(tap_io.fd, TUNSETIFF, (void *) &ifr) < 0) { + ESP_LOGE(TAG, "Cannot configure ioctl(TUNSETIFF) for \"%s\"", DEVTAP); + return NULL; + } + + + return &tap_io; +} + +esp_err_t tapio_output(void *h, void *buffer, size_t len) +{ + tap_io_t *io = h; + ssize_t written; + +#if CONFIG_EXAMPLE_CONNECT_TAPIF_OUT_LOSS + if (((double)rand()/(double)RAND_MAX) < ((double)CONFIG_EXAMPLE_CONNECT_TAPIF_OUT_LOSS)/100.0) { + ESP_LOGW(TAG, "Simulated packet drop on output"); + return ESP_OK; /* ESP_OK because we simulate packet loss on cable */ + } +#endif + + /* signal that packet should be sent(); */ + written = write(io->fd, buffer, len); + if (written < len) { + ESP_LOGE(TAG, "Failed to write from tap fd: returned %ld", written); + return ESP_FAIL; + } + return ESP_OK; +} diff --git a/components/protocol_examples_tapif_io/linux_connect.c b/components/protocol_examples_tapif_io/linux_connect.c new file mode 100644 index 0000000..a7fce1e --- /dev/null +++ b/components/protocol_examples_tapif_io/linux_connect.c @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "esp_err.h" +#include "esp_netif.h" // esp-netif +#include "tapio.h" // esp-netif's driver side +#include "lwip/tapif.h" // esp-netif's network stack side +#include "esp_log.h" + +#if CONFIG_EXAMPLE_CONNECT_WAIT_FOR_IP +#include "esp_event.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" + +static const char *TAG = "linux_connect"; + +static EventGroupHandle_t s_events; + +static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) +{ + xEventGroupSetBits(s_events, 1); +} + +#endif // CONFIG_EXAMPLE_CONNECT_WAIT_FOR_IP + +#define TAP0_GOT_IP (0x1234) + +esp_err_t example_connect(void) +{ +#if CONFIG_EXAMPLE_CONNECT_LWIP_TAPIF + // configure linux tapio + esp_netif_driver_ifconfig_t driver_cfg = { + .handle = tapio_create(), + .transmit = tapio_output, + }; + // configure lwip netif for the tapif + struct esp_netif_netstack_config stack_cfg = { + .lwip = { + .init_fn = lwip_tapif_init, + .input_fn = lwip_tapif_input, + } + }; + // configure inherent esp-netif parameters + esp_netif_ip_info_t ip_info = {}; + esp_netif_flags_t netif_flags = (ESP_NETIF_FLAG_EVENT_IP_MODIFIED | ESP_NETIF_FLAG_AUTOUP); +#if !CONFIG_EXAMPLE_CONNECT_WAIT_FOR_IP + ip_info.ip.addr = ipaddr_addr(CONFIG_EXAMPLE_CONNECT_TAPIF_IP_ADDR); + ip_info.netmask.addr = ipaddr_addr(CONFIG_EXAMPLE_CONNECT_TAPIF_NETMASK); + ip_info.gw.addr = ipaddr_addr(CONFIG_EXAMPLE_CONNECT_TAPIF_GW); +#else + netif_flags |= ESP_NETIF_DHCP_CLIENT; +#endif + + esp_netif_inherent_config_t base_cfg = { + .if_key = "TAP", + .ip_info = &ip_info, + .flags = netif_flags, + .get_ip_event = TAP0_GOT_IP, + .route_prio = 100 + }; + + // put all configs together + esp_netif_config_t cfg = { + .base = &base_cfg, + .driver = &driver_cfg, + .stack = &stack_cfg + }; + + // create the interface and attach it to the tapio-handle + esp_netif_t *tap_netif = esp_netif_new(&cfg); + esp_netif_attach(tap_netif, driver_cfg.handle); +#if CONFIG_EXAMPLE_CONNECT_WAIT_FOR_IP + ESP_LOGI(TAG, "Waiting for IP addresses..."); + esp_netif_action_connected(tap_netif, 0, 0, 0); + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, TAP0_GOT_IP, event_handler, NULL)); + s_events = xEventGroupCreate(); + xEventGroupWaitBits(s_events, 1, pdFALSE, pdFALSE, portMAX_DELAY); + esp_netif_get_ip_info(tap_netif, &ip_info); + ESP_LOGI(TAG, "Assigned IP address:"IPSTR ",", IP2STR(&ip_info.ip)); +#endif // CONFIG_EXAMPLE_CONNECT_WAIT_FOR_IP +#endif // EXAMPLE_CONNECT_LWIP_TAPIF + return ESP_OK; +} diff --git a/components/protocol_examples_tapif_io/lwip/tapif.c b/components/protocol_examples_tapif_io/lwip/tapif.c new file mode 100644 index 0000000..a96ece6 --- /dev/null +++ b/components/protocol_examples_tapif_io/lwip/tapif.c @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2001-2003 Swedish Institute of Computer Science + * + * SPDX-License-Identifier: BSD-3-Clause + * + * SPDX-FileContributor: 2022-2023 Espressif Systems (Shanghai) CO LTD + */ +#include +#include "lwip/opt.h" +#include "lwip/pbuf.h" +#include "lwip/snmp.h" +#include "lwip/ethip6.h" +#include "netif/etharp.h" + +#include "esp_netif.h" +#include "esp_netif_net_stack.h" + +#define IFNAME0 't' +#define IFNAME1 'p' + +static err_t +low_level_output(struct netif *netif, struct pbuf *p) +{ + esp_netif_t *esp_netif = esp_netif_get_handle_from_netif_impl(netif); + char buf[1518]; /* max packet size including VLAN excluding CRC */ + + if (p->tot_len > sizeof(buf)) { + MIB2_STATS_NETIF_INC(netif, ifoutdiscards); + LWIP_DEBUGF(NETIF_DEBUG, ("tapif: packet too large")); + return ERR_IF; + } + + /* initiate transfer(); */ + pbuf_copy_partial(p, buf, p->tot_len, 0); + + int ret = esp_netif_transmit(esp_netif, buf, p->tot_len); + /* Check error */ + if (likely(ret == ESP_OK)) { + return ERR_OK; + } + if (ret == ESP_ERR_NO_MEM) { + return ERR_MEM; + } + return ERR_IF; +} + +static void +low_level_init(struct netif *netif) +{ + + /* Obtain MAC address from network interface. */ + netif->hwaddr[0] = 0x02; + netif->hwaddr[1] = 0x12; + netif->hwaddr[2] = 0x34; + netif->hwaddr[3] = 0x56; + netif->hwaddr[4] = 0x78; + netif->hwaddr[5] = 0xab; + netif->hwaddr_len = 6; + + /* device capabilities */ + netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_IGMP; +} + +err_t lwip_tapif_init(struct netif *netif) +{ + LWIP_ASSERT("Tried to initialize tapif with NULL netif", netif != NULL); + netif->name[0] = IFNAME0; + netif->name[1] = IFNAME1; +#if LWIP_IPV4 + netif->output = etharp_output; +#endif /* LWIP_IPV4 */ +#if LWIP_IPV6 + netif->output_ip6 = ethip6_output; +#endif /* LWIP_IPV6 */ + netif->linkoutput = low_level_output; + netif->mtu = 1500; + + low_level_init(netif); + netif_set_link_up(netif); + return ERR_OK; +} + +esp_err_t lwip_tapif_input(void *h, void *buffer, size_t len, void *l2_buff) +{ + struct netif *netif = h; + struct pbuf *p; + LWIP_ASSERT("running tapif input with NULL netif", netif != NULL); + + p = pbuf_alloc(PBUF_RAW, len, PBUF_RAM); + if (p == NULL) { + return ESP_ERR_NO_MEM; + } + memcpy(p->payload, buffer, len); + + /* full packet send to tcpip_thread to process */ + if (unlikely(netif->input(p, netif) != ERR_OK)) { + LWIP_DEBUGF(NETIF_DEBUG, ("tapif_input: IP input error\n")); + pbuf_free(p); + return ESP_FAIL; + } + + return ESP_OK; +} diff --git a/components/protocol_examples_tapif_io/lwip/tapif.h b/components/protocol_examples_tapif_io/lwip/tapif.h new file mode 100644 index 0000000..aae29e7 --- /dev/null +++ b/components/protocol_examples_tapif_io/lwip/tapif.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2001-2003 Swedish Institute of Computer Science + * + * SPDX-License-Identifier: BSD-3-Clause + * + * SPDX-FileContributor: 2022-2023 Espressif Systems (Shanghai) CO LTD + */ +#pragma once +#include "lwip/esp_netif_net_stack.h" + +/** + * @brief lwip netif init API + * @param netif pointer to lwip's netif + * @return ERR_OK on success + */ +err_t lwip_tapif_init(struct netif *netif); + +/** + * @brief Input data path + * @param h pointer to network stack handle (stuct netif* in our case) + * @param buffer Data + * @param len Data size + * @param l2_buff Data L2 buffer + */ +esp_err_t lwip_tapif_input(void *h, void *buffer, size_t len, void *l2_buff); diff --git a/components/protocol_examples_tapif_io/make_tap_netif b/components/protocol_examples_tapif_io/make_tap_netif new file mode 100755 index 0000000..3857edb --- /dev/null +++ b/components/protocol_examples_tapif_io/make_tap_netif @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +sudo ip tuntap add dev tap0 mode tap user `whoami` +sudo ip link set tap0 up +sudo ip addr add 192.168.5.1/24 dev tap0 diff --git a/components/web_server/web_server.c b/components/web_server/web_server.c index 2cdf12f..0b405ca 100644 --- a/components/web_server/web_server.c +++ b/components/web_server/web_server.c @@ -3,6 +3,7 @@ #include "ping.h" #include "esp_http_server.h" #include "esp_log.h" +#include static const char *TAG = "web_server"; @@ -88,7 +89,7 @@ static esp_err_t main_page_handler(httpd_req_t *req) return err; } - return httpd_resp_sendstr_chunk(req, NULL);; + return httpd_resp_sendstr_chunk(req, NULL); } static esp_err_t send_page_header(httpd_req_t *req) @@ -123,6 +124,9 @@ static esp_err_t send_device_table(httpd_req_t *req) "" ); + if (err != ESP_OK) + return err; + for (size_t index = 0; index < device_list_count(); index++) { const device_info_t *current_device; @@ -156,7 +160,7 @@ static esp_err_t send_device_row(httpd_req_t *req, const device_info_t *device) "" "" - "" + "" "" "%u.%u.%u.%u" "%02X:%02X:%02X:%02X:%02X:%02X" @@ -210,4 +214,4 @@ static esp_err_t ping_handler(httpd_req_t *req) } return main_page_handler(req); -} \ No newline at end of file +} diff --git a/docs/web-interface.png b/docs/web-interface.png new file mode 100644 index 0000000000000000000000000000000000000000..1035dcb8518e9dd4c92d35fa252ba42bcc4895c9 GIT binary patch literal 50365 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 z zta*X^bOYD)f`}qtd9QEfFL(M;q{NV<C8^RC7a!MfZDv1pV zb-Lg~PDxBk)^f-dfU9ySVLGjdFvHb~-5q%nf+r~;!RFlLjZSkPp2C!rwq)rmM#?hy zv%E*Uibh;skw{exePDGF9$iaYExQ1{5r=~)4IMsqI&jy-N3a(iwZmxnsKd$nK=0lb z57xU&|A*CvZD-p#nUOD{y(edlYynW z3lY|N4xWJWz2z@-HHO1!OpoIHTzng|zkQ!@4tu&_jlUMHwpgq4wGQ!>BslE0Qg_#4 z#PVMlnHN2ov@D2_!;WKO7jVC_V#V3rmH5PG?o&h{@W+Z7-%%rQx%k^rkX{lDo*O(E zm%Q&S)X|J$VN@xc1nVx}_h=u_J25ZPQ0|>(bImHfE`CgFILzq`5*Y?r@NorKJdz;e zQuIGE_H}I8ifWwXd@1`@B!s54#S>9PCErt@*v`>^AL?Kn5sv^u%OuF$KDC5uGzPoir_`#x8alsTI|I2L>U4V|1_X5QZ z->XaH%&%fi5el>m<;f9-!ZfudEkw|rLM@Auf`K@8uZD<8CVh;u+y%i~l2~W0$Ksc} zJQ=z(+0X*BF>4Yz>g6&8+lZd(F@!du@LOpmm*5a;y*6-t8XVDaPgZZfo7_p_8NZZ& zqLfVa{pl((J}B)H{`1$#^1$L3`+@v~@nl z0Y80SGIp=?&F1-K0sH3V=nRd)A%<&W?QE+#<tf@bJ!stGd z0IX7aGOFrBj-=VeLGWU%ZZ8^0D)icUt?tN-UG2sF&s)Z_BQ-oRQhxkZZ))(8lJ!a4 zc3sWx8m7(7PW1auQM88vo?Q- zl41{YpW14P{t8j1d!%yB0?i5$ZKpIxQjJJ#LvL({p7-Y2#_Os!oW&+u>Pgwo;4d&! zc|xBqP4|NdP+P`-uE!!&!er?*jQrcnbVCVUNj17(X^{uhy;VwoI$E-HuRB47LiR4= zA$`k2gsH-=w-3ao)Yx#mQ!wa#xGjJ9Ga&`>8y0^E`OCXI5V6FkzafuQ?Ul-hcrBUU63TFR_x4Ha((yj z#`jL`3ghL(0>P%D!xfs)C^;hZ_*{RvOx4O)SQGZx8_21(x_dg-_graS@~6AMQbu(J zTN6d!Inz|ONX2%A#^ZDll_^6{&>EPe12Haq?7)-be||N5%LdhjLg=9Uieb{Y=#O)6 zx4ffB(2*g$i_?qZ!`te2r@A#h*$?;oVmP#G2fWMZl%rG%dOPui<&^@f*UQp%^R^w` zoAG8F>Lo#Tvm0*I9}rL!*4)s7(Tpi2Suc#;`n7Yl4P+@~r-xAMaTDyX#J7b9BL=cFK&N}5C;ZBPQyZS#eFCnYw!&gS%P_fRnoWF7VS}|zPGMb~ z!+%RTM#tum)3tvF{9+I8SO0Oj`|9p5Q7D0!_Hx1E`N6g9YxMO+opD43Jj0qvNpZ@Q zAQeS5+Y^zg@hpC_#W`20+!Y3`OnsEE1sl&fdRyIrdeg)EXY$ZIya!7KNy>AMaF6W_%l(JWD>K(asuNjUqwp%OxhZxaX;7=ff_7)(>X%`dw0T zy^Yuthbqx*8SjR==Fl|22Wxtz=3@I&$MWRpJD^AZymup^uR(htCBqq0&0$@%odPLb zm4LTsP)UoI1^r1Yx*sW4eenm<;T>n|D*B)C6&RY6x(Rw;%Ab37M7LxABd zf3B{f{i2&%)0=qaL_H=waLQyb5G6?Js^}g1&oe?%Dnzd=aBvPv)X?|K#W@8k7Rlinut@15 zrBZfK<&62POpNQ2LCue5e|m&=c8iqP`k*(=ws2WSqnk z@&gHYu2fI>z!{h4ST|BN8Uq& zDa+t0JZTx#YRgG@m@BP4_ESyOP(B4r0-2_$*+CemeptRYjAFzf*0G=kZMn!t4^ylR zSd)(6XhK1$8nUsTA|So~*sU}EgT3rM0ROSN^F;>UoH`*z4c|FRWWC1~U0IrC`T2~d zE5n*|cQ0D0c*X!O1!|e!lsidZ)`c z4MRs1kP2};Uc0Jg*df`l!_?N|@7$bp@$1WqlY-^R<3zJ-QVmp$3*dkK{~NfHer45IuH*9-8PzOB(@iL$6Wq+ zbJ6?IM`1e35w&zAt)7Zej6^kqk{1;Q#-1~{WyypmF@}sLUpCV(WF8}LeYdMbI~46f!^xbdUlJ6me-nz6K>d}A3C#-4?^c| zTL=wz7oUWn!I0|kL32W>-btEOt<(QJh}5Zaub~>fd=JTMa3d>GgVW+AVfJ7xMk=>V z+A2}hoOj|{Vm$ow)8{|H4E{@ns2Gru4G#^4Dx8Z-q6F;Q$jC^*k$|eKtPIG<$ZW*` z!vkbeEy+mdSGR2m6%rYQ9|<_#D4GhFy*~{V2dalKDz~~Y`0u^EwfQ(DD!wz0$|(pd z_>sUz&pDc`(b5~f)WP=e6tSf<;^bL+X-wI zeHjqofd5+0iZK?I;B$eh!C(-xIzh$OulX@})Cih#=TD){I*IMNM9OPyp;cRNeU)rQ;b>%E8v?eY-<1jdmJ4=h%2ARIlD0Lk+M~v(=4yVR zFQ)h3QOndbN|u+mh@7M=tkj})z`M(zdbBI_!v@AiXz_)z(^JC`W0>5Naj2d zA3IMYT&aHUTpon?_P!_$Wl5O*!l z8rc=~-a{l_K8!sexEk~X?I{e;(~p8LoF5MHQ-Mg-Bo7=4<#KOkECn{Had_Q*bFkH5 z(#S5KR0GI(JP5~!^@UC7Tk_pP~z4! zV&bG8?A59)>mkc{li#LI#|iM{{LXyus|?Yov5G_YY)5Hq-#y`wLp6P}N<~QRhqU_t z*5k{l^Pmri)_Gl0(=UHTXp@4Ml%?AruiQ4QDfh)n2&IFW-t+5c&}+vFe4C5f)6OCY z{jx;V+c$(h<(l4Gb<2*fTQIQw=U90f9zGPBx+8L&LeJ{KH*NUTWZQEFOYV`&m} zrjIyv*;JFeO;u`p0VKMkr?4uqn4nf>JsBxUt7Sj@Xf+YC>E0?|-8zg(Z+ z0M)y_vjYa0V7csYCGlaq&sROhe~I$E3>6beYSzKC7ekUmaMpYLiJl4y9Qp3u-yHhr z>3L8gV&(BV@24OkS9JSy%EfG-1O@dDqEIgxM}yxnuTq(d5jncezUMe%MXhzdL_aq!(7Z3{=c30F({Mu}&bUa21#qr>?>WPo zAHvlTwc3{?9U&|VExzYdx1fgdShad=YtkDuqxa8#s+=0uWx;1;q_Qz`+GxYg$UINgLwMh})i!7Ngg+ z4~*}d-HwTR*cXztMD@aeWr>)H>x|al7m>oj>BNV(^83lOd5c=a9Q+7i466lsX$0gd zaJH4Ov&M-YN?PEa+8A-4gy3uTWH)*>oV87;eVapYKUKcwlU|W^v_k%AiQj1=|C-RTA2TBkctQ802S_N#jLMR5`*RVpw&9u}c(dF8 zh2ZDq3QkgAplAxh<$0>vonOI|qDv4ZYYx$T$2y*?V#D>&xJZDfH4Dw@>rUcQF02cv z@V7z4TgNnb#H^tN^7_gK2FTcH<>@g%5)FXQ5LJuiW_1-p>kG%1-JV%*Wy(EEHgg3J zT^I(2<*_H?$!MB|x|#==^A!hl9wKRdQR76z70KF^;En~)4{xk%ycavPR2bju~ z_&&GU5;a<2)c{cNx!$eq{~OVZ1Tv!V(Vx<{PM#w~+&QG{*}@(l7sl#c8_^ENPn%ke zcpO-PtKmyA$Ewfo+w#907R{oI?J|T@zJ|BL{0nPZzwBau9f~V`k#PZc^Y4LK;P5`> zJ+X7C!taoeZ|6qXajyYVrC>Zm=B1UL4Q@`7MoP@>jrNe(9jok!Dx?SJ3g=zf$g#0( zxGh)F9|UKjdKv*XC69Zj@A%Xd*~i~ZFxojC%e_$lQYT_M6O>FDj0ETZkh0^SExS^3 zagpo2^)+_9E02fv>n_FCf?-r(tX)LV87Err%QHPn_@xUS8_JfBf*aoLy3g(J!bK?M zvvalu``~-lzqUcBk*X4r5W&B76*fzCX!pkKxKG@8@62gUm`gi3kQH6x#KZcv2%e)* zV+!_k8DV3UBc$uGNy^xSe0PD<@{qKD;SSr8ij%;c)W(DTG$s#%S7>RZ;+isU(@l7Z zc>D0c88-S4cqD^Ti51oT#U`@BW7Eo}qJgk5*q{juK0(vKAV{m>I}%XmG1KMko_o!? zSk0r{QTIU0!#;4Q3S((FQjv(~N$9N@ny17dv5`AHPqQ+05wM+vZ)9m-R(TwukXRA!2ew-jg;gZG0q@Hq)Y;F zdN_-@)$7-oApRKnci-1pZ zdR^pL?|K4-nqzVdx)~tN5OR)EMBUV2GkD#484 zsY43C`VR4UX}M`d{L)yB;3|wemBk&z_Xi4fWi| zS6!Ep0DHTg>ZKV&)%4J4D&)N%a>#=!HXa;s!`lY2j{YOOdNXG*R>u0+110?7(B^2% zY5Ng*lk{kQJNVV&6a}q%kW^cT$AY;iryXCjZGd!GHov!$ib@c`2?EAsRmm4>)+D~F zcA>;~?{|J6>;B$$% zKo|%BgFuA#eq*Y@e^cT97MGPp0Ps+DN~!N1>d37O_=Hp}ENBB^NMEolV)*j`xujsn z=|3(B00YQ?nt)vTS8o>AX@C)?0=Uwd1`EaD`iWm<7f*#@Bc7yzCV+W;dv^x_;7Ez~ zV&>4jMkV@y^#;xOulD|riP)3KPA>&%={^952oe9h12Ve*P1|pL zii(IRsi~PMg}wh*o|7vYjtq3*`p+f+cP@0?DYw3{*T!omL8Wy$pq za@q&C$1NbTmZy@joe*9Q2?|95d$B8d3c;zCgxe;ZFPfM|dq9HR3^PTPEVX}e7Eib; zW8m-NIzEbIAu#NKu^U}A(wth?Z&on9|OUNT6`Qv@o4r&tee&llw%&@rK%9(cc& zDm=sx%>xrlMZQL`je-8iu1&?ks6tb*AUof8HM2teD?a6SJXcJUXNLfbr@gYK! z&S`}xX|vb!>12Z;d#8eymJl+5|20|6a@~^|SIcZ$Mho)R4Bq(0y0u^+A?_NLatT&g z`)BOnvRI+{QGuLx9TS5{hkyrBw=xOsu=^122~GJm37#SF?@ zUAw%7bcG_qI`sVoNlKOEQkCc1YqdP-FKy``z76bBa6RBw?~J9YH_8Rp3yl)W**QUg ztDT^$Gg)V*kFDR{6wpv~BS4&jzw+nFk#7T;znvqbsWyPHsS{MU(m~gZ~;r0DC)h1^7g}rLPDj&Qr@Fj|BSm0DKi>8tX-!<3HqY zTT}PK#f+1KZTe?*A8M>Vc(47+YH@{Y*p4lYjL?d|;(3zR`u=R2R7xm3OIN;`V{!t^ zq~!@l^i{!Wjn5CdKG$o{FEUjTSYLJ89a)9n{EX&1uu=6T0>hW>0 z`!2agf=^ll!VQ9I&ILp6P}aDfFhWsHVvVXC>p+P;Y?IIFE3(pJE^!qSsuxzb^Udf6 z3x%EPJ&>*TBh=~(&K4`6QqW8HUN+e{-Ja@;S&X60)N+9P@-sWGg1gkUg;#ki5|iw3 zK2{n;XRLt0Uv~zM+F{ec)wLPzL-B`xuOZdy`htswLJ^9PF^MhTWGy@IU*j9wFy0JFZj9(?LTL&L6SLGmTUB=Sz%ofEWW`;4yQ`9qmH&i*vqaJsIG7C#ty;}aD1VybI`w@ z7j=Klme7!YUn==-66Y6fRpTGHZy<~IJ|10*xqcq4Qf;#FWN5rmofILL2BsqW%zhSx z_IGngvj!z7X?{K`Lo>mJu%cThlsOb49TK`%pI${)w`Q6%mL1AfoLV zX0dWP4037MQhNz1Rf$;e5$iH}X^4%fJM;wNH1E{y8P%HSGHWB&T#$YsE@>%IRiU=l zRdn1Qg4w)J)1$yYz!CW_iqX{T^ThK0pHnB-a!Y!61Uv^khICvlf#L3uX3yY;&s~e4 z^eY#t=Pn()8_TznI}?#QixEG3K39Ag=V7wPOR8%Q>bw$ea5}+%(0ES-5?UsV$>f2$ z@69;;?;M?Xw_@y%?A-H4D8W=+(;t4i_%Nyyj1A|g-d zyAS)WGaQ)wn(>S?lc}7Ap_ahmRyB>xKv9C}a?b~yk2^8eW`W?yKaQ|K6YXWiQT7z=x@9TG+DDI z_EG`m>Il$ib()BSeLfV(0P zP$@>7s3j5$Jy}RL%i6_5DDqn#8|A7ftEal?C?gn@uoQtpO38Tl&n{*4aPX{whfERBIA(Gouom~EepH=@q6iyld&7$M$H z+R@+Ql}R{|aqoYn9ui5NjS+M97lk(r(0`{<5Rd#@O;%r~K=74ozrvWFL#rjpRSe34 z=5c|DPuV`JOmw$QPbW{*>2i?KEAB>Vrg{O1xH0SGnO0rwi;r9K*cjBAri#?f63WbZ zU)ITjA(M5QF2A}JtDT28cN-Ywd`XVOp-N(9%vjU-xKv~TUWxl$=0~g)J$M^rV*!ug zdC7#z&QF<>Ic$hh@#x3WUjP~o$0q5!?7(s3nKrcZK1Fs>8XretB7%mpSXd10cV$2rq}CA_j{UZZ zShQu2fit0+wccuaFLPyozs@H(*DZ)V6L$4o8HR`tTcZa9x67o(huiF{A%sNfWr|KhIlFqrm#5w zy1}4Eyd+3Ge%wDy(Rj0DXerYY$$9kb5}qu1M9<)~$; zOt|7w!hd?uDrYA2e&k!+CnONz;B!0A1=>V#3enIKB_vv!{UqMko_}_1!RmOj#-Ovo zOEEH)+}^5;$tvd7s-9Wek}>^on2+3iKKO`J z)6CuzgzD(8aOEE%Ts{6#$zWkczW)XnC$4!EwQ@*)EH9@ik<~r?+si_vET*v_K<0`@ z;9CMCEl!+^EdTL>GUgrfrR;in+)R5wNV#CFh|HmpJaIW{wfh=r=m1`Ul^@P~tAJRM zrAarFcq0ETqJX`vD_PI5^t&#uvg%;H#i;$VgW8&|gX@^$G-U-9KJ}plQBj$Grxd+w zku)FC56uP3d_ZBCGp(lr|6_YR?iDH)xpb}lVFCt@3*0581472@QvVL@>UgXd`+~! zQy&_rB4Hux1SkJJpwm308oC*y@n}nxUfk(IjfA9=J~8HVZjuIl>S7pwFju8Yq{s-m zG*CSW@RyeT7OFEFF~fb+rcODfNHZ6k^(K3lhD3#~q3CLO%ws<~N-AnXk^eko|9Vk9 zs=jY-Asze5zy_>6tMDXMS_m6&)ezwZwAwoZDhspOPX>aMnI_nzMI{BlN^E6LKZC+9 zeV1}1YU0?QH-1j6vG;WFM}lrWE@}-FFy#-`(*=D<8QRhTyI$^lDe`;c! zBcscD&o~sj3xz(%&TG(8wl)`|w1mzkMQr?F@|O`f1Rv^2`0rs7T%65y`J8Fz5W?R$ zU1;%xsg{2TDe%M9$cApzmYLAOtwrqnvpznbAJ&kyP=OLL|1><4?;CWw=W&-KI-k+Z zlJ2!UHXrDum53*hO2UpjPFyyxw+0mj!NVL{9W&K zxOzKewjF&N;E%9)hHjV$2Z) zM|RBY3Z&(c-5j`M&{J@y>@xCC&5=M6X!9&M@@G&&$!`!?S011gVLzl%XD91F;aW$} zgq%LS(474~pa6X3pV+lK0|qk=!p0W|@g%|7;<-@7zyVCL;J9evW;c9jF~H*TP@@s< zY9KpeM+!xm9Jko|qPoJ+ij%M+$j0Dpukj9qvMbSy8_nnpRI+P9`^S2|cPcHg$^`{n zLa>yY(`kW}XvPo^ndRfw&X)EPoJHB4Am$U7YM|);!gOYq4i0e9!yC#Jm(J2SJ?6pOp25(a)y?icTC$ zn5O>!;8Z8NpJ0?ep==F#z!f}OZZ@zDNy&K?wSpEvFj1e(?o&A-@EWx(O@Z^JvDJLg zF}4WFCA;gfx}!825GHhu$E@)0y6Kj5B##3P)9xVRAT3e}hF8o5{1g(OPnpiwJ07Vj z9g~+1hdfjLziHd>snQ00FgNa~zqcdmL|1XAzQ?hmP`TR< zoD8llUys28gD<>?oewmN1nrUOVd?(xKsKP8A4)bJBlltE>o2bIrrN;*nm-~QX54BQ zBRebY{c?Y0c15l)R`-ZO4}1dAA60Vrf+7!LdnG5A)a9OIikQRT0N{rg)02lt3@)%6P5(qE$DSNI!Mwu=<_)$CE?{A%Sv3->A>5g(bqC z0wA@Jo1lp;60rD+B`_~V(7rNq63K{Mh1%R~G%li&5lH&;hm_{8x@@veHk_H=S2#3= z>3rqxKKO>)avt;@Y(XLj#W>zgU$=i@v30UpiZ)biH`2I^x3^D0a0>|{+X`jxhy`sB z9c~Z{iRE?0tzRzrV8rBVc#`{b1iJHjT}&?PAFsfh8`RX0ndrl_)5BH_yyjw8tgric zU_junjh^h6>I%yt`x*#+0RswgD$0Mrz+c1u}gy zEBPi*^dw!+fG*oe!PR+_6#GrWhbA@rJ5-HB7liW0FbaDx4R*XxOo|ip&lS@?6+($tDPJQHWF{!}WeQvhf9Jp& zA`zBaHdf@jmAXd%$q>$lGbGkUkIk)w-Y+jan?##6#Vzx1yKJ#^F+}LbhS|t2$u$$Sp@TrF5(7D3jmj!%kcd1vq zy1s77+abSS7aFu+v()mS`kFvdB4$Qz)D?k}BVPCTI%{79vfs@xzdt^39iJEbI1=P^ zvEu57`-QD}tqo`9zoNCKs>EUYYez@Q6S?Inlb-s(itNW;?_3i%W;{dl@_>1^_zg@` z<@|_M8pL`-H;+x_e<`q!jr+G6&415F@1;0+mh{rUxx1q%L930{U$ZH4rhT=ukUhL$ zQ54ydKw1vrPlYgwpyG6~4B$TJ`f{G%aZ@t^v-s0Ob!+(WU`ZLCIM);bl9#`et~1sY z!R1u1WVg@a!7W5xBep^&!Q(}<^Y&cGr}0Q2muBR>K$fM=AKT@G6ZP+B3j&H6rG5Q^ z5*E41Q+?Yn)(d(3o|>m91^u9B*|qCoHi+xnOGF_u$%(SqUp)Ct?JgIAkq81OSa7v2l&?9v!=)PKPZg2*(cz!W`Q zhIIB92-fTCK##PcdTaIl3hg1KgzAKWK=~~iMU!D5ra3ra?Nj%y{Y{fwoDi@eLTg=JJ<0JBt=s=F=uOyZXAV$fjVVE%S3`k-nI}vue0pW zYj|CpLbJhQ=Q^(t3YlAhCpko{6OH#nZ%u1AJ_#5nPbkI)j$|>fKNvQ3uaUkPtb}~Y zi4Cg>-jQkJxD1W&c}6AZ!iZ@5peuej4QfN!8cCiOWw!H2^vl@5wC8-eqedn4LiKLG z8Ym*m+5M|e32w(QS4uJ+Ir2iJDa5f0;q$2{yDVUqkwA}kq?i+Q6@X@WYzuJ}=F9T9 zl8UMf#_7{V_BImtb|R#$Dh^5E|zJ|o`BMH{Lh&beL_JaF;>PAN=SuH+zaE!jZDIgI3>be^~qT;IQ{%>}ti%KQ6C+zQO-1FAU#q&*7 zq@@*6y!qY8&OfNL1Y&#_CL?ZSfCNLxdN-noABeA+6t_$h_i(qi;O0M7*E5S=obC)Y z&7-=xK=9qpRo)r96xXWbf{zkB%4CzyazygJG6$jNePe=waVL=IeOA1AB?7)FMh_FV z48qYia{BU$!*|L8-H}I}nJWIy%qGR7dQ$S@ z63Zi}KwYFw-lf3{c?1?8t%G)?zS_Wn>qCLR9AbnII=Qk-gqCaFbkL18OcT`vc)G1^ zOX5R@RFY0CpbjC#lP{1qk+3vyJv#ub3w_b9HuR?zGYy6VOk?q3yPQi!{Y+pw-#I)E zXQctD(4qs@STU{_n(u3>yR8c-V#c zZ>!Wc&C9N#F2~@~(p*wv@0wSE9zmhdj9|Kk?jC{Zk-fJ*o+aN)Hv1{hoHM;6-WQjt zw@1>R5x0YlmY1C$@%}w?KO7_)oLAT-o=RnBa<8~OEZGF#S#wO6#<_q7G-vW!b!?!R z`DClP`^F64#?|z+re}_;7ov(X>wrjMyWT=D%*{TaZ1ppCIJ3QL)9leb3D%K|g6YE!L)!7;o|Umn`8DehhO`?ZUyrd$(R=hm4y@ z)mNP_^aL9;_EHIh3*^pk9bMx)s(3i(dj>vnt6ix{ZW}AUB(a&5YR%I)vGG-5U4|c{ zb+48exz5!?r4G%g9Y1XUI2|G%<<8n}^~BdHa)`i?XX*4U9^+2%q=>mGKp$yzOEYBP zH&?HY{tM9^5e>bM7`pc}p*0XB+-cEF-!-xhQr$MvuzibZA2a{%^T2^e7IpRVwn0w) z5npHjfSZ)#5Up##y(O1Ex_>gx?eWIVr!jp{an-XsB3J`PIe+gmuzJh*DK3L0`Q3MX zDxHbhP0#Qf(RSe@F`^g$5>qA#ilgNneU6-A|B?f8k?JdB=j}gyET1QMq)CVRe@{%m z>UdV4_x8=rcMi8wEndF`YEn3xr1kXWrcpKAUSq;v`gOO_v9(afyxZdvJw1r$cqC?Z z3`9(S93dB5Eiok}ry%1ZfiZPXD{lEs%KEm?)Qb`ce3&yjJR%pXTpDlsUy5@|Xez0$ zn^6NBi41I&AI>Z}@kCM#dj*=2<)VJ32Xhn?@0Q$tmlPb0QDDisX~dW7BO6(-yMo82 z8Z~WAwl-RAX2>}Lx=XD%AUCA;yPv4NFRZrT_Ppka&m`D;{J7(4 zj;fF5%h+snd6KRKU*ejut4%N z8tT}U#x>m&bAC@OWN5c?oGOe+T>dIFG@iAnVLLvk;m$T_uKaDvk^O2!2=q@`Jl=Ji zQ-M>9V^Hg?gG^eUrM_&ZF9sK&<=f#%{Hi=#|}UCT?ds=lt$*w#Go7 zT+!E66mf0-_s^rXtPw%{Ph2j9(?&~CBM8Mtvxc3L-(p+*O#I0q?|3W8CqmQ47fM%s zk2}W*4XsZ6P$R1(F38t|32A+C+DTvkou=V;mAP#hkT8)&+HgHQHy(>v@>AV@$$$2} ztw6*ROp|?rXVg*kSw-BvW7;aVqOh6U<*K#To!GdNc=IowHL^BVZq6~X1G3LZe5y)~ ztuLXhZ$*Lgj+~MO4Y|UO-ng&YqPH{({6bg_xG9($yy;I2kFob)qiN}hbrF7~zC9%J;|Q2xmL=bn)J zEI2&49t@V`zf(d(R_5o-XDEj=8BIo2_m~+tEZ~*@6NI1`^nSKix;o%kmrN%6t9J6Y zwsY~^pCdF1FwCevAroml=A2w3+BBjclluXHHM*-}%Hzwgia;?@(K zd<1;4#x1&p`D3$7FM3#CniN+sv-FDJAR21MBd)sh3rsn&O--f!dmpLeRdjx!|7&Xl zD&4JNAWd{n*KJOAM*;$UZyswU+Uk;cGRS28?~$v%E-o&bhshB7-5(3aXkb!ef6*&n z8l0Rxoa*bH$ETBB`$@%A<9HF6y(er5OkOaJj7rfmy7R1m{rwZZeEGFgBw>vF4$b?Op5WpN2lNot`y} z$(vrhY&Nu-HDSRIS;Z(PXc@om_dLTspLsTXt`?wcI%PQ0S@%=iJ+1~6|JmKp`lZic zGi8WORb`DO3o_xREE?G#iMcu`(mTK`@Y!c-4?RC&ZLj!ZTq$Ptt^dA4-%lP(Y+~~$ zTY}}>WPS-C$q1J1IcoZfhM4`vljHk}u+_N{U;f?6_T>S>0PlumB!$9vQrYTO1U*|S9Nwhl)0F*k^)#Z47r zXN-N`MC{=2ZrkvkZnhq_HwSL72N42n(SNe3ubre79RHb=q4SG~rB&;C7s??Ztfn+F z3F^{R$Cn{+wmT;%+dl^1Pw*Gqa$-8fQme1+$8dbCl088dl#{c)F24j16M?ty`v1Cn z%c!=V@N2Y%0>#@xfug0jyIX+*#ogWA-6>MsHMm0v?oP4b!QBHCclS5_{oniXu65V_ z^xm5<$;#xM%sg|>oSE6r-usoD<9CobBtE@8QqflAJ#q_Mu$#wksHg~|_pQ1EqemaD z+$X5MR8WI7Lw5EZn?%*FLK@%?K$PIlt;bm8rZ=@kVpP>Kk*Sf=`Pm$5$Qm9SG7IEc;w)24K=`$+RuqUPW7HVMalRve& z#vSlb31hN2UCkmLu=dYEe$i&1xZ7i@rdJRN>Zv-;yxDg8>$Auuz%RwS{@{`7U$RP; z3=^jpSxJVjM>Mht7tyjXtRAwOT#)>rG)Vvu4gi7l$Zv{zqfZJhh&j>(A9GQK9#@6BaB@&P8^Yf-*Vh zIJ3tLKczdslZjSuhrVw!kyPI(p(#Jk_bjatUTOy=v&s8+Ca-)O4DT8RBhZC=VpE-c zk{XqK;dhOp?ESq_peFo~YB`avz4cc&r{uS9Ut3kvz)DRR4pZe4weR!2%Z z%f5hDQ&qHDPT#>8Rg`qu^wOuLQ)GH<1kv~xCK9^W&pz-LWe5|A9&CV~9u!dV=>)-` zyd=ZPr{P+nEW4(;iG1aj3Al-UmpzxALhGUNIgG~~qpl?BFTgcE7qJcUzJyIrjNl_BJtK0mPb^i~jFVJK_Z%#2&Ts(k7g z^7>WR`2iCYh!KVo`&I5(!;)NxDOq`w>aDYn{=Vov#_tzj7hWtEO0y5H32Zw#*K}rs z8%jsMGmJuTpRiv^Cd_qA_vMA^c9vwVt_TN$)!UM05WfsX7`>FNJ9S7QA@Y9-yB3fS z@XrGGC20v0m0G=}aw8(2Z4Q7FyAQ$6EizPiPO#&v4R6a9x5Jn@W46jEo>XjluDd$@ z8!NWyTKUx9HjXIzZMSOa!xZ~_u7VO_5-Nh6H|d@&dDJKawi zsDbFF2O8VF_gGgN4deb;$bNr%P4(Dp*Ow+E%9IW6NiNMu9 zJNP(|r<94uL9&=ih($gAlh!*0A7n@SRJj~PHsx}nr`mAk1tF zgy%qHHZ}MPbKap1VuBa)g70X zuiU2Ga=A78ybnb~X{i5G79^XqVurxFR8r72m_HQkVzc%5x@}@FH84qnrL~F;Io^mB z{Ve3@FFkj!TF#RYoop{rEI8leanY3^Mq$39Q)ZA*6U|gYM6SCNL4AEi>$mE)=yA0U zeI2mh6_TMDHXX=xzc;vHq;>U)y@#1uhPO#Y{p9I2ukszfRx9wRk+;@~VwAdMvW2#( zDi#fVa6e%7#u!M!WWW?Lmg+E*pWrNIZ9&?tTex$#ok(=rM4Q(U#aq3c6P`xIFsiTp`e>@3Im=_*P4vX!Hj!eS1n%cBHKQ440!Sxr8dLh4OlPN_jJuU|zC?anK(#kw6piXd8q{@fB~E&#eR@1*yUl#aJQ_@w3Z zW$H?b%#)m?cR}uH>GFo!06-54p`#AV!w<$E8-{ zDu!k)Ge37~Jd_9!jKtkPVskhhfn0@F+<45hMtH%HdWqgN&&I`&=~;&{;kxj%^%0j# z8If*Da5bovLm=$g344SQcNNf-ALn<^k0h5>G+U_nE`_&J|HbqThlq&fWC2sxu_P@q zkjY?rhL~HcWy)_#YU&b|nx3|>`fKQAr1+%IHUfC;A+XraG|?G4ce=mSK#|Ab^W|Pb zENhMy@1oAWN*V#=pCbblJhPs|bfx6=A#+YWq5zS%&^(|uR%uD7M8U6@-=#bh$rnca zS~*If(u};AAc$D5Od>+ti)3BA9CAimxh5CTUI7UAA)#B~eG)m_7$R&@dI$sc=E(_h z65U4|EPsPk-UUxk4b+}X;Uk2EP}uUK<6Vhi|21{=+m*lavgH%)B9X{l&Pq%$8l>zn zIcL;qiFpxR{zVxw1_hijAfp4v;ugh^7twggi&n__VxhhU%t?zkumzgpiR_&0iNqO! zb+e^n7A~bf_^u>~S<&-3PFVkkUC)=!z4egpE|LM`KkD>p0`cM}b1=tb^{&aZfVi)4 z;^SmZ)jAO!{tp_Rhnvy*(|S*xwY*S4Ta|P0pW#vIFb7SYM4?d9Uz)W-WKz5QHtU2bs4ae1vHjY7G}Mn>0d<2K zdpoxsZ16+81Q0v2^V{F0v|r*}*0!E6Ul}&1TJ7Xv7u8ws7`f@5Zx*>#mMuSVMx#f2 zyLO`w@?J2I?Mf)S~(6IMWBbdt35U-)m5H^ikU|L z?nNw)E~pJg@ob&b_=b(I4b@dPHqTl1ViJ%I(oh$cmd33FEBo*taP(xSb+_XlDjrO{ z2+3O;i5}*ivbY{iYf!ulC(8Ru{m=qBzsrS%ZK~j|%$aUm^b>4OxZ zIb=6T$!CIyV@NzEE5c+q|vi&*n<#(2VqZkw3-#$a7PpUQOU6M42;Zqy3#vmTIcavL1Po)zPVqcd~Z1Qf!}t7 z1RVuSp4oK;)ra@>D{ilQtxrFU>LJg0f}kr#^v!al^4jRotc zg(l07k(>h)TbDUs=G(W`EZpEiV+l!pM&Jaq8Uw7q{F$}x(^^V&KPH6PEq4eI@--*t zP{nR_2*rGO+P9BFZ&xnx;IToB?44q{wj0jXJ1$jfoqPYM<)i$yWr(HOHyW1Ed)osO zKaJ+BX>*7Zbda=Xj?wV1ecdAdZg-OWT0YR*aRVY5^>;8H{z(6YM*qn#UFPf7iWqB_ zuA&ngbDeX+lrCi0vSzbfw=QI4Uya@Cf+yP!3RmdtWsUcEV`K97)R_kmhiZ~grBPr) zg@x-KvUj<=&}E81BAeb<%XU9wzYu;~{0i>tl#L=jp@j+S;XU4x7>xzm-W z+Up_hO)!bs{i(A?=Pr3Ti{g=1TL8kN(wD167jcBPl*9VAuXN$n9GBcaUoy3?cV?%m zSfVnN6|Az|ARMJz*0S5_`uD#2 z_>`?SpBVMGw=3^u(pw`bnf%AManlWaAKAQ=7zvG!msWKc94MM!QC)zlN|A&u@$$HK zn?Dxt&7N9=eHG`!1bowWF8#NtD*!E5Z0S5Ji>ID7p&FdG=ABPoI3>ndKbu_sXywE< zSgRgIWuTe&RXgw6anj#sMVEY13>YCuaqb>PSJ#FCw?r)1+j)}~!MD*Egp$(5@ zVUaOL%S-y2&OZ$M=JhA%*RxoKxI_^LnnD~eo@msbS%U5lZ$C6T3rOSG_m^G%FpIT} z^hCh4d0H;bVjIsEh=kn^!te!64*TV2*0YHvaD>gx<_vUd$A7)#7y(>c%|7RF5^j~- z%fM-#1oa$bXEl~0{|+Uh4uYSNu|~IS44*hM>r`+E`As_M05jnzK)pHeDN z$asXJN04vsJT(=EbmZ?gnQ?sYX|x$l6SImoYVem(ngc$MpL?_`xd`_ zP36mvLDLPQrj?nUQfhjGb3@CC>a>gg@lA`Y-}b< zbvD;!VFXQ>J{xRYo@{fshxsmB>TK(iQc;CIJ^2tybx63~U+irE{mbfcwxLAvS&5Ov z{P|!^ht55=?o*b&AAqvx;qPFy#*jN`+|bG}G*lm6Bt2x~SmRui;q(H>Ps^u#v_gZU z5`Ag1L{#~RLFz`rjs6g)f|PX{kj->6&@^htKz=i!HW!>lKD`$c8V*1b(8rIcI1xdaRxKaizmlH0?lxmbzCbYx&)34+Sj3Log!C+}zDg|KwzW1drbn zcW)?WmvZ$FMYm4_y__|-qZ}6ze}-pM)sB}Zs?)KWV(*7`HL9IZYq(C`*89^0jwNF9 zE%)1)hC=UMZ}e=^>r1o={EujxFU_b2p;L1#XXtQZWKJEA{TO z89|wj>e%Fro}UVy{LA$TNO`M|SbMIi;`c3%HjmB>680Jw+hL?L4M<_etGJik68kUHLyMYrH}gwo`d zQR{e9sP|nu@KAkVskEvf%J}lma-@NEi~dM=Z{k)AAJ5&3$Uo^6_k^5k{f-laoP90g zkuQx}&tqxPpmlU~^3)4S=ZNHWJ(#F99bwI(E#95zIc-(}60NqfgF6FzyBWT0qTX3L z-}9m2Bp*&~g;(BjuP=S@Qeq-DUH!{kX^PEM1uSAX5sbE6t^J}(_r+c@Km;0VZ#oM} zEKE_EN?+zy>hNP`2*QVI3A>x~oKtBuI3cM|U$7Vi8gmF~HKO&8kDNfxq5c6&JkI-5 zMX-B~#o$i>tEu$#ZlCkT25C%o%Xcd|QbhK&l+xf-HPmj*s@nCEGa*V3&5-Df-dJRGq1}*Sb5i^nYv!PbfOPTc zVSsp;Khfw!3dDF^6TUJEzBCmwwo(kq?hRD{Z4>5?H-ocnGk88`{j`fmPA=ojx4u;< zORW?xGkAQ4*Is$0h^}3jQr=Qn24FhDk zBo(ls=P*2~OZqsIM)>>%RxI%u`!#FUQRF>o@CPUfm9XN!4k*I;$*w2)yf@HoZvTTi> zQ$HyBkPEXgFhs*>95yy8>&rJeYF-&S?H-C4YOjVVDJgp{{Z%JYGBQ6vvqpxFsq+iS zq{L}zp#I^q?bYc&_m{gsofanmD|8)n#d8U(9?UX33BY(#d3E!sjv)45Th332ya$o^~T6KaJ{%LR-mC+MIsb9ntjT-@p5%Cfe$Hi`=M-_c?IZ(;L4 ze@be-P9MPNsLYn+f8l`Y_w@AzBckKu#rl24B>bOX_4xbi^HyP#GvkgiV6m;3KbcYb9owidU6hqD6mi3))4MhLf%b3T%g7WjJw|94qZEYEv zGDGfCtLy9gU_E?vWbGbbeEW#;0onF)TCfX1Gyb$ur!#J@cJycO;ryF%xk*7%fkz9+FwIkHA1gN}rx zw`#_JEWEOjVTR#>C^Qs!hPD+RyRtdX2i@2no)!uwkpBd0-%BE~u>yS_M>o*R7iNc{ zW%k}`J5to|olxwl=&kvh*=sUA8aU^3Y{0L>ffUx}Tec@0VtfAoZhUCt015WkvBl*S zSGh(_{%Ez&{x`GNGZy$j+pwjk%tim-@tU~-;RvqF7-l`eq<4H;1mm*&!B0noPE(gr z60f_emOR18Sm0@>u{VaOA4svp28Np zZDVgC-ZNEdiF%EDl)x9x0qr3!?Ma2xM<_i&>x7$^7pm7pri>Gk2)uaM4+QajS?72! zk1IjA>a)lG;BKUp2;j*ljv-oyiad2Z=F*Ku7@sWrmZ}%rJ!aC)=Z_4^bGz_hHK?!j z9pA-wG8}H59EBAWAjx{QLvAj4!2Pr))IW_^=b1k4?S>~7kTE7d<2|gwF_wVJzvhn1 zn)l<1S}N4~FwWwiU$_%A>(yGBcUp)WRMimv{6!~pJ1{vJ4GAj7^7h6+gpzSGSGs>t z)yZ;jaVk~=?cR|ceL|q6E8RooK6`u@XzjhRw-=`0q`v6c;6gV2sGLODeKjesusHeA zT%P2iy(_F4?b`L-*Iimr7onJgs{|ntkz<8cta)+y*N##ZKp}^Vi%X&1viX8b%gmj& z64!ZQ0h#O4jp;N6_mdbEVbM$oE^s(<`;LyLSVVH7kQoDp`>UP)YajtOmg{Gj?)G>$X*CGFhWnys!$^$ zNMoPi{;Z(YEfaasQAhM-rJ|=iWjUveKO(T0xpB)0z72kdq?q--)7STFmF)8Or3w?} zG_kvjA$IFadgpwUb94*2pWo4)-2A^^{>ih67*a>~1lJa*v#^&6p3;Z+6q#X(h=^D$ z<fuk?&wVI%+1n$XelGsrr1vqdmUH~U_z0WrLM|VqW7l(C z%s_EQUrz>|5Cvz0Zs&FK%oNxHpRMED_p!2xZ{gLWPUGlMhRnHG%f=Ji?^Zcwtk{b@ ze>%fi@i7Tqg`u}?!Ex)rRW37(S&LYJg+RRyi9yx3b$YPPV=W>- z>7D_v))>Ik$wid&80frY#coY3qR^X8ew-z|8`t^d!Chn0>7@j*H`V!RiYT)h0M!cJ zfJKR`3=G)8^2{G+R?GZJ4Y2+wIe;_WW{bI|>HWE+bqpC9IZsoUDdEOPG6&;0t&vBI zBkkX_GB)`lKuK9SxoOQyda=X1DIq=m;Z3jLj;TEt0v**3!2YIGhKU$s%v%o#7qN_W zN?pQ8K@H?$q+^rdQvLe<{>4JYu_a7)*!j5q^LHRNyTZNcu^|N~ijij0(lRS3Rr$@Q0$`pcW{bm!jk19qMp&@f;3Nr;L^T;NlG1GEUr5T^i~f{Xu0>aL zcql|`XkJMpNdp`nYrY)50FovBRvk^I%{!%`N>omsxpN+lt#VO4s!vWfFSb>K=*rWs z;mngIC+oYYmjRY&vLt*#nJs#L-mA80oVMw#hCT1L{5Ag#ZF$eqR@ms<=H%7p5c1z$ zb-{-?s{6$FQ3VwMM`5z!D-UB7`KI!a3Lv6PMd_;BdwE3Oc!Dq8Ek`yJyN-VcF)*@Z z3$DX%(-x|D7*n(+G`EO&<8EOv7nl-!shS9Gsau5Q;(+9d_>W{J-|*5&@bYtow?wL6 zY{gN&F#%^AJU8kVrn^CVtJd-gy7+K-8Pc?%%U&?=EuY`Cj%|R>^Y-)9Bx-W2d0NhF zxj(HthcaI-*To*wb-5=7qi39vE2{ixsf+PIn2|w@+cpr(&yh zy&~bF|CtRMUGOAki+qSmJ40dcA!MEm0Eo(Zbup(yWIHDD{6iypKm;1?6o~6?9iCe) zl`0K$d6VgzYF+SKLSFH$;If~73BVt9v2b{vKC z5P?tNC$Zt&-uCY6P({KI$i>CEm>)YA#8#jCeB&Kqys|mg`paiW$2}CqAi3IxbaGM+$BcQTgByao$o#`o%<61E61f819TOa7wS@#)AetP!{z-X z%t8ey+S0;p8fq2b-cszq5Li!x06}8xHn;TSx3-o*m@ovNE%Y)^Px4r=)6)ya6-9U_3u;Xl7cIt0wdmgDlz9>^x&}YhDoYnSLGLJ7r>eZKSytoib4D z&nWh5s9_R_MA;_P>fhJldLlsDP^A1~Sp!@f(k^!atk)q!0eI?hni4aTs? z#XJf(lsfc^x_4wqO}JorgO8<-9~`XEW%il=g|LR*2e4)}c%-_RpU`tfC4I zZ0Vsl+hZiDwMX;eGi^CZztG*(z+1ypxStcSNl;JhRJfS*#f*)NL_Svyg9}vCCOj|f zqpX+l+-2dUDKU%lnh5lw`C#?{b&F2SfrXjhzRP?4MNd%rCA&53PJgoT{rE<7(dBKJ zv`7Q$`H>8CYI+bJL*7E;10Dwlyfq>n z)`l|2+04V2jC#KDt7`HC)GuK|iQgUqMP3Dtj?uV7(I!Dmb5kQr&Ujqi>sG<)p}9INWrI z@1zWM=dtQiy-3+GlJ`*rK$59KV+;LH)l}-_&0oAl-WR zxxU|0NvZPi2olUgubF#m0n`E^ynSm&s9*DI1aYw0FMO+HJQsa$K>45rKVW6WWlQl^ z<#B(^Z|3AFIo8KI7PJ$gG|XKaN|r~(BgKD@ zm(5lCA6_oBdlHjVE8CSEdYIDYMuByqRD~<#oz-`0j+PJK;KPxNa)>+mvO-YDS}~#5 z?v!lThnAz-&6$9ls2+9qaMg9+{<(JyDJeG#OW(82sj;Z!$~`=QSr5mW&d!Qf*el&j&UVuMzL7WWX^mG?`ee{f-g^JXuKO74&>pjlXCF~KHaY1 zuX#Ds%L@FN8b-mwBKkcTx#E>_8}kk20+nS&BGARb=~ufEO+j|B4#*>L zErFN8>tZSRVnm%ks=!{`-$u%ARZF!}+#!f^L0_-;q7JcAm%9)6b8n`JL(bG(|96(M z>332lq3lumEue$l(7Ol0S2+FS=9q$Hj=uq}gO{|h30vAk2RAj`o(<^3$`-Ci2#rYd&!{{yYznSOq7G^{&05Y0c=6YzX?2vRU!5U4KnByCO zg*2;;lJXHodg#!e%7r)|Ng_pu^W0SGvA?&FTqhOXUn^rV10XH>&b@PYw=Ywhjc+nU zLOKD(;*n7@&Rm0{DmV!BI{@Bu)|NVFpu_NJhD-&ToNR}XUTvJZ6Bm&Xj6NwGYO~1~{MYYG z1t0t`=|AE|KXyj#Zj?RS=(ojyLbC53)cU(IVwm*h(-kF+c^pds4@T1beEuTeayeKE z60A86kT^?3yD&W~a&SE-Co|;}|Ly!ibFNm+NLg}M1kMG&JTQ&eW{n7PVquTYU82|)o6zbpKJ~3uf(Eh~_xv8+2enSYFCuL@S zFa4a;&gmu??`VmQkF9`xF;!W60g9iwEf zsQZa@sIu0FmVLFLniX%Ws^s6*vs*02EM-GXPG8fiUif9L>^`EPAON@)TKtfa#?vM& zHRRjhWC3X?|2jM?(YG{Nox_q{PJv5lyF1l4Ak0Mv#_XqW@VN?c$$gvmiD$Rx`F4AF z7)5V7^PWmYdJzPJBccEbNI@!GJ{W?VzihZPX>x7A?nV#+{JQbBY@8xABXm&m_E5t z;?xcM;!Qvzm%1xT509lL^O`)|=M?PC=y}8)TZB&6R_v1mI{%uV4^kw#t zU8c3N1{NOWLj^_9?+bO1&0m288V?T}hkFL?KmKii85ccn1Yrs?6MJ%bdU_tMw&CjQ z>#wY=L{Y8x%Qn8&1{W+M9yA=v$AUk*NE($>$A$@vXnJep!M4LA*Kadjvt$Y8oG)sz z#^sJg8>V)y?|+(c&NqmfE#Z9> zf$GQjcbug`CkM!lr%PkqTk(DUdfN+m&o;VkVPBlpuqy6Y?y`pX2{xkSjH9c%aDnD- z)oxpEayrf6*~^lJHd_uh^RX?RS`0D$@6VlBG?pnPQlPq?x(Fgg?>QF#e!EI4DrZI? zOak2TZ4d=6&?G~$Q!X6#^lP=jDaU#WRFk`TF>>m z6ss5XW=|(BYp!Eb+P}Q&`Eu3f%lKw5t#M)e`r_vFlE#*i;^0sROs?68tVWV6oYyhz*}Jj!YG7RR zlc`)2uAZqwsJKSPn!k6Ha*14ge7tZd22m8%zkwTbN9Y5#?Vp(xd{+XjJ!=VQ_w`eY z8>(UGx1rmUAu5v22+wUeIfvNX_X{2z`I?DddkN!po~yi#M+^O(K@v3OD}j?(>pRyk z3=YimZ?(#GtUt_@^IYq2WG=M)awTZ0^*$AZP^M2Fw9I?BKUplzJNN$pDWXSMj5>gb zeqQE&wYgxbbv@QNYjJUZz~XGY>$A2Ba}YlYK=C_vNFX2710sQo=ddRIJz01r?E=oK;%2^*y6xNJjn#l|rfACC2v0yK z@d%RGgqHWmiE`xGayIU2!!LB4C)*Q13qrWZiG^YG zm45GE+qMPgjSpWM(Z5_Ry#sgMpt;fkg23+v$~xb1ocGcn<7LcMnUBOU-w^YQrYIBW z<*S-ie&4O`2pgf4qqreafxujY!V+Jg{ErUY(7&`COffjM95d7>mkNHyK>fI5M~8H zV7pYupPEpey;F~7icvKk%+1dX~ z#f%aUYLt1qrA}GKbZJ|(tE$xKQJ?>^`kA_IrT;Od?PM2enK9RZnYw9Lw=&b{Pp(RP zht%l+Y)@=Z!eY`Y_vK8T{;39WYOrD^Q zK4U{t;H}!lGoAO zE${dSdq#a^j}IV2^esC8fc!vHsQ7)&D^$ zCWyJI(=#%Z+uUqn+OOcv-cSm9lygjJSy|IvNESH-#bm3i_14gT;F0$RM$=onwf3Q6 z*^FkF{ZBCX$rOfSi}0)QW?>Kz5WvB~?X9%botyld(dP-MD7^bAFTXogBu!35CG9On z`XX#*W(ETzH;#{^0RR_7TG-DfJ@@`M;QHe4y}|iEz$&y$`2Pp+-~WXY|DORy{(q%5 a*Z#QqX{3hIJU`yRUXo&RKPp8G0{<78u_v4W literal 0 HcmV?d00001 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 84a912a..86ec110 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,8 +1,19 @@ +idf_build_get_property(target IDF_TARGET) + +set(requires + arp_monitor + device_list + web_server + esp_netif + esp_event +) + +if("${target}" STREQUAL "linux") + list(APPEND requires protocol_examples_tapif_io) +endif() + idf_component_register( SRCS "main.c" INCLUDE_DIRS "." - REQUIRES - arp_monitor - device_list - web_server -) \ No newline at end of file + REQUIRES ${requires} +) diff --git a/main/main.c b/main/main.c index 617b5aa..83f3992 100644 --- a/main/main.c +++ b/main/main.c @@ -1,20 +1,30 @@ #include "arp_monitor.h" #include "device_list.h" #include "web_server.h" +#include "esp_err.h" +#include "esp_event.h" +#include "esp_netif.h" #include "esp_log.h" +#ifdef CONFIG_IDF_TARGET_LINUX +#include "protocol_examples_common.h" +#endif + +static const char *TAG = "main"; + void app_main(void) { device_list_init(); arp_monitor_init(); + +#ifdef CONFIG_IDF_TARGET_LINUX + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + ESP_ERROR_CHECK(example_connect()); + ESP_LOGI(TAG, "Linux network interface initialized"); +#else + ESP_LOGW(TAG, "Network initialization for the ESP32 target is not configured"); +#endif + web_server_init(); - - arp_event_t event = {0}; - IP4_ADDR(&event.ip, 192, 168, 1, 10); - uint8_t mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; - memcpy(event.mac, mac, ETH_HWADDR_LEN); - - arp_monitor_post_event(&event); - vTaskDelay(pdMS_TO_TICKS(100)); - ESP_LOGI("TEST", "Count = %u", device_list_count()); } diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..e69de29 diff --git a/sdkconfig.defaults.linux b/sdkconfig.defaults.linux new file mode 100644 index 0000000..82d7ad8 --- /dev/null +++ b/sdkconfig.defaults.linux @@ -0,0 +1,9 @@ +CONFIG_LWIP_ENABLE=y +CONFIG_ESP_NETIF_TCPIP_LWIP=y +CONFIG_EXAMPLE_CONNECT_LWIP_TAPIF=y +CONFIG_EXAMPLE_CONNECT_IPV4=y +# CONFIG_EXAMPLE_CONNECT_IPV6 is not set +# CONFIG_EXAMPLE_CONNECT_WAIT_FOR_IP is not set +CONFIG_EXAMPLE_CONNECT_TAPIF_IP_ADDR="192.168.5.100" +CONFIG_EXAMPLE_CONNECT_TAPIF_NETMASK="255.255.255.0" +CONFIG_EXAMPLE_CONNECT_TAPIF_GW="192.168.5.1"