Files
desklock/firmware/components/esp_hosted/docs/feature_network_split.md
T
jpmschweitzerandClaude Fable 5 cb5826e02b
Test, Build and Push / test-gateway (push) Successful in 12s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Fork fix: the SDIO wedge is FIXED (esp-hosted-mcu #167)
Root cause (verified against our exact IDF tree, not the community guess):
the "258" in "sdio_write_task: Failed to send data: 258" is NOT a timeout
(that is 263). 258 = 0x102 = ESP_ERR_INVALID_ARG. On the ESP32-P4, block-
mode CMD53 writes require the SOURCE buffer to be 64-byte (cache-line)
aligned; the IDF sdmmc driver rejects a misaligned source with INVALID_ARG
BEFORE any bus activity. esp_hosts write loop then declares "Unrecoverable
host sdio state" and reboots the whole P4. The audio TX payload is not
64-aligned, so streaming mic audio wedged on the very FIRST frame (which is
exactly what we saw: listening -> instant Failed to send -> reboot).

This also explains why buffer/queue/clock/retry tuning all did nothing: the
write never reached the bus. And why our symptom was instant, not after
~100 writes (the community block-mode-desync theory) — it is the first
misaligned buffer, every time.

Fix: vendored esp_hosted 2.12.11 as an editable local component (overrides
the registry copy) and bounce a misaligned TX payload through one aligned
DMA scratch buffer in hosted_sdio_write_block (port_esp_hosted_host_sdio.c).
TX is serialized by the bus lock so a single static bounce buffer is safe;
freed in hosted_sdio_deinit. Host-only change — no C6 reflash.

VERIFIED ON HARDWARE (autonomous self-test): 40s of continuous mic-audio
upstream streaming — the traffic that previously wedged on the first frame
— ran clean, zero timeouts, zero reboots. A guarded SDIO_TX_SELFTEST harness
is kept (compiled out) for future SDIO stress testing.

Credit: root cause + patch designed via multi-agent investigation; the
precise 258=INVALID_ARG decode (correcting the upstream community timeout
assumption) came from checking our actual esp_err.h.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 09:50:27 +02:00

6.7 KiB
Raw Blame History

Network Split Feature for ESP-Hosted MCU

Supported Slave Targets ESP32-C5 ESP32-C6 ESP32-S2 ESP32-S3

Overview

Network Split allows the Host MCU and ESP32 Slave to share one IP address and split traffic between them.

It is especially useful when the host sleeps — the ESP continues handling selected network activity (e.g., MQTT, DNS).

Highlights:

  • Port-based traffic routing
  • Shared IP address
  • Packet filtering and DPI
  • Wake-on-packet support (e.g., MQTT)
  • Power management integration
  • Supported Slave Targets: ESP32-C5/C6/S2/S3 only

Configuration

On the Slave

  1. Run:

    idf.py menuconfig
    
  2. Enable:

    Example Configuration
    └── [*] Enable Network Split
    
  3. (Optional) Customize under Network Split Configuration:

    ├── Host Static Port Forwarding
    │   ├── TCP dst: 22,80,443,8080,8554
    │   └── UDP dst: 53,123
    ├── Port Ranges
    │   ├── Host: 4915261439
    │   └── Slave: 6144065535
    └── Default Destination: slave / host / both
    

On the Host

  1. Run:

    idf.py menuconfig
    
  2. Enable:

    Component config
    └── ESP-Hosted config
        └── [*] Enable Network Split
    
  3. Match port ranges with the slave:

    └── LWIP port config
        ├── Host LWIP: 4915261439
        └── Slave LWIP: 6144065535
    
  4. Integrate into your app:

    #include "esp_hosted.h"
    
    void app_main(void) {
        esp_hosted_init();
        // Host now shares IP and splits traffic with slave
    }
    

Tip

The port ranges on the host and slave must match perfectly to avoid routing issues.


Packet Routing Decisions

All incoming network packets arriving at the slave device are evaluated by the logic in nw_split_router.c. For each packet received from the WiFi router, the slave determines whether it should be processed locally, forwarded to the host, or handled by both network stacks, according to defined routing rules.

Packet Type Destination Port Condition Routed To
Broadcast, ARP Request, ICMP Request N/A Slave Network Stack
ARP Response, ICMP Response N/A Both Network Stacks
DHCP Any Both Network Stacks
TCP/UDP Listed in Static Port Forwarding Host Network Stack
TCP/UDP Within Host Port Range Host Network Stack
TCP/UDP Within Slave Port Range Slave Network Stack
TCP/UDP Port 5001 (iperf) Both Network Stacks
MQTT (Port 1883) Payload contains "wakeup-host" Host Network Stack (Wake-up)
Others Not matched by any rule Default Destination (as configured)
Packet destined for Host Network Stack Host is in deep sleep Dropped (unless wake-up packet)

Tip

The packet routing logic is fully customizable within the nw_split_router.c file, allowing users to adapt the network management behavior to their specific requirements.

The following diagrams illustrate the decision-making process for packet routing in the Network Split feature.

flowchart TD
    A[New Packet] --> B{MAC Broadcast?}
    B -->|Yes| C[Send to<br/>Slave Network Stack]
    B -->|No| D{IP Packet?}
    D -->|Yes| E[Protocol Processing]
    D -->|No| F[ARP Processing]

    E --> G{TCP?}
    E --> H{UDP?}
    E --> I{ICMP?}

    classDef slaveNode fill:#d9f7be,stroke:#389e0d
    classDef hostNode fill:#d6e4ff,stroke:#1d39c4
    classDef invalidNode fill:#ffccc7,stroke:#cf1322
    classDef bothNode fill:#fff1b8,stroke:#d4b106

    class C slaveNode

TCP/UDP Packet Processing

Specific case of UDP/TCP packet processing is illustrated below

flowchart TD
    G[TCP/UDP Packet] --> G1{Port config in <br/>Host Static Forwarding List?<br/>}
    G1 -->|Yes| K[Send to<br/>Host Network Stack]

    G1 -->|No| G2{Dest Port in<br/>Slave Range?}
    G2 -->|Yes| C[Send to<br/>Slave Network Stack]

    G2 -->|No| G3{Dest Port in<br/>Host Range?}
    G3 -->|Yes| G4{Host Awake?}
    G4 -->|Yes| K
    G4 -->|No| G5{MQTT with<br/>wakeup signal?}
    G5 -->|Yes| K
    G5 -->|No| L[Drop Packet]

    G3 -->|No| D[Send to<br/>Default Network Stack<br/>from config]

    classDef slaveNode fill:#d9f7be,stroke:#389e0d
    classDef hostNode fill:#d6e4ff,stroke:#1d39c4
    classDef invalidNode fill:#ffccc7,stroke:#cf1322
    classDef defaultNode fill:#fff,stroke:#333

    class K hostNode
    class C slaveNode
    class L invalidNode
    class D defaultNode

Internals

Core Routing API

// nw_split_router.c
hosted_l2_bridge nw_split_filter_and_route_packet(void *frame_data, uint16_t frame_length);

typedef enum {
    SLAVE_LWIP_BRIDGE = 0,
    HOST_LWIP_BRIDGE  = 1,
    BOTH_LWIP_BRIDGE  = 2,
    INVALID_BRIDGE    = 3
} hosted_l2_bridge;

Wake-Up on MQTT

static bool host_mqtt_wakeup_triggered(const void *payload, uint16_t length) {
    return memcmp(payload, "wakeup-host", strlen("wakeup-host")) == 0;
}

Extras

Use Same Port on Both Sides?

Yes — port 5001 (iperf) is a working example. Others can be added similarly in nw_split_router.c.


Files of Interest

Purpose File
Routing logic slave/main/nw_split_router.c
Host-side config host/api/include/esp_hosted_config.h
Slave control slave/main/interface.h
API entry point esp_hosted_init()

Debugging & Tips

Checklist

  • Port ranges must not overlap
  • Logs from nw_split_router.c help debug routing
  • Static forwarding can override port ranges
  • Only supported on ESP32-C5/C6/S2/S3

Debug Logging

Enable verbose logs in nw_split_router.c:

esp_log_level_set("nw_split_router", ESP_LOG_VERBOSE);

Wi-Fi sniffer to inspect raw traffic (if really needed)