Contents Menu Expand Light mode Dark mode Auto light/dark, in light mode Auto light/dark, in dark mode Skip to content
libtracer
libtracer

Getting started

  • Getting started
  • Examples
    • In-process pub/sub
    • Pub/sub fan-out & dispatch cost
    • Wire codec round-trip
    • Wire codec deep-dive & throughput
    • Rope scatter-gather
    • Two nodes over a wire — FWD delivery
    • Composition axes
    • Register a vertex, and address it
    • Read and write
    • await
    • Creation
    • :children[]
    • Retirement
    • A HANDLER vertex
    • A STREAM vertex
    • Subscribe to one vertex
    • One edge, a whole subtree
    • Delivery terminates at the target
    • The delivery policy is per subscription
    • Unsubscribe & the release hook
    • Unsubscribing from inside a delivery
    • Retire drops a producer's subscriptions
    • The TLV header and the opt byte
    • Structured or opaque — one bit decides
    • A PATH body is packed segment records
    • The escape record
    • The trailer: CRC and timestamp
    • What the frame reader refuses
    • decode_into: a flat arena
    • tlv_view_t: a scattered frame
    • The segment, and its refcount
    • subview
    • borrow: the app's own bytes
    • The rope
    • subrope and the iovec egress
    • A bounded backend
    • A DEVICE link
    • A shared seam needs a thread-safe backend
    • The failable block seam
    • Two L0 seams, and the question that picks
    • A bump source
    • The upstream decides what the buffer means
    • A long-lived seam has to recycle
    • Classes do not share
    • A container that fails by value
    • The std::pmr adapter
    • Open by default, and the first ACE is the lock
    • The caller context is not the subject
    • access_mask is a bitfield
    • EVERYONE@ is reserved both ways
    • Effective ACL = own + inherited
    • expires_ns is checked against your clock
    • Two evaluators, and DENY
    • An ACL is a security document
    • The dst is a source route
    • Terminus or forward — one test decides
    • One NAME, one slot
    • A mount run is consumed whole
    • Three nodes, and a forwarder that stores nothing
    • The src you accumulated is the way home
    • A repeating flow buys its route back
    • A stale label is dropped and NACK'd
    • One seam, every wire technology
    • A kind is a NAME, resolved twice
    • DIAL and LISTEN are two constructors
    • A datagram already has boundaries
    • A stream has none, so the kind supplies them
    • No frame crosses until the Upgrade completes
    • The one BUS kind
    • One listener, many slots
    • Rust: a VALUE frame and its CRC trailer
    • Rust: an address is packed segment records
    • Rust: a remote write is one FWD frame
    • Rust: subscribing is a write
    • Rust: reading a node's :stats
    • TypeScript: a VALUE frame and its CRC trailer
    • TypeScript: write a remote vertex, then read it
    • TypeScript: subscribe to a remote producer
    • TypeScript: a remote failure is a typed error
    • TypeScript: dial a WebSocket link
    • ESP-IDF: the component, and nothing else
    • ESP-IDF: sizing the arena
    • ESP-IDF: one task, no pool locks
    • ESP-IDF: one link, and its max_frame
    • ESP-IDF: reading the sub-pools' :stats

Specification

  • The specification
    • Protocol v1 — the wire format

