Подключены ESP32|Linux, обновлен интерфейс

This commit is contained in:
2026-09-11 20:53:38 +03:00
parent 9fce687690
commit e7a01aaeda
23 changed files with 477 additions and 647 deletions
File diff suppressed because it is too large Load Diff
@@ -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,103 +0,0 @@
/*
* 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 <string.h>
#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;
}
@@ -1,25 +0,0 @@
/*
* 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);
@@ -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
+228 -36
View File
@@ -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)
@@ -74,7 +87,7 @@ static esp_err_t main_page_handler(httpd_req_t *req)
ESP_LOGE(TAG, "Page header isn't created");
return err;
}
err = send_device_table(req);
if (err != ESP_OK)
{
@@ -88,7 +101,7 @@ static esp_err_t main_page_handler(httpd_req_t *req)
ESP_LOGE(TAG, "Page footer isn't created");
return err;
}
return httpd_resp_sendstr_chunk(req, NULL);
}
@@ -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);
@@ -142,30 +249,35 @@ 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>"
req,
"</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, "");
}