ESP-IDF: one link, and its max_frame¶
A link is created the production way: the app declares a udp-server module on its
transport_vertex_t, then writes a connection SPEC to the module’s creator endpoint,
/net/udp-server/conn. Left on the default sources, the link draws its receive blocks from the
arena’s net sub-pool at its inbound frame cap. That makes max_frame a sizing decision on an
MCU, not a tuning knob.
What to notice¶
The cap decides the draw. A UDP link’s receive buffers are one byte past the smaller of its SPEC’s
max_frameand its rx backend’s slot. The default rx backend has no slot bound, so withoutmax_framethe cap is the datagram limit, 64 KiB, twice the default arena: those draws are refused and every datagram is dropped, counted in the link’sdropped_rx. The app setsmax_frameto 1 KiB and readstr::mem::net_source()’s census before and after the link comes up; the difference is what the link drew.Nothing is injected.
transport_vertex_t net(g, router)takes its receive backend and egress source from the net sub-pool by default. A node that wants its links on a separate, fixed pool passes its own, and that pool’s slot then bounds the buffers too (thefull_nodeexample does this for its listener).Only the UDP transport is compiled.
sdkconfig.defaultsturns TCP and WebSocket off.lwIP is started first on a chip.
esp_netif_init()brings up the TCP/IP task the link’s socket needs. Thelinuxtarget has host sockets and skips it.Board-only, named and not run: a peer reaches the link only after the board joins a network (Wi-Fi or Ethernet bring-up, which is the application’s). From a host on that network, dial
kind=udp,addr=<board IP>,port=47301.
Source¶
1/*
2 * SPDX-License-Identifier: Apache-2.0
3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
4 */
5
6/**
7 * @file
8 * @brief ONE CONCEPT — one link, created in-band, draws its receive blocks from the arena's net
9 * sub-pool, so its `max_frame` is a sizing decision.
10 *
11 * A link is created the production way: the app declares a `udp-server` module on the
12 * `transport_vertex_t`, then writes a connection SPEC to its creator endpoint
13 * `/net/udp-server/conn`. Left on the default sources, the UDP link draws its receive scratch
14 * and each receive segment from the net sub-pool at its frame cap. That cap is the datagram
15 * limit (64 KiB) unless the SPEC sets `max_frame`, and 64 KiB is twice the default arena: on
16 * an MCU, `max_frame` is how a link is made to fit. The app sets it to 1 KiB and reads
17 * the net sub-pool's census before and after the link comes up.
18 *
19 * Board-only step, not run here: a peer can reach the link only once the board has joined a
20 * network (Wi-Fi or Ethernet bring-up, which is the application's and not libtracer's). From a
21 * host on that network, dial `kind=udp`, `addr=<board IP>`, `port=47301`.
22 */
23
24#include <cstdint>
25#include <cstdio>
26#include <cstdlib>
27
28#include "libtracer/conn_spec.hpp"
29#include "libtracer/fwd_router.hpp"
30#include "libtracer/tracer.hpp"
31#include "libtracer/transport_udp.hpp"
32#include "libtracer/transport_vertex.hpp"
33#include "sdkconfig.h"
34#if !CONFIG_IDF_TARGET_LINUX
35#include "esp_netif.h"
36#endif
37
38namespace {
39
40using tr::graph::path_t;
41
42/** @brief The port the link listens on. */
43constexpr std::uint16_t kPort = 47301;
44
45/** @brief The inbound frame cap the SPEC sets: every receive block is this size. */
46constexpr std::uint32_t kMaxFrame = 1024;
47
48/** @brief Failed checks so far. */
49int g_failures = 0;
50
51/** @brief Print @p what with its verdict and count a failure. */
52void check(bool ok, const char* what) {
53 std::printf(" [%s] %s\n", ok ? "ok" : "FAIL", what);
54 if (!ok) ++g_failures;
55}
56
57/** @brief Print the verdict; on the `linux` target also exit with it, so CI can run this. */
58void finish() {
59 std::printf("RESULT %s\n", g_failures == 0 ? "ok" : "FAIL");
60#if CONFIG_IDF_TARGET_LINUX
61 std::exit(g_failures == 0 ? 0 : 1);
62#endif
63}
64
65} // namespace
66
67extern "C" void app_main(void) {
68#if !CONFIG_IDF_TARGET_LINUX
69 ESP_ERROR_CHECK(esp_netif_init()); // starts lwIP; the link's socket needs it
70#endif
71 tr::graph::graph_t g;
72 tr::net::fwd_router_t router(g);
73 tr::net::transport_vertex_t net(g, router); // receive and egress on the net sub-pool
74 check(
75 net.register_module(tr::net::kUdpServerSuggestedModule, "udp", tr::net::conn_role_t::LISTEN)
76 .has_value(),
77 "declare the udp-server module");
78
79 tr::mem::block_source_t& pool = tr::mem::net_source();
80 const std::size_t before = pool.stats().in_use;
81
82 tr::net::conn_spec_t spec("host");
83 spec.kind("udp").port(kPort).max_frame(kMaxFrame);
84 check(g.write(path_t("/net/udp-server/conn"), spec.view()).has_value(),
85 "write the SPEC: one udp link, max_frame 1 KiB");
86 check(g.find(path_t("/net/udp-server/host").key()).has_value(),
87 "the link is a vertex at /net/udp-server/host");
88
89 const tr::mem::source_stats_t s = pool.stats();
90 std::printf("net sub-pool: %zu bytes in use before the link, %zu after (max_frame %u)\n",
91 before, s.in_use, static_cast<unsigned>(kMaxFrame));
92 check(s.in_use > before && s.in_use - before < 4 * kMaxFrame,
93 "the link drew its receive blocks from the net sub-pool, sized by max_frame");
94 finish();
95}
Build and run¶
$ cd integrations/esp-idf/examples/concepts/one_link
$ idf.py set-target esp32c6 build
$ idf.py flash monitor # board-only
CI builds it for esp32c6, and also builds and runs it on the ESP-IDF linux target, where the
link binds a host UDP port. See also: a datagram already has boundaries ·
DIAL and LISTEN are two constructors.