Подключены ESP32|Linux, обновлен интерфейс
This commit is contained in:
Executable → Regular
+31
-73
@@ -1,117 +1,75 @@
|
||||
# macOS
|
||||
# OS metadata
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Directory metadata
|
||||
.directory
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
*:Zone.Identifier
|
||||
|
||||
# Temporary files
|
||||
# Temporary files and logs
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
*.bak
|
||||
*.tmp
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Build artifacts and directories
|
||||
# Build outputs
|
||||
**/build/
|
||||
build/
|
||||
/build-esp32/
|
||||
*.o
|
||||
*.a
|
||||
*.out
|
||||
*.exe # For any host-side utilities compiled on Windows
|
||||
|
||||
# ESP-IDF specific build outputs
|
||||
*.exe
|
||||
*.bin
|
||||
*.elf
|
||||
*.map
|
||||
flasher_args.json # Generated in build directory
|
||||
sdkconfig.old
|
||||
sdkconfig
|
||||
flasher_args.json
|
||||
|
||||
# ESP-IDF dependencies
|
||||
# For older versions or manual component management
|
||||
# Local ESP-IDF configurations
|
||||
/sdkconfig
|
||||
/sdkconfig.old
|
||||
/sdkconfig.esp32
|
||||
/sdkconfig.esp32.old
|
||||
|
||||
# Dependencies and locally installed tools
|
||||
/components/.idf/
|
||||
**/components/.idf/
|
||||
# For modern ESP-IDF component manager
|
||||
managed_components/
|
||||
# If ESP-IDF tools are installed/referenced locally to the project
|
||||
.espressif/
|
||||
|
||||
# Local lwIP library is kept on disk; the modified ARP source is in patches/lwip.
|
||||
/components/lwip/
|
||||
|
||||
# Personal instructions and IDE settings
|
||||
/.local-notes/
|
||||
.vscode/
|
||||
*.user
|
||||
*.workspace
|
||||
*.suo
|
||||
*.sln.docstates
|
||||
|
||||
# CMake generated files
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
CTestTestfile.cmake
|
||||
compile_commands.json
|
||||
|
||||
# Python environment files
|
||||
# Python caches and environments
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
dist/
|
||||
|
||||
# Virtual environment folders
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
|
||||
# Language Servers
|
||||
# Language servers
|
||||
.clangd/
|
||||
.ccls-cache/
|
||||
compile_commands.json
|
||||
|
||||
# Windows specific
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# User-specific configuration files
|
||||
*.user
|
||||
*.workspace # General workspace files, can be from various tools
|
||||
*.suo # Visual Studio Solution User Options
|
||||
*.sln.docstates # Visual Studio
|
||||
|
||||
# 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/
|
||||
!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
|
||||
|
||||
+3
-2
@@ -1,8 +1,9 @@
|
||||
# The following five lines of boilerplate have to be in your project's
|
||||
# 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)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/prepare_lwip.cmake")
|
||||
project(monitoring)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/tapif_patch.cmake")
|
||||
|
||||
@@ -7,24 +7,30 @@
|
||||
|
||||
Рабочая конфигурация проверена на **ESP-IDF v6.0.2** с использованием целевой
|
||||
платформы Linux и TAP-интерфейса без физической платы ESP32.
|
||||
Сборка прошивки для **ESP32** также выполнена успешно; запуск на физической
|
||||
плате пока не проверялся.
|
||||
|
||||
## Структура проекта
|
||||
|
||||
| Модуль | Назначение |
|
||||
| --- | --- |
|
||||
| `components/lwip` | Локальная копия lwIP; в `etharp_input()` установлен перехват ARP |
|
||||
| `patches/lwip/etharp.c` | Изменённый файл lwIP с перехватом ARP в `etharp_input()` |
|
||||
| `patches/lwip/prepare.py` | Подготавливает lwIP из ESP-IDF в каталоге сборки с рабочими исправлениями |
|
||||
| `cmake/prepare_lwip.cmake` | Подключает подготовленную библиотеку при сборке Linux и ESP32 |
|
||||
| `components/arp_monitor` | Принимает IP/MAC из lwIP через очередь FreeRTOS |
|
||||
| `components/device_list` | Хранит до 64 найденных устройств в RAM |
|
||||
| `components/web_server` | Отдаёт HTML-таблицу устройств и обрабатывает `/ping` |
|
||||
| `components/web_server` | GET `/` показывает таблицу; POST `/` запускает Ping с возвратом на главную |
|
||||
| `components/ping` | Запускает стандартный `esp_ping` для выбранных узлов |
|
||||
| `components/protocol_examples_tapif_io` | Подключает lwIP к Linux TAP без ESP32 |
|
||||
| `main/main.c` | Отвечает за инициализацию компонентов |
|
||||
| `patches/protocol_examples_tapif_io` | Исправленные `tapif.c` и `tapif.h` для TAP-компонента ESP-IDF v6.0.2 |
|
||||
| `cmake/tapif_patch.cmake` | Подставляет исправленные файлы при сборке Linux |
|
||||
| `main/main.c` | Инициализирует компоненты и вызывает `example_connect()` |
|
||||
| `main/idf_component.yml` | Подключает TAP-компонент для Linux или `protocol_examples_common` для платы из ESP-IDF |
|
||||
|
||||
Путь входящего пакета:
|
||||
|
||||
1. TAP передаёт Ethernet-кадр в lwIP.
|
||||
1. Сетевой интерфейс (TAP при тестировании на Linux) передаёт Ethernet-кадр в lwIP.
|
||||
2. `ethernet_input()` определяет тип кадра `ETHTYPE_ARP` и вызывает
|
||||
`etharp_input()` из `components/lwip/lwip/src/core/ipv4/etharp.c`.
|
||||
`etharp_input()` из `patches/lwip/etharp.c`.
|
||||
3. После разбора ARP-заголовка `etharp_input()` вызывает
|
||||
`arp_monitor_report()` с IP- и MAC-адресом отправителя.
|
||||
4. `arp_monitor_report()` копирует данные в неблокирующую очередь FreeRTOS.
|
||||
@@ -37,7 +43,10 @@
|
||||
|
||||
## Примечание
|
||||
|
||||
Текущая версия полностью проверена на целевой платформе Linux с использованием
|
||||
TAP-интерфейса без физической платы. Для запуска на ESP32 необходимо
|
||||
дополнительно реализовать инициализацию Wi-Fi и подключение устройства к
|
||||
локальной сети.
|
||||
Проект проверен в работе на Linux/TAP. Прошивка для ESP32 успешно собирается,
|
||||
но на самой плате пока не запускалась.
|
||||
|
||||
Изменённый `etharp.c` хранится в `patches/lwip`. Библиотека lwIP автоматически
|
||||
подготавливается из ESP-IDF v6.0.2 в каталоге сборки. Хранить её в `components`
|
||||
или вручную восстанавливать после клонирования не нужно.
|
||||
TAP-компонент берётся из ESP-IDF с исправлениями из `patches/protocol_examples_tapif_io`.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Prepare the same complete component layout that worked locally, inside build.
|
||||
# Run before project() so ESP-IDF can discover the generated component normally.
|
||||
if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../components/lwip/CMakeLists.txt")
|
||||
message(FATAL_ERROR "Remove components/lwip: it would override the generated build component.")
|
||||
endif()
|
||||
if(NOT PYTHON)
|
||||
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
||||
set(PYTHON "${Python3_EXECUTABLE}")
|
||||
endif()
|
||||
set(monitoring_lwip_generator "${CMAKE_CURRENT_LIST_DIR}/../patches/lwip/prepare.py")
|
||||
set(monitoring_arp_source "${CMAKE_CURRENT_LIST_DIR}/../patches/lwip/etharp.c")
|
||||
set(monitoring_lwip_dir "${CMAKE_BINARY_DIR}/monitoring_components/lwip")
|
||||
execute_process(
|
||||
COMMAND "${PYTHON}" "${monitoring_lwip_generator}"
|
||||
"${IDF_PATH}/components/lwip" "${monitoring_lwip_dir}" "${monitoring_arp_source}"
|
||||
RESULT_VARIABLE monitoring_lwip_result
|
||||
OUTPUT_VARIABLE monitoring_lwip_output
|
||||
ERROR_VARIABLE monitoring_lwip_error
|
||||
)
|
||||
if(NOT monitoring_lwip_result EQUAL 0)
|
||||
message(FATAL_ERROR "lwIP preparation failed: ${monitoring_lwip_error}")
|
||||
endif()
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||
"${monitoring_lwip_generator}"
|
||||
"${IDF_PATH}/components/lwip/CMakeLists.txt"
|
||||
"${IDF_PATH}/components/lwip/lwip/src/include/lwip/sockets.h"
|
||||
"${IDF_PATH}/components/lwip/port/linux/include/sys/fcntl.h")
|
||||
list(APPEND EXTRA_COMPONENT_DIRS "${monitoring_lwip_dir}")
|
||||
message(STATUS "lwIP: prepared in ${monitoring_lwip_dir}; ARP source is patches/lwip/etharp.c")
|
||||
@@ -0,0 +1,56 @@
|
||||
# Applied after project(): component targets already exist at this point.
|
||||
include_guard(GLOBAL)
|
||||
|
||||
function(monitoring_patch_tapif)
|
||||
idf_build_get_property(tap_target IDF_TARGET)
|
||||
if(NOT "${tap_target}" STREQUAL "linux")
|
||||
return()
|
||||
endif()
|
||||
|
||||
idf_component_get_property(tap_lib protocol_examples_tapif_io COMPONENT_LIB)
|
||||
idf_component_get_property(tap_dir protocol_examples_tapif_io COMPONENT_DIR)
|
||||
idf_build_get_property(tap_idf_path IDF_PATH)
|
||||
get_filename_component(tap_dir "${tap_dir}" REALPATH)
|
||||
get_filename_component(tap_sdk_dir
|
||||
"${tap_idf_path}/examples/common_components/protocol_examples_tapif_io" REALPATH)
|
||||
if(NOT "${tap_dir}" STREQUAL "${tap_sdk_dir}")
|
||||
message(FATAL_ERROR
|
||||
"TAP must come from ESP-IDF. Move components/protocol_examples_tapif_io outside the project, then reconfigure.")
|
||||
endif()
|
||||
|
||||
set(tap_patch_dir "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../patches/protocol_examples_tapif_io")
|
||||
set(tap_generated_dir "${CMAKE_BINARY_DIR}/monitoring_tapif")
|
||||
file(MAKE_DIRECTORY "${tap_generated_dir}/lwip")
|
||||
|
||||
# Quoted includes search the source's own directory before -I directories.
|
||||
# Relocate this unmodified SDK source so it finds our matching declaration.
|
||||
configure_file("${tap_dir}/linux_connect.c"
|
||||
"${tap_generated_dir}/linux_connect.c" COPYONLY)
|
||||
configure_file("${tap_patch_dir}/tapif.h"
|
||||
"${tap_generated_dir}/lwip/tapif.h" COPYONLY)
|
||||
|
||||
get_target_property(tap_sources "${tap_lib}" SOURCES)
|
||||
set(tap_new_sources "")
|
||||
set(tap_input_found FALSE)
|
||||
set(tap_connect_found FALSE)
|
||||
foreach(tap_source IN LISTS tap_sources)
|
||||
get_filename_component(tap_absolute "${tap_source}" ABSOLUTE BASE_DIR "${tap_dir}")
|
||||
if("${tap_absolute}" STREQUAL "${tap_dir}/lwip/tapif.c")
|
||||
list(APPEND tap_new_sources "${tap_patch_dir}/tapif.c")
|
||||
set(tap_input_found TRUE)
|
||||
elseif("${tap_absolute}" STREQUAL "${tap_dir}/linux_connect.c")
|
||||
list(APPEND tap_new_sources "${tap_generated_dir}/linux_connect.c")
|
||||
set(tap_connect_found TRUE)
|
||||
else()
|
||||
list(APPEND tap_new_sources "${tap_source}")
|
||||
endif()
|
||||
endforeach()
|
||||
if(NOT tap_input_found OR NOT tap_connect_found)
|
||||
message(FATAL_ERROR
|
||||
"Unexpected TAP source list. This compatibility patch was prepared for ESP-IDF v6.0.2.")
|
||||
endif()
|
||||
set_property(TARGET "${tap_lib}" PROPERTY SOURCES "${tap_new_sources}")
|
||||
message(STATUS "TAP: using ESP-IDF component with project-local tapif.c/tapif.h fix")
|
||||
endfunction()
|
||||
|
||||
monitoring_patch_tapif()
|
||||
@@ -1,9 +0,0 @@
|
||||
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()
|
||||
@@ -1,63 +0,0 @@
|
||||
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
|
||||
@@ -1,161 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
```
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#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 <sys/socket.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <lwip/sys.h>
|
||||
#include "errno.h"
|
||||
|
||||
#include <linux/if.h>
|
||||
#include <linux/if_tun.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/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
|
||||
@@ -3,12 +3,21 @@
|
||||
#include "ping.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define PING_FORM_MAX_LENGTH (DEVICE_LIST_MAX_SIZE * (4 + ETH_HWADDR_LEN * 2 + 1) - 1)
|
||||
|
||||
static const char *TAG = "web_server";
|
||||
|
||||
static esp_err_t main_page_handler(httpd_req_t *req);
|
||||
static esp_err_t ping_handler(httpd_req_t *req);
|
||||
static esp_err_t redirect_to_main(httpd_req_t *req);
|
||||
static void format_last_seen(TickType_t last_seen, char *text, size_t size);
|
||||
static esp_err_t send_page_header(httpd_req_t *req);
|
||||
static esp_err_t send_device_table(httpd_req_t *req);
|
||||
static esp_err_t send_device_row(httpd_req_t *req, const device_info_t *device);
|
||||
@@ -18,7 +27,7 @@ static httpd_handle_t start_webserver(void);
|
||||
static void register_http_handlers(httpd_handle_t server);
|
||||
|
||||
static const httpd_uri_t main_uri = {.uri = "/", .method = HTTP_GET, .handler = main_page_handler, .user_ctx = NULL};
|
||||
static const httpd_uri_t ping_uri = {.uri = "/ping", .method = HTTP_GET, .handler = ping_handler, .user_ctx = NULL};
|
||||
static const httpd_uri_t main_post_uri = {.uri = "/", .method = HTTP_POST, .handler = ping_handler, .user_ctx = NULL};
|
||||
|
||||
void web_server_init(void)
|
||||
{
|
||||
@@ -55,18 +64,22 @@ static void register_http_handlers(httpd_handle_t server)
|
||||
ESP_LOGE(TAG, "Failed to register main URI");
|
||||
}
|
||||
|
||||
err = httpd_register_uri_handler(server, &ping_uri);
|
||||
err = httpd_register_uri_handler(server, &main_post_uri);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to register ping URI");
|
||||
ESP_LOGE(TAG, "Failed to register main POST URI");
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t main_page_handler(httpd_req_t *req)
|
||||
{
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
esp_err_t err = httpd_resp_set_type(req, "text/html; charset=utf-8");
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
|
||||
esp_err_t err;
|
||||
err = httpd_resp_set_hdr(req, "Cache-Control", "no-store");
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
|
||||
err = send_page_header(req);
|
||||
if (err != ESP_OK)
|
||||
@@ -97,37 +110,131 @@ static esp_err_t send_page_header(httpd_req_t *req)
|
||||
return httpd_resp_sendstr_chunk(
|
||||
req,
|
||||
"<!DOCTYPE html>"
|
||||
"<html>"
|
||||
"<html lang=\"ru\">"
|
||||
"<head>"
|
||||
"<meta charset=\"UTF-8\">"
|
||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
|
||||
"<title>ARP Monitoring</title>"
|
||||
"<style>"
|
||||
":root{color-scheme:light;--ink:#142139;--muted:#63738c;--line:#e6ecf3;--blue:#1463ef}"
|
||||
"*{box-sizing:border-box}"
|
||||
"body{margin:0;background:#f4f7fb;color:var(--ink);font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:14px;line-height:1.5}"
|
||||
"main{max-width:1280px;margin:0 auto;padding:44px 28px 28px}"
|
||||
"h1{margin:0;font-size:36px;line-height:1.2;letter-spacing:-1.2px;color:#0e1729}"
|
||||
".intro{margin:12px 0 28px;color:var(--muted);font-size:15px}"
|
||||
".summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px;margin-bottom:26px}"
|
||||
".card{display:flex;align-items:center;gap:20px;padding:24px;background:#fff;border:1px solid var(--line);border-radius:14px;box-shadow:0 3px 14px #23395d04}"
|
||||
".icon{display:flex;align-items:center;justify-content:center;flex:none;width:54px;height:54px;border-radius:13px;background:#eaf1ff;color:var(--blue)}"
|
||||
".icon svg{width:27px;height:27px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}"
|
||||
".icon.green{background:#e8f8ee;color:#16803d}.icon.red{background:#fff0f0;color:#ca3540}"
|
||||
".card-label{margin:0;color:#485a73;font-weight:500}.card-value{display:block;font-size:32px;font-weight:700;line-height:1.2;letter-spacing:-.7px;margin:3px 0}"
|
||||
".card-note{font-size:12px;color:var(--muted)}"
|
||||
".panel{background:#fff;border:1px solid var(--line);border-radius:14px;box-shadow:0 4px 20px #23395d05;overflow:hidden}"
|
||||
".panel-head{padding:22px 24px;display:flex;align-items:center;justify-content:space-between;gap:12px}"
|
||||
"h2{font-size:19px;letter-spacing:-.4px;margin:0}.count{margin-left:8px;color:var(--muted);font-weight:500}"
|
||||
".tag{color:#576b87;background:#f4f7fb;border:1px solid var(--line);padding:4px 10px;border-radius:6px;font-size:12px;white-space:nowrap}"
|
||||
".table-scroll{overflow-x:auto}table{width:100%;border-collapse:collapse;text-align:left;white-space:nowrap}"
|
||||
"th{background:#f8fafd;color:#5c6c83;font-size:12px;font-weight:600;padding:13px 24px;border-top:1px solid var(--line)}"
|
||||
"td{padding:19px 24px;border-top:1px solid var(--line);color:#43536c}"
|
||||
"th:first-child,td:first-child{width:76px;padding-right:12px}"
|
||||
"tbody tr:hover{background:#f8fbff}tbody tr:has(input:checked){background:#f0f6ff}"
|
||||
"input[type=checkbox]{width:18px;height:18px;margin:0;vertical-align:middle;accent-color:var(--blue);cursor:pointer}"
|
||||
".address{font-weight:600;color:#1b2c47;font-variant-numeric:tabular-nums}"
|
||||
".mac{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:13px}"
|
||||
".status{display:inline-flex;align-items:center;gap:7px;border-radius:20px;padding:4px 11px;font-size:12px;font-weight:600}"
|
||||
".status:before{content:'';width:7px;height:7px;border-radius:50%;background:currentColor}"
|
||||
".online{background:#e7f8ec;color:#18763a}.offline{background:#ffeded;color:#ba303b}"
|
||||
".panel-foot{border-top:1px solid var(--line);padding:18px 24px;display:flex;align-items:center;justify-content:space-between;gap:18px}"
|
||||
".hint{margin:0;color:var(--muted);font-size:13px}"
|
||||
"button{display:inline-flex;align-items:center;justify-content:center;gap:10px;border:1px solid #1059db;border-radius:8px;background:var(--blue);color:white;padding:11px 19px;line-height:1.5;font-family:inherit;font-size:14px;font-weight:600;cursor:pointer;box-shadow:0 3px 7px #1463ef20;white-space:nowrap}"
|
||||
"button:hover{background:#0c53d3}button:active{transform:translateY(1px)}"
|
||||
"button svg{width:16px;height:16px;fill:currentColor}"
|
||||
"input:focus-visible,button:focus-visible,.table-scroll:focus-visible{outline:3px solid #87b4ff;outline-offset:4px}"
|
||||
".notes{margin:18px 4px 0;color:var(--muted);font-size:12px;line-height:1.8}.notes p{margin:0}"
|
||||
".empty{text-align:center;padding:44px 24px;white-space:normal;color:var(--muted)}"
|
||||
"@media(max-width:760px){main{padding:28px 16px}h1{font-size:29px}.intro{font-size:14px}.summary{gap:10px}.card{padding:16px;gap:10px}.icon{display:none}.card-value{font-size:27px}.card-note{font-size:11px}.panel-head{padding:18px}.panel-foot{padding:18px;flex-wrap:wrap}th,td{padding-left:18px;padding-right:18px}}"
|
||||
"@media(max-width:420px){.summary{gap:8px}.card{padding:12px 9px}.card-label{font-size:12px}.card-note{display:none}.panel-foot button{width:100%}.tag{display:none}}"
|
||||
"</style>"
|
||||
"</head>"
|
||||
"<body>"
|
||||
"<body><main>"
|
||||
"<h1>ARP Monitoring</h1>"
|
||||
"<p class=\"intro\">Отслеживание устройств в Вашей локальной сети с помощью ARP. Отслеживайте и диагностируйте подключенные устройства в реальном времени.</p>"
|
||||
);
|
||||
}
|
||||
|
||||
static esp_err_t send_device_table(httpd_req_t *req)
|
||||
{
|
||||
esp_err_t err;
|
||||
const size_t count = device_list_count();
|
||||
size_t online = 0, total = 0;
|
||||
for (size_t index = 0; index < count; index++)
|
||||
{
|
||||
const device_info_t *device = device_list_get_by_index(index);
|
||||
if (device != NULL)
|
||||
{
|
||||
total++;
|
||||
if (device->active)
|
||||
online++;
|
||||
}
|
||||
}
|
||||
|
||||
static const char *const summary_parts[] = {
|
||||
"<section class=\"summary\" aria-label=\"Сводка устройств\">"
|
||||
"<div class=\"card\"><span class=\"icon\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"13\" rx=\"2\"/><path d=\"M12 17v4m-4 0h8\"/></svg></span>"
|
||||
"<div><p class=\"card-label\">Всего устройств</p><strong class=\"card-value\">",
|
||||
"</strong><span class=\"card-note\">Обнаружено по ARP</span></div></div>"
|
||||
"<div class=\"card\"><span class=\"icon green\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\"><path d=\"M3 9a14 14 0 0 1 18 0M6 12a9 9 0 0 1 12 0M9 15a5 5 0 0 1 6 0\"/><circle cx=\"12\" cy=\"19\" r=\"1\"/></svg></span>"
|
||||
"<div><p class=\"card-label\">Online</p><strong class=\"card-value\">",
|
||||
"</strong><span class=\"card-note\">Сейчас онлайн</span></div></div>"
|
||||
"<div class=\"card\"><span class=\"icon red\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\"><path d=\"m3 3 18 18M9 6a14 14 0 0 1 12 3M3 9l2-1m9 3a9 9 0 0 1 4 1M6 12l2-1m1 4a5 5 0 0 1 6 0\"/><circle cx=\"12\" cy=\"19\" r=\"1\"/></svg></span>"
|
||||
"<div><p class=\"card-label\">Offline</p><strong class=\"card-value\">",
|
||||
"</strong><span class=\"card-note\">Не ответили</span></div></div></section>"
|
||||
"<section class=\"panel\" aria-labelledby=\"devices-title\"><div class=\"panel-head\"><h2 id=\"devices-title\">Устройства <span class=\"count\">(",
|
||||
")</span></h2><span class=\"tag\">ARP / IPv4</span></div>"
|
||||
};
|
||||
const size_t values[] = {total, online, total - online, total};
|
||||
for (size_t index = 0; index < sizeof(values) / sizeof(values[0]); index++)
|
||||
{
|
||||
err = httpd_resp_sendstr_chunk(req, summary_parts[index]);
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
char number[32];
|
||||
int length = snprintf(number, sizeof(number), "%zu", values[index]);
|
||||
if (length < 0 || (size_t)length >= sizeof(number))
|
||||
return ESP_FAIL;
|
||||
err = httpd_resp_sendstr_chunk(req, number);
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
}
|
||||
err = httpd_resp_sendstr_chunk(req, summary_parts[4]);
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
|
||||
err = httpd_resp_sendstr_chunk(
|
||||
req,
|
||||
"<form action=\"/ping\" method=\"get\">"
|
||||
"<table border=\"1\">"
|
||||
"<tr>"
|
||||
"<th>Select</th>"
|
||||
"<th>IP address</th>"
|
||||
"<th>MAC address</th>"
|
||||
"<th>Last seen</th>"
|
||||
"<th>Status</th>"
|
||||
"</tr>"
|
||||
"<form action=\"/\" method=\"post\">"
|
||||
"<div class=\"table-scroll\" tabindex=\"0\" role=\"region\" aria-label=\"Список устройств\"><table>"
|
||||
"<thead><tr>"
|
||||
"<th scope=\"col\">Выбор</th>"
|
||||
"<th scope=\"col\">IP-адрес</th>"
|
||||
"<th scope=\"col\">MAC-адрес</th>"
|
||||
"<th scope=\"col\">Последний ARP</th>"
|
||||
"<th scope=\"col\">Статус</th>"
|
||||
"</tr></thead><tbody>"
|
||||
);
|
||||
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
|
||||
for (size_t index = 0; index < device_list_count(); index++)
|
||||
if (total == 0)
|
||||
{
|
||||
err = httpd_resp_sendstr_chunk(req,
|
||||
"<tr><td colspan=\"5\" class=\"empty\">Устройства пока не обнаружены.<br>Ожидаем входящие ARP-пакеты.</td></tr>");
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < count; index++)
|
||||
{
|
||||
const device_info_t *current_device;
|
||||
current_device = device_list_get_by_index(index);
|
||||
@@ -143,29 +250,34 @@ static esp_err_t send_device_table(httpd_req_t *req)
|
||||
|
||||
return httpd_resp_sendstr_chunk(
|
||||
req,
|
||||
"</table>"
|
||||
"<br>"
|
||||
"<input type=\"submit\" value=\"Ping selected devices\">"
|
||||
"</form>"
|
||||
"</tbody></table></div>"
|
||||
"<div class=\"panel-foot\"><p class=\"hint\">Выберите устройства для проверки доступности.</p>"
|
||||
"<button type=\"submit\"><svg viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M4 2v12l10-6z\"/></svg>Проверить выбранные (Ping)</button></div>"
|
||||
"</form></section>"
|
||||
"<div class=\"notes\"><p>Последний ARP — время с последнего ARP-пакета устройства. Значения пересчитываются при загрузке страницы.</p>"
|
||||
"<p>Статус обновляется при получении ARP и проверке Ping; постоянная проверка доступности не выполняется.</p></div>"
|
||||
);
|
||||
}
|
||||
|
||||
static esp_err_t send_device_row(httpd_req_t *req, const device_info_t *device)
|
||||
{
|
||||
char html[256];
|
||||
char html[640];
|
||||
char last_seen[96];
|
||||
|
||||
snprintf(
|
||||
format_last_seen(device->last_seen, last_seen, sizeof(last_seen));
|
||||
|
||||
int length = snprintf(
|
||||
html,
|
||||
sizeof(html),
|
||||
|
||||
"<tr>"
|
||||
"<td>"
|
||||
"<input type=\"checkbox\" name=\"mac\" value=\"%02X%02X%02X%02X%02X%02X\">"
|
||||
"<input type=\"checkbox\" name=\"mac\" value=\"%02X%02X%02X%02X%02X%02X\" aria-label=\"Выбрать %u.%u.%u.%u\">"
|
||||
"</td>"
|
||||
"<td>%u.%u.%u.%u</td>"
|
||||
"<td>%02X:%02X:%02X:%02X:%02X:%02X</td>"
|
||||
"<td>%lu</td>"
|
||||
"<td class=\"address\">%u.%u.%u.%u</td>"
|
||||
"<td class=\"mac\">%02X:%02X:%02X:%02X:%02X:%02X</td>"
|
||||
"<td>%s</td>"
|
||||
"<td><span class=\"status %s\">%s</span></td>"
|
||||
"</tr>",
|
||||
|
||||
device->mac.addr[0],
|
||||
@@ -180,6 +292,11 @@ static esp_err_t send_device_row(httpd_req_t *req, const device_info_t *device)
|
||||
ip4_addr3(&device->ip),
|
||||
ip4_addr4(&device->ip),
|
||||
|
||||
ip4_addr1(&device->ip),
|
||||
ip4_addr2(&device->ip),
|
||||
ip4_addr3(&device->ip),
|
||||
ip4_addr4(&device->ip),
|
||||
|
||||
device->mac.addr[0],
|
||||
device->mac.addr[1],
|
||||
device->mac.addr[2],
|
||||
@@ -187,31 +304,106 @@ static esp_err_t send_device_row(httpd_req_t *req, const device_info_t *device)
|
||||
device->mac.addr[4],
|
||||
device->mac.addr[5],
|
||||
|
||||
(unsigned long)device->last_seen,
|
||||
last_seen,
|
||||
|
||||
device->active ? "online" : "offline",
|
||||
device->active ? "Online" : "Offline"
|
||||
);
|
||||
|
||||
if (length < 0 || (size_t)length >= sizeof(html))
|
||||
return ESP_FAIL;
|
||||
|
||||
return httpd_resp_sendstr_chunk(req, html);
|
||||
}
|
||||
|
||||
static void format_last_seen(TickType_t last_seen, char *text, size_t size)
|
||||
{
|
||||
TickType_t elapsed = (TickType_t)(xTaskGetTickCount() - last_seen);
|
||||
uint64_t seconds = (uint64_t)elapsed / configTICK_RATE_HZ;
|
||||
|
||||
if (seconds == 0)
|
||||
snprintf(text, size, "только что");
|
||||
else if (seconds < 60)
|
||||
snprintf(text, size, "%" PRIu64 " с назад", seconds);
|
||||
else if (seconds < 3600)
|
||||
snprintf(text, size, "%" PRIu64 " мин %" PRIu64 " с назад",
|
||||
seconds / 60, seconds % 60);
|
||||
else if (seconds < 86400)
|
||||
snprintf(text, size, "%" PRIu64 " ч %" PRIu64 " мин назад",
|
||||
seconds / 3600, (seconds % 3600) / 60);
|
||||
else
|
||||
snprintf(text, size, "%" PRIu64 " д %" PRIu64 " ч назад",
|
||||
seconds / 86400, (seconds % 86400) / 3600);
|
||||
}
|
||||
|
||||
static esp_err_t send_page_footer(httpd_req_t *req)
|
||||
{
|
||||
return httpd_resp_sendstr_chunk(
|
||||
req,
|
||||
"</body>"
|
||||
"</main></body>"
|
||||
"</html>"
|
||||
);
|
||||
}
|
||||
|
||||
static esp_err_t ping_handler(httpd_req_t *req)
|
||||
{
|
||||
char query[256];
|
||||
if (req->content_len == 0)
|
||||
return redirect_to_main(req);
|
||||
|
||||
if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK)
|
||||
if (req->content_len > PING_FORM_MAX_LENGTH)
|
||||
{
|
||||
ping_selected_devices(query);
|
||||
httpd_resp_set_status(req, "413 Payload Too Large");
|
||||
httpd_resp_sendstr(req, "Too many selected devices");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return main_page_handler(req);
|
||||
char *body = malloc(req->content_len + 1);
|
||||
if (body == NULL)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Not enough memory");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
size_t received = 0;
|
||||
while (received < req->content_len)
|
||||
{
|
||||
int result = httpd_req_recv(req, body + received, req->content_len - received);
|
||||
if (result <= 0)
|
||||
{
|
||||
free(body);
|
||||
if (result == HTTPD_SOCK_ERR_TIMEOUT)
|
||||
httpd_resp_send_err(req, HTTPD_408_REQ_TIMEOUT, "Request timed out");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
received += (size_t)result;
|
||||
}
|
||||
|
||||
if (memchr(body, '\0', received) != NULL)
|
||||
{
|
||||
free(body);
|
||||
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid form data");
|
||||
}
|
||||
|
||||
body[received] = '\0';
|
||||
ping_selected_devices(body);
|
||||
free(body);
|
||||
|
||||
return redirect_to_main(req);
|
||||
}
|
||||
|
||||
static esp_err_t redirect_to_main(httpd_req_t *req)
|
||||
{
|
||||
esp_err_t err = httpd_resp_set_status(req, "303 See Other");
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
|
||||
err = httpd_resp_set_hdr(req, "Location", "/");
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
|
||||
err = httpd_resp_set_hdr(req, "Cache-Control", "no-store");
|
||||
if (err != ESP_OK)
|
||||
return err;
|
||||
|
||||
return httpd_resp_sendstr(req, "");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
dependencies:
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 6.0.2
|
||||
protocol_examples_tapif_io:
|
||||
dependencies: []
|
||||
source:
|
||||
path: ../../esp-idf/examples/common_components/protocol_examples_tapif_io
|
||||
type: local
|
||||
version: '*'
|
||||
direct_dependencies:
|
||||
- protocol_examples_tapif_io
|
||||
manifest_hash: 33a56fe4b18b5bca5e7e9903993768ba91305bb35b493fb3746cfbe293644f19
|
||||
target: linux
|
||||
version: 3.0.0
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 202 KiB |
@@ -6,10 +6,13 @@ set(requires
|
||||
web_server
|
||||
esp_netif
|
||||
esp_event
|
||||
nvs_flash
|
||||
)
|
||||
|
||||
if("${target}" STREQUAL "linux")
|
||||
list(APPEND requires protocol_examples_tapif_io)
|
||||
else()
|
||||
list(APPEND requires protocol_examples_common)
|
||||
endif()
|
||||
|
||||
idf_component_register(
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
dependencies:
|
||||
protocol_examples_tapif_io:
|
||||
path: ${IDF_PATH}/examples/common_components/protocol_examples_tapif_io
|
||||
rules:
|
||||
- if: "target in [linux]"
|
||||
protocol_examples_common:
|
||||
path: ${IDF_PATH}/examples/common_components/protocol_examples_common
|
||||
rules:
|
||||
- if: "target not in [linux]"
|
||||
+6
-10
@@ -5,26 +5,22 @@
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#ifdef CONFIG_IDF_TARGET_LINUX
|
||||
#include "nvs_flash.h"
|
||||
#include "protocol_examples_common.h"
|
||||
#endif
|
||||
|
||||
static const char *TAG = "main";
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(nvs_flash_init());
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
|
||||
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
|
||||
ESP_LOGI(TAG, "Network interface initialized");
|
||||
|
||||
web_server_init();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Prepare the known-working lwIP component layout for ESP-IDF v6.0.2."""
|
||||
import filecmp
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
|
||||
def replace_once(text, old, new, label):
|
||||
if text.count(old) != 1:
|
||||
raise RuntimeError(f"Unexpected {label}; expected ESP-IDF v6.0.2 sources")
|
||||
return text.replace(old, new, 1)
|
||||
|
||||
|
||||
def write_changed(path, data):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not path.exists() or path.read_bytes() != data:
|
||||
path.write_bytes(data)
|
||||
|
||||
|
||||
def prepare(source, destination, arp):
|
||||
source, destination, arp = source.resolve(), destination.resolve(), arp.resolve()
|
||||
if not arp.is_file():
|
||||
raise RuntimeError(f"Missing modified ARP source: {arp}")
|
||||
if source == destination or source in destination.parents or destination in source.parents:
|
||||
raise RuntimeError("Source and generated directories must be separate")
|
||||
if any(c in arp.as_posix() for c in ('"', ';', '$')):
|
||||
raise RuntimeError("ARP path contains characters that require CMake escaping")
|
||||
|
||||
cmake = (source / "CMakeLists.txt").read_text()
|
||||
cmake = replace_once(cmake, '"lwip/src/core/ipv4/etharp.c"',
|
||||
f'"{arp.as_posix()}"', "lwIP CMakeLists.txt")
|
||||
cmake = replace_once(cmake,
|
||||
'if(NOT ${target} STREQUAL "linux")\n set(priv_requires vfs)\nendif()',
|
||||
'set(priv_requires)\n\nif(NOT ${target} STREQUAL "linux")\n list(APPEND priv_requires vfs)\nendif()',
|
||||
"lwIP dependencies")
|
||||
sockets_path = "lwip/src/include/lwip/sockets.h"
|
||||
sockets = (source / sockets_path).read_text()
|
||||
if "#ifndef SIOCATMARK" not in sockets:
|
||||
line = "#define SIOCATMARK _IOR('s', 7, unsigned long) /* at oob mark? */"
|
||||
sockets = replace_once(sockets, line, "#endif\n#ifndef SIOCATMARK\n" + line,
|
||||
"SIOCATMARK definition")
|
||||
fcntl_path = "port/linux/include/sys/fcntl.h"
|
||||
fcntl = (source / fcntl_path).read_text()
|
||||
if "#define F_GETFD" not in fcntl:
|
||||
fcntl = fcntl.rstrip("\n") + "\n\n#ifndef F_GETFD\n#define F_GETFD 1\n#endif\n"
|
||||
changed = {"CMakeLists.txt": cmake.encode(), sockets_path: sockets.encode(),
|
||||
fcntl_path: fcntl.encode()}
|
||||
|
||||
# Validate all substitutions before writing. Do not copy or compile stock ARP.
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
for path in source.rglob("*"):
|
||||
relative = path.relative_to(source)
|
||||
if ".git" in relative.parts or not path.is_file():
|
||||
continue
|
||||
name = relative.as_posix()
|
||||
if name in changed or name == "lwip/src/core/ipv4/etharp.c":
|
||||
continue
|
||||
target = destination / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not target.is_file() or not filecmp.cmp(path, target, shallow=False):
|
||||
shutil.copy2(path, target)
|
||||
for name, data in changed.items():
|
||||
write_changed(destination / name, data)
|
||||
stock_arp = destination / "lwip/src/core/ipv4/etharp.c"
|
||||
if stock_arp.exists():
|
||||
stock_arp.unlink()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
if len(sys.argv) != 4:
|
||||
raise RuntimeError("Usage: prepare.py SDK_LWIP BUILD_LWIP PATCHED_ETHARP")
|
||||
prepare(*(Path(value) for value in sys.argv[1:]))
|
||||
except (OSError, RuntimeError) as error:
|
||||
sys.exit(str(error))
|
||||
+1
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "esp_netif.h"
|
||||
#include "esp_netif_net_stack.h"
|
||||
#include "tapif.h"
|
||||
|
||||
#define IFNAME0 't'
|
||||
#define IFNAME1 'p'
|
||||
+1
@@ -6,6 +6,7 @@
|
||||
* SPDX-FileContributor: 2022-2023 Espressif Systems (Shanghai) CO LTD
|
||||
*/
|
||||
#pragma once
|
||||
#include "esp_err.h"
|
||||
#include "lwip/esp_netif_net_stack.h"
|
||||
|
||||
/**
|
||||
Reference in New Issue
Block a user