RFCs and decisions

  • ADR and RFC index
    • RFC 0001 — Protocol-v1 wire-format consistency consolidation
    • RFC 0002 — Protocol error model: the tr:: concept namespace
    • RFC 0003 — Concrete-path delivery for bridged wildcard subscriptions
    • RFC 0004 — Remote operation addressing: path-as-route + the FWD/FIELD frames
    • RFC 0005 — Subtree subscriptions: vertical bubbling, branch-write decomposition, write-creates
    • RFC 0006 — Nesting depth is receiver-resource-bounded: the fixed cap of 32 is removed
    • RFC 0007 — SUBSCRIBER delivery terminates at the target: no automatic re-dispatch to the target’s subscribers
    • RFC 0008 — Vertex operations: assign and propagate; structural selective propagation; value-agnostic per-vertex delivery_mode
    • RFC 0009 — Vertex removal and subscriber eviction
    • RFC 0010 — Owner-writable application property fields: the field descriptor table, the reserved settings.app namespace, and owner-defined :schema
    • RFC 0011 — Node identity facet: a wire-readable, pre-auth :identity field serving the ADR-0045 ed25519 TOFU public key at every vertex
    • RFC 0013 — Readable creatable-child-type catalog: the :children.schema read
    • RFC 0014 — Creator endpoint: connection lifecycle and link liveness
    • RFC 0016 — Composed branch read: a plain READ of a branch serves the folded POINT tree of its registered subtree
    • RFC 0017 — Element addressing: [n] on the value plane, and per-element delivery
    • RFC 0018 — Packed path segments: a PATH body becomes length-prefixed records
    • RFC 0019 — Path depth is bounded by bytes: the 32-segment PATH cap is deleted
    • RFC 0020 — A bus link’s connection NAME is not a routable next-hop (reject, never broadcast, on the request plane)
    • RFC 0021 — The frame of reference of a wire SUBSCRIBER’s PATH target
    • RFC 0022 — Delivery policy is per-subscription; settings_t dissolves
    • RFC 0023 — The path segment cap is repriced: 32 → 255, derived from the wire’s own widths
    • RFC 0024 — Bound paths: node-scoped vertex-ref source routing
    • RFC 0025 — Stream-class values: delivery classes over the rope primitive
    • RFC 0026 — The ACE access_mask canonical wire width is u32
    • RFC 0027 — Label-switched path compression: minting a per-host path label across the wire
    • RFC 0028 — The lean value path: one block per publish, copy-or-share by size, retention per vertex, sync as a trait
    • RFC 0029 — One path primitive: the owner-issued (index, generation) pair, carried per hop, local = forwarded
    • RFC 0030 — The host API walks the graph: a graph-owned path object, creation refused by default, the reply as a remote write, AWAIT and REPLY retired
    • RFC 0031 — Bus-session anchors are child vertices under their door: one walk and one gate reach a session, send-through is directed

Reference

  • Reference (descriptive)
    • Overview — the six-layer model
    • Module catalog & composition
    • Deployment profiles
    • Concurrency & scaling
    • Reclamation policy
    • Memory substrate
    • Views & ownership
    • Data format
    • Protocol-defined TLVs
    • Graph model
    • Addressing
    • Communication flows
    • User data packing
    • Vertex roles & aggregation
    • Host embedding
    • Network formation
    • CAN transport
    • WebSocket session authentication
    • Composition over the network
    • Transports are vertices
    • Bindings map
    • ROS 2 integration (rmw_tracer)
    • Backpressure & sizing
  • Design notes
    • Concurrency & scaling
      • Scaling and serialization
      • Write and delivery path
    • Zero-copy and flatten
    • Build configuration
      • The configuration space
    • Failable allocation and backpressure

C++ API reference

  • C++ API reference
    • Interface map
    • File map — header to page
    • status & errors — result taxonomy
    • config — the build's traits type
    • instrumentation — reachability counters
    • segment — refcounted bytes
    • backends — allocators
    • views — view_t / rope_t / cast
    • frame-codec — TLV codec + CRC
    • Wire format, bit by bit
    • path — addressing
    • graph — vertices & dispatch
    • security & ACL — access control
    • fwd-router — FWD routing and the /net plane
    • transport — the wire
    • connection config — the SPEC config keys
    • can — the header-elided CAN stack

Interoperate

  • Interoperability
  • Build a custom device
  • A production ESP32 node
  • Capability matrix
  • Implementation registry

Evidence

  • Performance & conformance
  • Test report

Glossary

  • Context glossary

Start here

  • Route by intent
Back to top

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_frame and its rx backend’s slot. The default rx backend has no slot bound, so without max_frame the cap is the datagram limit, 64 KiB, twice the default arena: those draws are refused and every datagram is dropped, counted in the link’s dropped_rx. The app sets max_frame to 1 KiB and reads tr::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 (the full_node example does this for its listener).

  • Only the UDP transport is compiled. sdkconfig.defaults turns 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. The linux target 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.

Next
ESP-IDF: reading the sub-pools’ :stats
Previous
ESP-IDF: one task, no pool locks
Copyright © 2026, avatarsd LLC
Made with Sphinx and @pradyunsg's Furo
On this page
  • ESP-IDF: one link, and its max_frame
    • What to notice
    • Source
    • Build and run