feat(esp_eth): add Ethernet sublayer with optional VLAN and switch

Introduce the experimental Ethernet sublayer (parent/child VLAN, optional
switch path, iodriver provider), restructure eth test apps, and document
the new APIs.
This commit is contained in:
Ondrej Kosta
2026-08-13 13:56:57 +02:00
parent 2dd71fddaa
commit 2e79c3fd69
61 changed files with 3860 additions and 45 deletions
@@ -1,6 +1,6 @@
# Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps
components/esp_eth/test_apps:
components/esp_eth/test_apps/test_app_driver:
enable:
- if: IDF_TARGET in ["esp32", "esp32p4", "esp32s31"]
reason: ESP32, ESP32P4 and ESP32S31 have internal EMAC.
@@ -13,3 +13,12 @@ components/esp_eth/test_apps:
- hal
- lwip
- soc
components/esp_eth/test_apps/test_app_sublayer:
enable:
- if: IDF_TARGET in ["esp32", "esp32p4"]
reason: ESP32 and ESP32P4 have internal EMAC.
depends_components:
- esp_eth
- esp_netif
- lwip
@@ -191,48 +191,58 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]")
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
ESP_LOGI(TAG, "-- Verify transmission using extended Tx fnc using one buffer--");
esp_eth_buf_desc_t tx_bufs[3] = {
{ .buf = (uint8_t *)test_pkt },
};
transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE;
ESP_LOGI(TAG, "transmit frame size: %" PRIu16, transmit_size);
recv_info.expected_size = transmit_size;
eth_mac_time_t ts;
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, &ts, 2, test_pkt, transmit_size));
tx_bufs[0].len = transmit_size;
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, &ts, tx_bufs, 1));
printf("test %lu.%lu sec\n", ts.seconds, ts.nanoseconds); // TODO finish the test
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE - 1;
ESP_LOGI(TAG, "transmit frame size: %" PRIu16, transmit_size);
recv_info.expected_size = transmit_size;
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 2, test_pkt, transmit_size));
tx_bufs[0].len = transmit_size;
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 1));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE + 1;
ESP_LOGI(TAG, "transmit frame size: %" PRIu16, transmit_size);
recv_info.expected_size = transmit_size;
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 2, test_pkt, transmit_size));
tx_bufs[0].len = transmit_size;
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 1));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
transmit_size = 2 * CONFIG_ETH_DMA_BUFFER_SIZE;
ESP_LOGI(TAG, "transmit frame size: %" PRIu16, transmit_size);
recv_info.expected_size = transmit_size;
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 2, test_pkt, transmit_size));
tx_bufs[0].len = transmit_size;
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 1));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
transmit_size = 2 * CONFIG_ETH_DMA_BUFFER_SIZE - 1;
ESP_LOGI(TAG, "transmit frame size: %" PRIu16, transmit_size);
recv_info.expected_size = transmit_size;
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 2, test_pkt, transmit_size));
tx_bufs[0].len = transmit_size;
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 1));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
transmit_size = 2 * CONFIG_ETH_DMA_BUFFER_SIZE + 1;
ESP_LOGI(TAG, "transmit frame size: %" PRIu16, transmit_size);
recv_info.expected_size = transmit_size;
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 2, test_pkt, transmit_size));
tx_bufs[0].len = transmit_size;
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 1));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
ESP_LOGI(TAG, "-- Verify transmission using extended Tx func with multiple buffers --");
uint16_t transmit_size_2;
// allocated the second buffer
uint8_t *pkt_data_2 = (uint8_t *)eth_test_alloc(ETH_MAX_PAYLOAD_LEN);
tx_bufs[1].buf = pkt_data_2;
// fill with data (reverse order to differentiate the buffers)
int j = ETH_MAX_PAYLOAD_LEN;
for (int i = 0; i < ETH_MAX_PAYLOAD_LEN; i++) {
@@ -247,9 +257,11 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]")
transmit_size_2 = CONFIG_ETH_DMA_BUFFER_SIZE;
recv_info.expected_size = transmit_size;
recv_info.expected_size_2 = transmit_size_2;
tx_bufs[0].len = transmit_size;
tx_bufs[1].len = transmit_size_2;
for (int32_t i = 0; i < config_eth_dma_max_buffer_num*2; i++) {
ESP_LOGI(TAG, "transmit joint frame size: %" PRIu16 ", i = %" PRIi32, transmit_size + transmit_size_2, i);
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 4, test_pkt, transmit_size, pkt_data_2, transmit_size_2));
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 2));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
}
@@ -258,8 +270,10 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]")
transmit_size_2 = CONFIG_ETH_DMA_BUFFER_SIZE;
recv_info.expected_size = transmit_size;
recv_info.expected_size_2 = transmit_size_2;
tx_bufs[0].len = transmit_size;
tx_bufs[1].len = transmit_size_2;
ESP_LOGI(TAG, "transmit joint frame size: %" PRIu16, transmit_size + transmit_size_2);
TEST_ESP_OK(esp_eth_transmit_vargs(eth_handle, 2, test_pkt, transmit_size, pkt_data_2, transmit_size_2));
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 2));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
ESP_LOGI(TAG, "Verify boundary conditions");
@@ -267,29 +281,36 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]")
transmit_size_2 = CONFIG_ETH_DMA_BUFFER_SIZE;
recv_info.expected_size = transmit_size;
recv_info.expected_size_2 = transmit_size_2;
tx_bufs[0].len = transmit_size;
tx_bufs[1].len = transmit_size_2;
ESP_LOGI(TAG, "transmit joint frame size: %" PRIu16, transmit_size + transmit_size_2);
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 4, test_pkt, transmit_size, pkt_data_2, transmit_size_2));
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 2));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE - 1;
transmit_size_2 = CONFIG_ETH_DMA_BUFFER_SIZE;
recv_info.expected_size = transmit_size;
recv_info.expected_size_2 = transmit_size_2;
tx_bufs[0].len = transmit_size;
tx_bufs[1].len = transmit_size_2;
ESP_LOGI(TAG, "transmit joint frame size: %" PRIu16, transmit_size + transmit_size_2);
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 4, test_pkt, transmit_size, pkt_data_2, transmit_size_2));
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 2));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE + 1;
transmit_size_2 = CONFIG_ETH_DMA_BUFFER_SIZE;
recv_info.expected_size = transmit_size;
recv_info.expected_size_2 = transmit_size_2;
tx_bufs[0].len = transmit_size;
tx_bufs[1].len = transmit_size_2;
ESP_LOGI(TAG, "transmit joint frame size: %" PRIu16, transmit_size + transmit_size_2);
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 4, test_pkt, transmit_size, pkt_data_2, transmit_size_2));
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 2));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
uint16_t transmit_size_3 = 256;
// allocated the third buffer
uint8_t *pkt_data_3 = (uint8_t *)eth_test_alloc(256);
tx_bufs[2].buf = pkt_data_3;
// fill with data
for (int i = 0; i < 256; i++) {
pkt_data_3[i] = i & 0xFF;
@@ -302,8 +323,11 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]")
recv_info.expected_size = transmit_size;
recv_info.expected_size_2 = transmit_size_2;
recv_info.expected_size_3 = transmit_size_3;
tx_bufs[0].len = transmit_size;
tx_bufs[1].len = transmit_size_2;
tx_bufs[2].len = transmit_size_3;
ESP_LOGI(TAG, "transmit joint frame size (3 buffs): %" PRIu16, transmit_size + transmit_size_2 + transmit_size_3);
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 6, test_pkt, transmit_size, pkt_data_2, transmit_size_2, pkt_data_3, transmit_size_3));
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 3));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE - 1;
@@ -312,8 +336,11 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]")
recv_info.expected_size = transmit_size;
recv_info.expected_size_2 = transmit_size_2;
recv_info.expected_size_3 = transmit_size_3;
tx_bufs[0].len = transmit_size;
tx_bufs[1].len = transmit_size_2;
tx_bufs[2].len = transmit_size_3;
ESP_LOGI(TAG, "transmit joint frame size (3 buffs): %" PRIu16, transmit_size + transmit_size_2 + transmit_size_3);
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 6, test_pkt, transmit_size, pkt_data_2, transmit_size_2, pkt_data_3, transmit_size_3));
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 3));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
transmit_size = CONFIG_ETH_DMA_BUFFER_SIZE + 1;
@@ -322,8 +349,11 @@ TEST_CASE("internal emac receive/transmit", "[esp_emac]")
recv_info.expected_size = transmit_size;
recv_info.expected_size_2 = transmit_size_2;
recv_info.expected_size_3 = transmit_size_3;
tx_bufs[0].len = transmit_size;
tx_bufs[1].len = transmit_size_2;
tx_bufs[2].len = transmit_size_3;
ESP_LOGI(TAG, "transmit joint frame size (3 buffs): %" PRIu16, transmit_size + transmit_size_2 + transmit_size_3);
TEST_ESP_OK(esp_eth_transmit_ctrl_vargs(eth_handle, NULL, 6, test_pkt, transmit_size, pkt_data_2, transmit_size_2, pkt_data_3, transmit_size_3));
TEST_ESP_OK(esp_eth_transmit_ctrl_bufs(eth_handle, NULL, tx_bufs, 3));
TEST_ASSERT(xSemaphoreTake(recv_info.mutex, pdMS_TO_TICKS(500)));
// stop Ethernet driver
@@ -0,0 +1,5 @@
# This is the project CMakeLists.txt file for the test subproject
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp_eth_sublayer_test)
@@ -0,0 +1,2 @@
| Supported Targets | ESP32 | ESP32-P4 |
| ----------------- | ----- | -------- |
@@ -0,0 +1,7 @@
idf_component_register(SRCS "test_main.c"
"test_sublayer_common.c"
"test_sublayer_events.c"
"test_sublayer_hooks.c"
INCLUDE_DIRS "."
PRIV_REQUIRES unity esp_eth esp_netif esp_event lwip
WHOLE_ARCHIVE)
@@ -0,0 +1,4 @@
## IDF Component Manager Manifest File
dependencies:
espressif/ethernet_init:
version: "^1.3.0"
@@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include "unity.h"
#include "unity_test_utils_memory.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include "test_sublayer_common.h"
#ifndef CONFIG_ETH_SUBLAYER_SUPPORT
#error "This test app requires CONFIG_ETH_SUBLAYER_SUPPORT"
#endif
#define TEST_TASK_STACK_SIZE 8192
#define TEST_TASK_PRIORITY 5
/* Some resources are lazy allocated in lwIP which we don't have under control */
#define TEST_MEMORY_LEAK_THRESHOLD 400
void setUp(void)
{
unity_utils_record_free_mem();
}
void tearDown(void)
{
sublayer_test_force_teardown();
unity_utils_evaluate_leaks_direct(TEST_MEMORY_LEAK_THRESHOLD);
}
static void test_task(void *pvParameters)
{
unity_run_menu();
}
void app_main(void)
{
xTaskCreatePinnedToCore(test_task, "testTask", TEST_TASK_STACK_SIZE,
NULL, TEST_TASK_PRIORITY, NULL, tskNO_AFFINITY);
}
@@ -0,0 +1,196 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <string.h>
#include "test_sublayer_common.h"
#include "esp_vfs_l2tap.h"
#include "esp_log.h"
#include "unity.h"
#include "ethernet_init.h"
static const char *TAG = "sublayer_test_common";
static sublayer_test_ctx_t *s_active_ctx;
/* ------------------------------------------------------------------ */
/* ETH event handler */
/* ------------------------------------------------------------------ */
static void eth_event_handler(void *arg, esp_event_base_t base,
int32_t event_id, void *event_data)
{
EventGroupHandle_t eg = (EventGroupHandle_t)arg;
(void)base;
(void)event_data;
switch (event_id) {
case ETHERNET_EVENT_START:
xEventGroupSetBits(eg, SUBLAYER_TEST_ETH_START_BIT);
break;
case ETHERNET_EVENT_CONNECTED:
xEventGroupSetBits(eg, SUBLAYER_TEST_ETH_CONNECT_BIT);
break;
case ETHERNET_EVENT_STOP:
xEventGroupSetBits(eg, SUBLAYER_TEST_ETH_STOP_BIT);
break;
default:
break;
}
}
/* ------------------------------------------------------------------ */
/* Granular init helpers */
/* ------------------------------------------------------------------ */
void sublayer_test_create_event_loop(sublayer_test_ctx_t *ctx)
{
TEST_ESP_OK(esp_netif_init());
if (esp_event_loop_create_default() == ESP_ERR_INVALID_STATE) {
esp_event_loop_delete_default();
TEST_ESP_OK(esp_event_loop_create_default());
}
ctx->eg = xEventGroupCreate();
TEST_ASSERT_NOT_NULL(ctx->eg);
TEST_ESP_OK(esp_event_handler_instance_register(ETH_EVENT, ESP_EVENT_ANY_ID,
eth_event_handler, ctx->eg,
&ctx->eth_evt_inst));
}
void sublayer_test_init_ethernet(sublayer_test_ctx_t *ctx)
{
TEST_ESP_OK(ethernet_init_all(&ctx->eth_handles, &ctx->eth_cnt));
TEST_ASSERT_GREATER_THAN(0, ctx->eth_cnt);
}
void sublayer_test_register_l2tap(sublayer_test_ctx_t *ctx)
{
esp_err_t ret = esp_vfs_l2tap_intf_register(NULL);
if (ret == ESP_ERR_INVALID_STATE) {
esp_vfs_l2tap_intf_unregister(NULL);
TEST_ESP_OK(esp_vfs_l2tap_intf_register(NULL));
} else {
TEST_ESP_OK(ret);
}
ctx->l2tap_registered = true;
}
esp_netif_t *sublayer_test_create_eth_netif(sublayer_test_ctx_t *ctx,
const char *if_key,
const char *if_desc)
{
esp_netif_inherent_config_t base_cfg = ESP_NETIF_INHERENT_DEFAULT_ETH();
base_cfg.if_key = if_key;
base_cfg.if_desc = if_desc;
esp_netif_config_t netif_cfg = {
.base = &base_cfg,
.driver = NULL,
.stack = ESP_NETIF_NETSTACK_DEFAULT_ETH,
};
esp_netif_t *netif = esp_netif_new(&netif_cfg);
TEST_ASSERT_NOT_NULL(netif);
TEST_ASSERT_LESS_THAN(SUBLAYER_TEST_MAX_NETIFS, ctx->netif_count);
ctx->netifs[ctx->netif_count++] = netif;
return netif;
}
void sublayer_test_start_and_wait_connect(sublayer_test_ctx_t *ctx)
{
bool loopback_en = true;
esp_eth_ioctl(ctx->eth_handles[0], ETH_CMD_S_PHY_LOOPBACK, &loopback_en);
TEST_ESP_OK(esp_eth_start(ctx->eth_handles[0]));
EventBits_t bits;
bits = xEventGroupWaitBits(ctx->eg, SUBLAYER_TEST_ETH_START_BIT, pdTRUE, pdTRUE,
pdMS_TO_TICKS(SUBLAYER_TEST_ETH_START_TIMEOUT_MS));
TEST_ASSERT_BITS(SUBLAYER_TEST_ETH_START_BIT, SUBLAYER_TEST_ETH_START_BIT, bits);
bits = xEventGroupWaitBits(ctx->eg, SUBLAYER_TEST_ETH_CONNECT_BIT, pdTRUE, pdTRUE,
pdMS_TO_TICKS(SUBLAYER_TEST_ETH_CONNECT_TIMEOUT_MS));
TEST_ASSERT_BITS(SUBLAYER_TEST_ETH_CONNECT_BIT, SUBLAYER_TEST_ETH_CONNECT_BIT, bits);
vTaskDelay(pdMS_TO_TICKS(SUBLAYER_TEST_EVENT_PROPAGATION_MS));
}
/* ------------------------------------------------------------------ */
/* Teardown */
/* ------------------------------------------------------------------ */
void sublayer_test_teardown(sublayer_test_ctx_t *ctx)
{
if (ctx->eth_handles != NULL && ctx->eth_cnt > 0) {
esp_eth_stop(ctx->eth_handles[0]);
if (ctx->eg != NULL) {
xEventGroupWaitBits(ctx->eg, SUBLAYER_TEST_ETH_STOP_BIT, pdTRUE, pdTRUE,
pdMS_TO_TICKS(SUBLAYER_TEST_ETH_STOP_TIMEOUT_MS));
}
vTaskDelay(pdMS_TO_TICKS(100));
}
for (int i = 0; i < ctx->netif_count; i++) {
if (ctx->netifs[i] != NULL) {
esp_netif_destroy(ctx->netifs[i]);
ctx->netifs[i] = NULL;
}
}
ctx->netif_count = 0;
if (ctx->sub != NULL) {
esp_eth_sublayer_del(ctx->sub);
ctx->sub = NULL;
}
if (ctx->eth_evt_inst != NULL) {
esp_event_handler_instance_unregister(ETH_EVENT, ESP_EVENT_ANY_ID, ctx->eth_evt_inst);
ctx->eth_evt_inst = NULL;
}
if (ctx->eth_handles != NULL) {
ethernet_deinit_all(ctx->eth_handles);
ctx->eth_handles = NULL;
ctx->eth_cnt = 0;
}
if (ctx->l2tap_registered) {
esp_vfs_l2tap_intf_unregister(NULL);
ctx->l2tap_registered = false;
}
if (ctx->eg != NULL) {
vEventGroupDelete(ctx->eg);
ctx->eg = NULL;
}
esp_event_loop_delete_default();
}
/* ------------------------------------------------------------------ */
/* Force-teardown safety net */
/* ------------------------------------------------------------------ */
void sublayer_test_register_ctx(sublayer_test_ctx_t *ctx)
{
s_active_ctx = ctx;
}
void sublayer_test_unregister_ctx(void)
{
s_active_ctx = NULL;
}
void sublayer_test_force_teardown(void)
{
if (s_active_ctx == NULL) {
return;
}
ESP_LOGW(TAG, "Force teardown triggered — cleaning up after test failure");
sublayer_test_teardown(s_active_ctx);
s_active_ctx = NULL;
}
@@ -0,0 +1,105 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#pragma once
#include "sdkconfig.h"
#ifndef CONFIG_ETH_SUBLAYER_SUPPORT
#error "This test app requires CONFIG_ETH_SUBLAYER_SUPPORT"
#endif
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_eth.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SUBLAYER_TEST_ETH_START_BIT BIT0
#define SUBLAYER_TEST_ETH_CONNECT_BIT BIT1
#define SUBLAYER_TEST_ETH_STOP_BIT BIT2
#define SUBLAYER_TEST_ETH_START_TIMEOUT_MS 10000
#define SUBLAYER_TEST_ETH_CONNECT_TIMEOUT_MS 30000
#define SUBLAYER_TEST_ETH_STOP_TIMEOUT_MS 10000
#define SUBLAYER_TEST_EVENT_PROPAGATION_MS 500
#define SUBLAYER_TEST_MAX_NETIFS 4
/**
* Base test context shared by all sublayer test modules.
* Embed as the FIRST member of test-specific context structs.
*/
typedef struct {
esp_eth_handle_t *eth_handles;
uint8_t eth_cnt;
EventGroupHandle_t eg;
esp_eth_sublayer_handle_t sub;
esp_event_handler_instance_t eth_evt_inst;
esp_netif_t *netifs[SUBLAYER_TEST_MAX_NETIFS];
uint8_t netif_count;
bool l2tap_registered;
} sublayer_test_ctx_t;
/**
* Create the default event loop and register an ETH event handler
* that signals START/CONNECT/STOP bits on ctx->eg.
*/
void sublayer_test_create_event_loop(sublayer_test_ctx_t *ctx);
/**
* Initialize all available Ethernet drivers (PHY + MAC).
* Populates ctx->eth_handles and ctx->eth_cnt.
*/
void sublayer_test_init_ethernet(sublayer_test_ctx_t *ctx);
/**
* Register L2TAP VFS interface (/dev/net/tap).
* Sets ctx->l2tap_registered so teardown knows to unregister.
*/
void sublayer_test_register_l2tap(sublayer_test_ctx_t *ctx);
/**
* Create an esp_netif with custom if_key/if_desc and track it in
* ctx->netifs[] for automatic cleanup during teardown.
*/
esp_netif_t *sublayer_test_create_eth_netif(sublayer_test_ctx_t *ctx,
const char *if_key,
const char *if_desc);
/**
* Enable PHY loopback, start ETH driver, and wait for START + CONNECT events.
*/
void sublayer_test_start_and_wait_connect(sublayer_test_ctx_t *ctx);
/**
* Graceful teardown: stop ETH, destroy tracked netifs, delete sublayer,
* unregister events, deinit ETH drivers, unregister L2TAP, delete event loop.
*/
void sublayer_test_teardown(sublayer_test_ctx_t *ctx);
/**
* Register the active test context for the force-teardown safety net.
*/
void sublayer_test_register_ctx(sublayer_test_ctx_t *ctx);
/**
* Clear the registered active context (call at end of normal teardown).
*/
void sublayer_test_unregister_ctx(void);
/**
* Force-teardown safety net called from Unity tearDown() — cleans up
* after a test failure regardless of which test module was active.
*/
void sublayer_test_force_teardown(void);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,210 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/*
* Target tests for the Ethernet sublayer event forwarding logic.
*
* Tested behaviour:
* - Without a connect-confirm event the sublayer forwards
* ETHERNET_EVENT_CONNECTED straight to esp_netif_action_connected(),
* bringing the netif up immediately.
* - With a connect-confirm event the netif stays down after
* ETHERNET_EVENT_CONNECTED and only comes up when the custom
* confirm event is posted.
* - ETHERNET_EVENT_DISCONNECTED brings the netif down.
* - A custom disconnect-trigger event also brings the netif down.
*
* PHY loopback mode is enabled so the tests are self-contained and do
* not require a physical cable.
*/
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_eth.h"
#include "esp_log.h"
#include "unity.h"
#include "ethernet_init.h"
#include "test_sublayer_common.h"
static const char *TAG = "sublayer_events_test";
#define EVENT_PROPAGATION_MS 200
ESP_EVENT_DEFINE_BASE(TEST_SUBLAYER_EVENT);
enum {
TEST_CONFIRM_EVENT_ID = 0,
TEST_DISCONNECT_EVENT_ID = 1,
};
/* ------------------------------------------------------------------ */
/* Test context and helpers */
/* ------------------------------------------------------------------ */
typedef struct {
sublayer_test_ctx_t base;
esp_netif_t *netif;
esp_eth_sublayer_vlan_handle_t vlan_hdl;
} events_test_ctx_t;
static void test_setup(events_test_ctx_t *ctx, const esp_eth_sublayer_config_t *sub_cfg_template)
{
memset(ctx, 0, sizeof(*ctx));
sublayer_test_create_event_loop(&ctx->base);
sublayer_test_init_ethernet(&ctx->base);
sublayer_test_register_ctx(&ctx->base);
esp_eth_sublayer_config_t cfg = *sub_cfg_template;
cfg.eth_handle = ctx->base.eth_handles[0];
TEST_ESP_OK(esp_eth_sublayer_new(&cfg, &ctx->base.sub));
TEST_ESP_OK(esp_eth_sublayer_vlan_add(ctx->base.sub, ESP_ETH_SUBLAYER_UNTAGGED_VID, &ctx->vlan_hdl));
ctx->netif = sublayer_test_create_eth_netif(&ctx->base, "ETH_DEF", "eth");
TEST_ESP_OK(esp_netif_attach(ctx->netif, ctx->vlan_hdl));
}
static void test_start_and_wait_connect(events_test_ctx_t *ctx)
{
sublayer_test_start_and_wait_connect(&ctx->base);
}
static void test_teardown(events_test_ctx_t *ctx)
{
sublayer_test_teardown(&ctx->base);
sublayer_test_unregister_ctx();
}
/* ------------------------------------------------------------------ */
/* Test cases */
/* ------------------------------------------------------------------ */
TEST_CASE("connect without confirm event sets link up", "[sublayer_events]")
{
ESP_LOGI(TAG, "--- no confirm event: CONNECTED should bring netif up immediately ---");
esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT();
events_test_ctx_t ctx;
test_setup(&ctx, &sub_cfg);
test_start_and_wait_connect(&ctx);
TEST_ASSERT_TRUE_MESSAGE(esp_netif_is_netif_up(ctx.netif),
"netif must be up after CONNECTED (no confirm event)");
test_teardown(&ctx);
}
TEST_CASE("connect with confirm event defers link up", "[sublayer_events]")
{
ESP_LOGI(TAG, "--- with confirm event: CONNECTED alone must NOT bring netif up ---");
esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT();
sub_cfg.connect_confirm_event = (esp_eth_sublayer_event_t){
.base = TEST_SUBLAYER_EVENT,
.event_id = TEST_CONFIRM_EVENT_ID,
};
events_test_ctx_t ctx;
test_setup(&ctx, &sub_cfg);
test_start_and_wait_connect(&ctx);
TEST_ASSERT_FALSE_MESSAGE(esp_netif_is_netif_up(ctx.netif),
"netif must stay down after CONNECTED when confirm event is configured");
ESP_LOGI(TAG, "--- posting confirm event ---");
TEST_ESP_OK(esp_event_post(TEST_SUBLAYER_EVENT, TEST_CONFIRM_EVENT_ID,
NULL, 0, portMAX_DELAY));
vTaskDelay(pdMS_TO_TICKS(EVENT_PROPAGATION_MS));
TEST_ASSERT_TRUE_MESSAGE(esp_netif_is_netif_up(ctx.netif),
"netif must be up after confirm event");
test_teardown(&ctx);
}
TEST_CASE("confirm event prior connect sets link up", "[sublayer_events]")
{
ESP_LOGI(TAG, "--- confirm event posted before CONNECTED: netif must come up on CONNECTED ---");
esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT();
sub_cfg.connect_confirm_event = (esp_eth_sublayer_event_t){
.base = TEST_SUBLAYER_EVENT,
.event_id = TEST_CONFIRM_EVENT_ID,
};
events_test_ctx_t ctx;
test_setup(&ctx, &sub_cfg);
ESP_LOGI(TAG, "--- posting confirm event while the physical link is down ---");
TEST_ESP_OK(esp_event_post(TEST_SUBLAYER_EVENT, TEST_CONFIRM_EVENT_ID,
NULL, 0, portMAX_DELAY));
vTaskDelay(pdMS_TO_TICKS(EVENT_PROPAGATION_MS));
TEST_ASSERT_FALSE_MESSAGE(esp_netif_is_netif_up(ctx.netif),
"netif must stay down when only the confirm event was received");
test_start_and_wait_connect(&ctx);
TEST_ASSERT_TRUE_MESSAGE(esp_netif_is_netif_up(ctx.netif),
"netif must be up after CONNECTED when the confirm event was received before");
test_teardown(&ctx);
}
TEST_CASE("eth disconnect event sets link down", "[sublayer_events]")
{
ESP_LOGI(TAG, "--- ETHERNET_EVENT_DISCONNECTED should bring netif down ---");
esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT();
events_test_ctx_t ctx;
test_setup(&ctx, &sub_cfg);
test_start_and_wait_connect(&ctx);
TEST_ASSERT_TRUE_MESSAGE(esp_netif_is_netif_up(ctx.netif),
"precondition: netif must be up before disconnect test");
esp_eth_handle_t hdl = ctx.base.eth_handles[0];
TEST_ESP_OK(esp_event_post(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED,
&hdl, sizeof(hdl), portMAX_DELAY));
vTaskDelay(pdMS_TO_TICKS(EVENT_PROPAGATION_MS));
TEST_ASSERT_FALSE_MESSAGE(esp_netif_is_netif_up(ctx.netif),
"netif must be down after ETHERNET_EVENT_DISCONNECTED");
test_teardown(&ctx);
}
TEST_CASE("custom disconnect trigger sets link down", "[sublayer_events]")
{
ESP_LOGI(TAG, "--- custom disconnect trigger should bring netif down ---");
esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT();
sub_cfg.disconnect_trigger_event = (esp_eth_sublayer_event_t){
.base = TEST_SUBLAYER_EVENT,
.event_id = TEST_DISCONNECT_EVENT_ID,
};
events_test_ctx_t ctx;
test_setup(&ctx, &sub_cfg);
test_start_and_wait_connect(&ctx);
TEST_ASSERT_TRUE_MESSAGE(esp_netif_is_netif_up(ctx.netif),
"precondition: netif must be up before disconnect trigger test");
TEST_ESP_OK(esp_event_post(TEST_SUBLAYER_EVENT, TEST_DISCONNECT_EVENT_ID,
NULL, 0, portMAX_DELAY));
vTaskDelay(pdMS_TO_TICKS(EVENT_PROPAGATION_MS));
TEST_ASSERT_FALSE_MESSAGE(esp_netif_is_netif_up(ctx.netif),
"netif must be down after custom disconnect trigger");
test_teardown(&ctx);
}
@@ -0,0 +1,757 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/*
* Target tests for Ethernet sublayer TX / RX / post-TX hooks.
*
* PHY loopback mode and L2TAP read/write are used so the tests are
* self-contained. Both untagged and tagged (802.1Q) netifs are
* exercised in the same cases where applicable.
*/
#include <string.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <unistd.h>
#include <errno.h>
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_eth.h"
#include "esp_eth_spec.h"
#include "esp_log.h"
#include "esp_vfs_l2tap.h"
#include "unity.h"
#include "ethernet_init.h"
#include "lwip/prot/ethernet.h"
#include "arpa/inet.h"
#include "test_sublayer_common.h"
static const char *TAG = "sublayer_hooks_test";
#define L2TAP_SEND_DELAY_MS 1000
#define L2TAP_IO_TIMEOUT_MS 5000
#define IFKEY_UNTAGGED "ETH_DEF"
#define IFKEY_VLAN "ETH_VLAN"
#define TEST_VLAN_ID 100
#define ETH_TYPE_HOOK_UNTAGGED 0x88B5
#define ETH_TYPE_HOOK_VLAN 0x88B6
#define ETH_TYPE_HOOK_HEAD 0x88B7 /* Ethertype of the TX hook inserted leading segment */
#define TX_PREFIX_HEAD_SEG_LEN (ETH_HEADER_LEN + TX_PREFIX_LEN)
#define TX_PREFIX_MAGIC_0 0xDE
#define TX_PREFIX_MAGIC_1 0xAD
#define TX_PREFIX_MAGIC_2 0xBE
#define TX_PREFIX_MAGIC_3 0xEF
#define TX_PREFIX_LEN 4
#define FRAME_MARKER_TX 0xA5
#define FRAME_MARKER_RX 0x5A
/*
* Use large frames so a missed free in a sublayer/hook path exceeds the
* non-zero leak threshold (see TEST_MEMORY_LEAK_THRESHOLD in test_main.c;
* some lwIP resources are lazy-allocated and not under our control).
*/
#define HOOK_TEST_FRAME_LEN 1000
#define HOOK_TEST_RX_BUF_LEN (HOOK_TEST_FRAME_LEN + TX_PREFIX_LEN)
typedef enum {
HOOK_TX_OP_NONE = 0,
HOOK_TX_OP_PREFIX,
HOOK_TX_OP_PREFIX_HEAD,
HOOK_TX_OP_MERGE,
HOOK_TX_OP_SKIP,
HOOK_TX_OP_ABORT,
HOOK_TX_OP_POST_TX_FREE,
} hook_tx_op_t;
typedef enum {
HOOK_RX_OP_NONE = 0,
HOOK_RX_OP_MODIFY,
HOOK_RX_OP_DROP,
HOOK_RX_OP_ABORT,
} hook_rx_op_t;
typedef enum {
HOOK_DIR_NONE = 0,
HOOK_DIR_TX,
HOOK_DIR_RX,
} hook_dir_t;
typedef struct {
hook_tx_op_t op;
uint32_t hook_calls;
uint32_t post_hook_calls;
uint32_t rx_strip_calls; /*!< PREFIX_HEAD companion strips on loopback */
void *freed_ptr;
} hooks_tx_ctx_t;
typedef struct {
hook_rx_op_t op;
uint32_t hook_calls;
} hooks_rx_ctx_t;
typedef struct {
hook_dir_t dir;
union {
hooks_tx_ctx_t tx;
hooks_rx_ctx_t rx;
};
} hooks_ctx_t;
typedef struct {
struct eth_hdr header;
uint8_t marker;
uint8_t pad[HOOK_TEST_FRAME_LEN - ETH_HEADER_LEN - 1];
} __attribute__((packed)) hook_test_frame_t;
_Static_assert(sizeof(hook_test_frame_t) == HOOK_TEST_FRAME_LEN,
"hook_test_frame_t must be HOOK_TEST_FRAME_LEN bytes");
typedef struct {
sublayer_test_ctx_t base;
esp_eth_sublayer_vlan_handle_t vlan_untagged;
esp_eth_sublayer_vlan_handle_t vlan_tagged;
hooks_ctx_t *hook_ctx;
} hooks_test_ctx_t;
static hooks_ctx_t s_hooks_ctx;
/* ------------------------------------------------------------------ */
/* Hook implementations */
/* ------------------------------------------------------------------ */
/**
* TX hook used by the sublayer hook tests. The active scenario is selected
* via hooks_ctx_t::tx.op when dir == HOOK_DIR_TX.
*
* Scenarios exercised (see esp_eth_sublayer.h TX hook contract):
* - HOOK_TX_OP_PREFIX: Modify buf pointers/len and increase buf_count —
* insert a 4-byte prefix before payload; on untagged
* frames split one descriptor into header + prefixed
* payload (bufs[0]/bufs[1]); on VLAN-tagged frames
* the sublayer already provides two descriptors.
* - HOOK_TX_OP_PREFIX_HEAD: Insert a new custom leading bufs[0] (broadcast DA,
* device SA, ETH_TYPE_HOOK_HEAD, magic payload), shift
* original descriptors down and increase buf_count.
* The RX hook strips that segment on loopback by
* recognizing ETH_TYPE_HOOK_HEAD.
* - HOOK_TX_OP_MERGE: Merge scattered segments into one contiguous bufs[0]
* (synthetic split for untagged, native VLAN split
* for tagged); buf_count reduced to 1.
* - HOOK_TX_OP_SKIP: Set buf_count to 0 — driver transmit and post_tx_hook
* are skipped; tx_bufs remains caller-owned.
* - HOOK_TX_OP_ABORT: Return non-ESP_OK — transmit aborted, post_tx_hook
* not called.
* - HOOK_TX_OP_POST_TX_FREE: Replace bufs[0] with a malloc'd copy so that
* post_tx_hook can free hook-owned memory after TX.
*/
static void test_fill_prefix_magics(uint8_t *dst)
{
dst[0] = TX_PREFIX_MAGIC_0;
dst[1] = TX_PREFIX_MAGIC_1;
dst[2] = TX_PREFIX_MAGIC_2;
dst[3] = TX_PREFIX_MAGIC_3;
}
static bool test_prefix_magics_match(const uint8_t *src)
{
uint8_t expected[TX_PREFIX_LEN];
test_fill_prefix_magics(expected);
return memcmp(src, expected, TX_PREFIX_LEN) == 0;
}
static esp_err_t test_tx_hook(esp_eth_handle_t eth, esp_eth_sublayer_tx_bufs_t *tx_bufs, void *ctx)
{
hooks_ctx_t *hctx = (hooks_ctx_t *)ctx;
if (hctx->dir != HOOK_DIR_TX) {
return ESP_OK;
}
hctx->tx.hook_calls++;
esp_eth_buf_desc_t *bufs = tx_bufs->bufs;
size_t *buf_count = tx_bufs->buf_count;
size_t buf_capacity = tx_bufs->buf_capacity;
switch (hctx->tx.op) {
case HOOK_TX_OP_PREFIX: {
/* Change buf pointers/len and bump buf_count (untagged) or edit bufs[1] (tagged). */
if (*buf_count < 1 || buf_capacity < 2) {
return ESP_ERR_INVALID_SIZE;
}
if (*buf_count >= 2 && bufs[0].len == ETH_HEADER_LEN + ETH_VLAN_TAG_LEN) {
/* Tagged VLAN path: header already in bufs[0], prefix payload in bufs[1]. */
size_t payload_len = bufs[1].len;
uint8_t *prefixed = malloc(TX_PREFIX_LEN + payload_len);
if (prefixed == NULL) {
return ESP_ERR_NO_MEM;
}
test_fill_prefix_magics(prefixed);
memcpy(prefixed + TX_PREFIX_LEN, bufs[1].buf, payload_len);
bufs[1] = (esp_eth_buf_desc_t){ .buf = prefixed, .len = TX_PREFIX_LEN + payload_len };
return ESP_OK;
}
/* Untagged path: single descriptor — shrink bufs[0] to header, add prefixed bufs[1]. */
size_t payload_len = bufs[0].len - ETH_HEADER_LEN;
uint8_t *prefixed = malloc(TX_PREFIX_LEN + payload_len);
if (prefixed == NULL) {
return ESP_ERR_NO_MEM;
}
test_fill_prefix_magics(prefixed);
memcpy(prefixed + TX_PREFIX_LEN, bufs[0].buf + ETH_HEADER_LEN, payload_len);
bufs[0].len = ETH_HEADER_LEN;
bufs[1] = (esp_eth_buf_desc_t){ .buf = prefixed, .len = TX_PREFIX_LEN + payload_len };
*buf_count = 2;
return ESP_OK;
}
case HOOK_TX_OP_PREFIX_HEAD: {
/*
* Insert a standalone leading Ethernet frame at bufs[0] (any ethertype
* is fine on TX). Shift the original frame descriptors down and bump
* buf_count. RX hook strips this segment before VLAN demux / L2TAP.
*/
if (*buf_count < 1 || buf_capacity < *buf_count + 1) {
return ESP_ERR_INVALID_SIZE;
}
uint8_t *head = malloc(TX_PREFIX_HEAD_SEG_LEN);
if (head == NULL) {
return ESP_ERR_NO_MEM;
}
memset(head, 0xFF, ETH_ADDR_LEN);
esp_eth_ioctl(eth, ETH_CMD_G_MAC_ADDR, head + ETH_ADDR_LEN);
head[ETH_HEADER_LEN - sizeof(uint16_t)] = (uint8_t)(ETH_TYPE_HOOK_HEAD >> 8);
head[ETH_HEADER_LEN - sizeof(uint16_t) + 1] = (uint8_t)ETH_TYPE_HOOK_HEAD;
test_fill_prefix_magics(head + ETH_HEADER_LEN);
for (size_t i = *buf_count; i > 0; i--) {
bufs[i] = bufs[i - 1];
}
bufs[0] = (esp_eth_buf_desc_t){ .buf = head, .len = TX_PREFIX_HEAD_SEG_LEN };
*buf_count += 1;
return ESP_OK;
}
case HOOK_TX_OP_MERGE: {
/* Merge scattered segments into one malloc'd bufs[0]; reduce buf_count to 1. */
if (*buf_count == 1 && bufs[0].len > ETH_HEADER_LEN) {
bufs[1] = (esp_eth_buf_desc_t){
.buf = bufs[0].buf + ETH_HEADER_LEN,
.len = bufs[0].len - ETH_HEADER_LEN,
};
bufs[0].len = ETH_HEADER_LEN;
*buf_count = 2;
}
if (*buf_count < 2) {
return ESP_ERR_INVALID_SIZE;
}
size_t total = 0;
for (size_t i = 0; i < *buf_count; i++) {
total += bufs[i].len;
}
uint8_t *merged = malloc(total);
if (merged == NULL) {
return ESP_ERR_NO_MEM;
}
size_t off = 0;
for (size_t i = 0; i < *buf_count; i++) {
memcpy(merged + off, bufs[i].buf, bufs[i].len);
off += bufs[i].len;
}
bufs[0] = (esp_eth_buf_desc_t){ .buf = merged, .len = total };
*buf_count = 1;
return ESP_OK;
}
case HOOK_TX_OP_SKIP:
/* buf_count = 0: skip driver TX and post_tx_hook; tx_bufs stays caller-owned. */
*buf_count = 0;
return ESP_OK;
case HOOK_TX_OP_ABORT:
/* Non-ESP_OK: abort transmit for this packet; post_tx_hook not called. */
return ESP_ERR_INVALID_STATE;
case HOOK_TX_OP_POST_TX_FREE:
/* Allocate replacement buffer; post_tx_hook must free bufs[0] after TX. */
if (*buf_count >= 1) {
uint8_t *copy = malloc(bufs[0].len);
if (copy == NULL) {
return ESP_ERR_NO_MEM;
}
memcpy(copy, bufs[0].buf, bufs[0].len);
copy[ETH_HEADER_LEN + 1] ^= 0x01;
bufs[0].buf = copy;
}
return ESP_OK;
default:
return ESP_OK;
}
(void)eth;
}
static void test_post_tx_hook(esp_eth_handle_t eth, const esp_eth_sublayer_tx_bufs_t *tx_bufs, void *ctx)
{
hooks_ctx_t *hctx = (hooks_ctx_t *)ctx;
if (hctx->dir != HOOK_DIR_TX) {
return;
}
hctx->tx.post_hook_calls++;
esp_eth_buf_desc_t *bufs = tx_bufs->bufs;
size_t buf_count = *tx_bufs->buf_count;
switch (hctx->tx.op) {
case HOOK_TX_OP_PREFIX:
if (buf_count >= 2) {
hctx->tx.freed_ptr = bufs[1].buf;
free(bufs[1].buf);
}
break;
case HOOK_TX_OP_PREFIX_HEAD:
if (buf_count >= 1) {
hctx->tx.freed_ptr = bufs[0].buf;
free(bufs[0].buf);
}
break;
case HOOK_TX_OP_MERGE:
case HOOK_TX_OP_POST_TX_FREE:
if (buf_count >= 1) {
hctx->tx.freed_ptr = bufs[0].buf;
free(bufs[0].buf);
}
break;
default:
break;
}
(void)eth;
}
static esp_err_t test_rx_hook(esp_eth_handle_t eth, uint8_t **buf, uint32_t *len,
esp_eth_sublayer_rx_info_t *info, void *ctx)
{
hooks_ctx_t *hctx = (hooks_ctx_t *)ctx;
if (hctx->dir == HOOK_DIR_TX) {
/*
* TX PREFIX_HEAD companion: strip the custom leading segment on loopback
* before VLAN demux / L2TAP. Counted on the TX context because the op
* union holds tx while dir == HOOK_DIR_TX.
*/
if (*len >= TX_PREFIX_HEAD_SEG_LEN) {
uint16_t etype = (uint16_t)(((*buf)[12] << 8) | (*buf)[13]);
if (etype == ETH_TYPE_HOOK_HEAD) {
if (!test_prefix_magics_match(*buf + ETH_HEADER_LEN)) {
return ESP_ERR_INVALID_ARG;
}
hctx->tx.rx_strip_calls++;
*buf += TX_PREFIX_HEAD_SEG_LEN;
*len -= TX_PREFIX_HEAD_SEG_LEN;
return ESP_OK;
}
}
return ESP_OK;
}
if (hctx->dir != HOOK_DIR_RX) {
return ESP_OK;
}
hctx->rx.hook_calls++;
switch (hctx->rx.op) {
case HOOK_RX_OP_MODIFY:
if (*len > ETH_HEADER_LEN + 1) {
size_t marker_off = ETH_HEADER_LEN;
if (*len > ETH_HEADER_LEN + ETH_VLAN_TAG_LEN + 1 &&
((uint16_t)(((*buf)[12] << 8) | (*buf)[13]) == ETH_T_8021Q)) {
marker_off = ETH_HEADER_LEN + ETH_VLAN_TAG_LEN;
}
if (marker_off + 1 < *len && (*buf)[marker_off] == FRAME_MARKER_TX) {
(*buf)[marker_off] = FRAME_MARKER_RX;
}
}
return ESP_OK;
case HOOK_RX_OP_DROP:
*len = 0;
// rx hook took the buffer, so we need to free it
free(info->l2_buffer);
info->l2_buffer = NULL;
return ESP_OK;
case HOOK_RX_OP_ABORT:
return ESP_ERR_INVALID_STATE;
default:
return ESP_OK;
}
(void)eth;
(void)info;
}
/* ------------------------------------------------------------------ */
/* Test helpers */
/* ------------------------------------------------------------------ */
static int l2tap_open_filtered(const char *if_key, uint16_t ethertype_filter)
{
int fd = open(L2TAP_VFS_DEFAULT_PATH, O_NONBLOCK);
TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, fd, "failed to open L2TAP device");
TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, ioctl(fd, L2TAP_S_INTF_DEVICE, if_key),
"failed to bind L2TAP fd to interface");
TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, ioctl(fd, L2TAP_S_RCV_FILTER, &ethertype_filter),
"failed to set L2TAP ethertype filter");
return fd;
}
static int l2tap_open_tx(const char *if_key)
{
int fd = open(L2TAP_VFS_DEFAULT_PATH, O_NONBLOCK);
TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, fd, "failed to open L2TAP device");
TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, ioctl(fd, L2TAP_S_INTF_DEVICE, if_key),
"failed to bind L2TAP fd to interface");
return fd;
}
static void test_build_frame(hook_test_frame_t *frame, esp_eth_handle_t eth,
uint16_t ethertype, uint8_t marker)
{
memset(frame, 0, sizeof(*frame));
esp_eth_ioctl(eth, ETH_CMD_G_MAC_ADDR, frame->header.src.addr);
memcpy(frame->header.dest.addr, frame->header.src.addr, ETH_ADDR_LEN);
frame->header.type = htons(ethertype);
frame->marker = marker;
}
static void test_setup(hooks_test_ctx_t *ctx)
{
memset(ctx, 0, sizeof(*ctx));
memset(&s_hooks_ctx, 0, sizeof(s_hooks_ctx));
ctx->hook_ctx = &s_hooks_ctx;
sublayer_test_create_event_loop(&ctx->base);
sublayer_test_init_ethernet(&ctx->base);
sublayer_test_register_l2tap(&ctx->base);
sublayer_test_register_ctx(&ctx->base);
esp_eth_sublayer_config_t sub_cfg = ESP_ETH_SUBLAYER_CONFIG_DEFAULT();
sub_cfg.eth_handle = ctx->base.eth_handles[0];
sub_cfg.tx_hook = test_tx_hook;
sub_cfg.post_tx_hook = test_post_tx_hook;
sub_cfg.rx_hook = test_rx_hook;
sub_cfg.hook_ctx = ctx->hook_ctx;
TEST_ESP_OK(esp_eth_sublayer_new(&sub_cfg, &ctx->base.sub));
TEST_ESP_OK(esp_eth_sublayer_vlan_add(ctx->base.sub, ESP_ETH_SUBLAYER_UNTAGGED_VID, &ctx->vlan_untagged));
TEST_ESP_OK(esp_eth_sublayer_vlan_add(ctx->base.sub, TEST_VLAN_ID, &ctx->vlan_tagged));
esp_netif_t *netif_untagged = sublayer_test_create_eth_netif(&ctx->base, IFKEY_UNTAGGED, "eth");
esp_netif_t *netif_vlan = sublayer_test_create_eth_netif(&ctx->base, IFKEY_VLAN, "eth_vlan");
TEST_ESP_OK(esp_netif_attach(netif_untagged, ctx->vlan_untagged));
TEST_ESP_OK(esp_netif_attach(netif_vlan, ctx->vlan_tagged));
sublayer_test_start_and_wait_connect(&ctx->base);
}
static void test_teardown(hooks_test_ctx_t *ctx)
{
sublayer_test_teardown(&ctx->base);
sublayer_test_unregister_ctx();
}
static bool verify_loopback_frame(const uint8_t *rx_buf, size_t rx_len,
const hook_test_frame_t *tx_frame)
{
return rx_len >= sizeof(*tx_frame) &&
memcmp(rx_buf, tx_frame, sizeof(*tx_frame)) == 0;
}
typedef struct {
const char *if_key;
const hook_test_frame_t *frame;
} l2tap_send_arg_t;
static void l2tap_send_task(void *arg)
{
l2tap_send_arg_t *send_arg = (l2tap_send_arg_t *)arg;
vTaskDelay(pdMS_TO_TICKS(L2TAP_SEND_DELAY_MS));
int tx_fd = l2tap_open_tx(send_arg->if_key);
ssize_t n = write(tx_fd, send_arg->frame, sizeof(*send_arg->frame));
TEST_ASSERT_NOT_EQUAL(-1, n);
close(tx_fd);
vTaskDelete(NULL);
}
static bool l2tap_wait_for_frame_timeout(int fd, uint8_t *rx_buf, size_t rx_buf_size,
size_t *rx_len, int timeout_ms)
{
struct timeval tv = {
.tv_sec = timeout_ms / 1000,
.tv_usec = (timeout_ms % 1000) * 1000,
};
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
int ret = select(fd + 1, &rfds, NULL, NULL, &tv);
if (ret <= 0 || !FD_ISSET(fd, &rfds)) {
return false;
}
ssize_t n = read(fd, rx_buf, rx_buf_size);
if (n <= 0) {
return false;
}
*rx_len = (size_t)n;
return true;
}
static bool l2tap_wait_for_frame(int fd, uint8_t *rx_buf, size_t rx_buf_size, size_t *rx_len)
{
return l2tap_wait_for_frame_timeout(fd, rx_buf, rx_buf_size, rx_len, L2TAP_IO_TIMEOUT_MS);
}
static bool verify_frame_prefix(const uint8_t *rx_buf, size_t rx_len,
const hook_test_frame_t *tx_frame)
{
size_t expected_len = sizeof(*tx_frame) + TX_PREFIX_LEN;
if (rx_len < expected_len) {
return false;
}
if (memcmp(rx_buf, tx_frame, ETH_HEADER_LEN) != 0) {
return false;
}
if (!test_prefix_magics_match(rx_buf + ETH_HEADER_LEN)) {
return false;
}
return memcmp(rx_buf + ETH_HEADER_LEN + TX_PREFIX_LEN,
((const uint8_t *)tx_frame) + ETH_HEADER_LEN,
sizeof(*tx_frame) - ETH_HEADER_LEN) == 0;
}
/* Send a frame and verify the TX PREFIX_HEAD rewrite (new leading buf) is
* delivered intact to L2TAP via EMAC loopback. */
static void test_tx_prefix_head_on_interface(hooks_test_ctx_t *ctx, const char *if_key,
uint16_t ethertype)
{
hook_test_frame_t tx_frame;
test_build_frame(&tx_frame, ctx->base.eth_handles[0], ethertype, FRAME_MARKER_TX);
int rx_fd = l2tap_open_filtered(if_key, ethertype);
l2tap_send_arg_t send_arg = { .if_key = if_key, .frame = &tx_frame };
xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL);
uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN];
size_t rx_len = 0;
bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len);
bool frame_ok = got_frame && verify_loopback_frame(rx_buf, rx_len, &tx_frame);
close(rx_fd);
vTaskDelay(pdMS_TO_TICKS(50));
TEST_ASSERT_TRUE(got_frame);
TEST_ASSERT_TRUE(frame_ok);
}
/* Send a frame and verify the TX PREFIX rewrite (extra segment + ethertype
* tweak) is delivered to L2TAP via EMAC loopback. */
static void test_tx_prefix_on_interface(hooks_test_ctx_t *ctx, const char *if_key,
uint16_t ethertype)
{
hook_test_frame_t tx_frame;
test_build_frame(&tx_frame, ctx->base.eth_handles[0], ethertype, FRAME_MARKER_TX);
int rx_fd = l2tap_open_filtered(if_key, ethertype);
l2tap_send_arg_t send_arg = { .if_key = if_key, .frame = &tx_frame };
xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL);
uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN];
size_t rx_len = 0;
bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len);
bool frame_ok = got_frame && verify_frame_prefix(rx_buf, rx_len, &tx_frame);
close(rx_fd);
vTaskDelay(pdMS_TO_TICKS(50));
TEST_ASSERT_TRUE(got_frame);
TEST_ASSERT_TRUE(frame_ok);
}
/* Send a VLAN frame and verify the TX MERGE rewrite (contiguous buffer) is
* delivered to L2TAP via EMAC loopback. */
static void test_tx_merge_on_vlan(hooks_test_ctx_t *ctx)
{
hook_test_frame_t tx_frame;
test_build_frame(&tx_frame, ctx->base.eth_handles[0], ETH_TYPE_HOOK_VLAN, FRAME_MARKER_TX);
int rx_fd = l2tap_open_filtered(IFKEY_VLAN, ETH_TYPE_HOOK_VLAN);
l2tap_send_arg_t send_arg = { .if_key = IFKEY_VLAN, .frame = &tx_frame };
xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL);
uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN];
size_t rx_len = 0;
bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len);
bool frame_ok = got_frame && verify_loopback_frame(rx_buf, rx_len, &tx_frame);
close(rx_fd);
vTaskDelay(pdMS_TO_TICKS(50));
TEST_ASSERT_TRUE(got_frame);
TEST_ASSERT_TRUE(frame_ok);
}
/* Send a frame on if_key and assert L2TAP never receives it.
* Used when a TX hook skips/aborts transmit, or an RX hook drops/aborts the
* frame before delivery. expect_write_error selects whether write() itself
* must fail (TX abort) or succeed (skip / RX drop-or-abort). */
static void test_expect_no_frame_on_interface(hooks_test_ctx_t *ctx, const char *if_key,
uint16_t ethertype, bool expect_write_error)
{
hook_test_frame_t tx_frame;
test_build_frame(&tx_frame, ctx->base.eth_handles[0], ethertype, FRAME_MARKER_TX);
int rx_fd = l2tap_open_filtered(if_key, ethertype);
int tx_fd = l2tap_open_tx(if_key);
ssize_t wr = write(tx_fd, &tx_frame, sizeof(tx_frame));
close(tx_fd);
if (expect_write_error) {
TEST_ASSERT_EQUAL(-1, wr);
} else {
TEST_ASSERT_NOT_EQUAL(-1, wr);
}
uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN];
size_t rx_len = 0;
bool got_frame = l2tap_wait_for_frame_timeout(rx_fd, rx_buf, sizeof(rx_buf), &rx_len, 1500);
close(rx_fd);
vTaskDelay(pdMS_TO_TICKS(50));
TEST_ASSERT_FALSE(got_frame);
(void)ctx;
}
/* Send a frame and verify the RX hook rewrote the payload marker
* (FRAME_MARKER_TX -> FRAME_MARKER_RX) before L2TAP delivery. */
static void test_rx_modify_on_interface(hooks_test_ctx_t *ctx, const char *if_key,
uint16_t ethertype)
{
hook_test_frame_t tx_frame;
test_build_frame(&tx_frame, ctx->base.eth_handles[0], ethertype, FRAME_MARKER_TX);
int rx_fd = l2tap_open_filtered(if_key, ethertype);
l2tap_send_arg_t send_arg = { .if_key = if_key, .frame = &tx_frame };
xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL);
uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN];
size_t rx_len = 0;
bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len);
bool marker_ok = got_frame && rx_len >= ETH_HEADER_LEN + 1 &&
rx_buf[ETH_HEADER_LEN] == FRAME_MARKER_RX;
close(rx_fd);
vTaskDelay(pdMS_TO_TICKS(50));
TEST_ASSERT_TRUE(got_frame);
TEST_ASSERT_TRUE(marker_ok);
}
static void test_set_tx_op(hooks_test_ctx_t *ctx, hook_tx_op_t tx_op)
{
memset(&s_hooks_ctx, 0, sizeof(s_hooks_ctx));
s_hooks_ctx.dir = HOOK_DIR_TX;
s_hooks_ctx.tx.op = tx_op;
ctx->hook_ctx = &s_hooks_ctx;
}
static void test_set_rx_op(hooks_test_ctx_t *ctx, hook_rx_op_t rx_op)
{
memset(&s_hooks_ctx, 0, sizeof(s_hooks_ctx));
s_hooks_ctx.dir = HOOK_DIR_RX;
s_hooks_ctx.rx.op = rx_op;
ctx->hook_ctx = &s_hooks_ctx;
}
/* ------------------------------------------------------------------ */
/* Test cases */
/* ------------------------------------------------------------------ */
TEST_CASE("sublayer tx and post tx hooks", "[sublayer_hooks]")
{
hooks_test_ctx_t ctx;
test_setup(&ctx);
test_set_tx_op(&ctx, HOOK_TX_OP_PREFIX);
ESP_LOGI(TAG, "TX hook: insert prefix, move frame to bufs[1]");
test_tx_prefix_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED);
test_tx_prefix_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.post_hook_calls);
ESP_LOGI(TAG, "TX hook: insert custom leading frame + reorder; RX hook strips on loopback");
test_set_tx_op(&ctx, HOOK_TX_OP_PREFIX_HEAD);
test_tx_prefix_head_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED);
test_tx_prefix_head_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.rx_strip_calls);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.post_hook_calls);
ESP_LOGI(TAG, "TX hook: merge tagged header and payload into one buffer");
test_set_tx_op(&ctx, HOOK_TX_OP_MERGE);
test_tx_merge_on_vlan(&ctx);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.post_hook_calls);
ESP_LOGI(TAG, "TX hook: buf_count=0 takes ownership, no loopback frame");
test_set_tx_op(&ctx, HOOK_TX_OP_SKIP);
test_expect_no_frame_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED, false);
test_expect_no_frame_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN, false);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls);
TEST_ASSERT_EQUAL(0, ctx.hook_ctx->tx.post_hook_calls);
ESP_LOGI(TAG, "TX hook: non-ESP_OK aborts transmit");
test_set_tx_op(&ctx, HOOK_TX_OP_ABORT);
test_expect_no_frame_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED, true);
test_expect_no_frame_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN, true);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.hook_calls);
TEST_ASSERT_EQUAL(0, ctx.hook_ctx->tx.post_hook_calls);
ESP_LOGI(TAG, "post_tx_hook frees buffer allocated in tx hook");
test_set_tx_op(&ctx, HOOK_TX_OP_POST_TX_FREE);
hook_test_frame_t tx_frame;
test_build_frame(&tx_frame, ctx.base.eth_handles[0], ETH_TYPE_HOOK_UNTAGGED, FRAME_MARKER_TX);
int rx_fd = l2tap_open_filtered(IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED);
l2tap_send_arg_t send_arg = { .if_key = IFKEY_UNTAGGED, .frame = &tx_frame };
xTaskCreate(l2tap_send_task, "l2tap_send", 2048, &send_arg, tskIDLE_PRIORITY + 2, NULL);
uint8_t rx_buf[HOOK_TEST_RX_BUF_LEN];
size_t rx_len = 0;
bool got_frame = l2tap_wait_for_frame(rx_fd, rx_buf, sizeof(rx_buf), &rx_len);
close(rx_fd);
vTaskDelay(pdMS_TO_TICKS(50));
TEST_ASSERT_TRUE(got_frame);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->tx.post_hook_calls);
TEST_ASSERT_NOT_NULL(ctx.hook_ctx->tx.freed_ptr);
test_teardown(&ctx);
}
TEST_CASE("sublayer rx hooks", "[sublayer_hooks]")
{
hooks_test_ctx_t ctx;
test_setup(&ctx);
test_set_rx_op(&ctx, HOOK_RX_OP_MODIFY);
ESP_LOGI(TAG, "RX hook: modify payload before VLAN demux / L2TAP delivery");
test_rx_modify_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED);
test_rx_modify_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->rx.hook_calls);
ESP_LOGI(TAG, "RX hook: len=0 drops frame on untagged and tagged");
test_set_rx_op(&ctx, HOOK_RX_OP_DROP);
test_expect_no_frame_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED, false);
test_expect_no_frame_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN, false);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->rx.hook_calls);
ESP_LOGI(TAG, "RX hook: non-ESP_OK aborts processing");
test_set_rx_op(&ctx, HOOK_RX_OP_ABORT);
test_expect_no_frame_on_interface(&ctx, IFKEY_UNTAGGED, ETH_TYPE_HOOK_UNTAGGED, false);
test_expect_no_frame_on_interface(&ctx, IFKEY_VLAN, ETH_TYPE_HOOK_VLAN, false);
TEST_ASSERT_GREATER_THAN(0, ctx.hook_ctx->rx.hook_calls);
test_teardown(&ctx);
}
@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import pytest
from pytest_embedded_idf import IdfDut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.eth_ip101
@pytest.mark.parametrize('config', ['default_generic'], indirect=True)
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_sublayer_events(dut: IdfDut) -> None:
dut.run_all_single_board_cases(group='sublayer_events', timeout=120)
@pytest.mark.eth_ip101
@pytest.mark.parametrize('config', ['default_generic'], indirect=True)
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_sublayer_hooks(dut: IdfDut) -> None:
dut.run_all_single_board_cases(group='sublayer_hooks', timeout=120)
@pytest.mark.eth_ip101
@pytest.mark.parametrize('config', ['default_generic_esp32p4'], indirect=True)
@idf_parametrize('target', ['esp32p4'], indirect=['target'])
def test_sublayer_events_esp32p4(dut: IdfDut) -> None:
dut.run_all_single_board_cases(group='sublayer_events', timeout=120)
@pytest.mark.eth_ip101
@pytest.mark.parametrize('config', ['default_generic_esp32p4'], indirect=True)
@idf_parametrize('target', ['esp32p4'], indirect=['target'])
def test_sublayer_hooks_esp32p4(dut: IdfDut) -> None:
dut.run_all_single_board_cases(group='sublayer_hooks', timeout=120)
@@ -0,0 +1 @@
CONFIG_IDF_TARGET="esp32"
@@ -0,0 +1 @@
CONFIG_IDF_TARGET="esp32p4"
@@ -0,0 +1,20 @@
CONFIG_ESP_MAIN_TASK_STACK_SIZE=4096
CONFIG_UNITY_ENABLE_FIXTURE=y
CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y
CONFIG_ETH_USE_ESP32_EMAC=y
CONFIG_ESP_TASK_WDT_EN=n
# interface configuration
CONFIG_ETHERNET_INTERNAL_SUPPORT=y
CONFIG_ETHERNET_PHY_GENERIC=y
CONFIG_ETHERNET_DEFAULT_EVENT_HANDLER=n
CONFIG_ESP_NETIF_L2_TAP=y
CONFIG_ETH_TRANSMIT_MUTEX=n
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_ETH_SUBLAYER_SUPPORT=y
CONFIG_ETH_SUBLAYER_TRANSMIT_MUTEX=y
CONFIG_ETH_SUBLAYER_TX_BUF_DESC_CAPACITY=3
CONFIG_ETH_SUBLAYER_VLAN_SUPPORT=y