counter_stream

Example of the component embedblocks/udp_link v0.1.0
# udp_link counter stream

A bring-up and test app for the udp_link component. Each endpoint sends an incrementing counter to the other and checks everything it receives. It needs no peripheral, so it is the first thing to run on a new board, a new ESP-IDF release or a new configuration.

Each endpoint runs one TX task and one RX task on the **same** udp_link instance, so both directions run at once. That also exercises udp_link's full-duplex guarantee.

## Modes

| Mode | Boards | Network | Use it for |
| --- | --- | --- | --- |
| **Loopback** (default) | 1 | two instances talk via `127.0.0.1` | first bring-up; a strict PASS/FAIL check of udp_link |
| **Two boards** | 2 | Wi-Fi station on the same access point | real link behavior: loss, reboots, disconnects |

## Requirements

- ESP-IDF **6.0.x**. udp_link's build checks and its manifest reject other releases.
- The udp_link component folder must be named `udp_link`, because ESP-IDF names a component after its folder. The example finds it two levels up (`../..`, set in this folder's `CMakeLists.txt`).
- Two-board mode only: two boards and a 2.4 GHz access point. Both board addresses must stay fixed for the whole run, through static IPs or DHCP reservations.

`sdkconfig.defaults` already sets what udp_link needs:

```
CONFIG_LWIP_CHECKSUM_CHECK_UDP=y   # required; the build fails without it
CONFIG_LWIP_NETIF_LOOPBACK=y       # loopback mode
CONFIG_VFS_SUPPORT_SELECT=y        # udp_link's default receive wait
CONFIG_FREERTOS_HZ=1000
```

If you built this project before adding those defaults, run `idf.py fullclean` so they take effect.

## Run: loopback (one board)

```sh
cd examples/counter_stream
idf.py set-target esp32s3          # your chip
idf.py build flash monitor
```

Every second the app prints both endpoints' counters and a verdict:

```
I (5012) udp_link_counter: A tx ok=500 drop=0 err=0 | rx ok=500 gap=0 late=0 bad=0 rej=0 restart=0 nomem=0
I (5012) udp_link_counter: B tx ok=500 drop=0 err=0 | rx ok=499 gap=0 late=0 bad=0 rej=0 restart=0 nomem=0
I (5012) udp_link_counter: report 5: PASS (inflight A->B 1, B->A 0; failed reports so far 0)
```

**Pass criterion:** every report says `PASS`, and `failed reports so far` stays at 0. Leave it running for several minutes, since a soak catches what a short run misses.

## Run: two boards

1. On each board, run `idf.py menuconfig` and set:
   - **udp_link counter stream**: untick *Loopback mode*, then set *Peer board IPv4 address* to the **other** board's address. Leave the port the same on both boards.
   - **Example Connection Configuration**: your Wi-Fi SSID and password.
2. Build and flash each board with its own configuration:

   ```sh
   idf.py -B build_a build flash monitor -p PORT_OF_BOARD_A     # peer IP = board B
   idf.py -B build_b build flash monitor -p PORT_OF_BOARD_B     # peer IP = board A
   ```

   Using separate build folders (`-B`) keeps the two configurations apart. You can also reconfigure and rebuild between flashes.

Each board prints its own counters. At startup it also prints the Wi-Fi power-save mode actually in effect: the app asks for `none`, but the driver may refuse, for example when Bluetooth is enabled too. In two-board mode there is no automatic PASS/FAIL, because one board cannot see the other's counters. Read the values against the table below.

## Reading the counters

| Counter | Meaning | Loopback | Two boards |
| --- | --- | --- | --- |
| `tx ok` | sends the stack accepted | +100 per second at the default 10 ms period | same |
| `tx drop` | `udp_link_send` returned `NO_MEM` or `NO_ROUTE`; the same counter is resent next period | 0 | nonzero while the link is down or overloaded |
| `tx err` | any other send result | **0** | **0**, including across Wi-Fi disconnects |
| `rx ok` | datagrams received with an intact payload whose counter equals `seq` | at most the peer's `tx ok` + 1 | slightly below the peer's `tx ok` |
| `rx gap` | sequence numbers skipped (lost, or still to arrive late) | 0 | small; grows with radio loss or a starved RX task |
| `rx late` | datagrams older than expected | 0 | rare |
| `rx bad` | payload pattern broken, or counter ≠ `seq` | **0** | **0** |
| `rx rej` | `udp_link_recv` rejected the datagram (malformed, wrong version or stream, truncated, other socket error) | **0** | **0** |
| `rx restart` | the sender's session changed | 0 | +1 each time the peer reboots |
| `rx nomem` | the receive wait ran out of memory; nothing was lost, and the task called again | 0 | 0 unless the heap is exhausted |

The counters in **bold** must be 0 in every mode. A nonzero value is a defect, in udp_link or in the configuration. In particular, a `tx err` that appears only while Wi-Fi drops or reconnects means the port reported an errno that udp_link's error mapping doesn't cover.

### What the loopback verdict checks

A report is `FAIL` if any of these happen:

- a bold counter is nonzero;
- `drop`, `gap`, `late` or `restart` is nonzero, since loopback has no radio and should lose nothing;
- in either direction, the receiver's `rx ok` exceeds the sender's `tx ok` + 1;
- an endpoint made more or fewer send attempts than one per period, with a tolerance of ±1 per report.

The receiver can briefly be one ahead because the sender counts a datagram only *after* `udp_link_send` returns. The higher-priority RX task can receive and count it first. The reporter reads each direction in the order sender, receiver, sender, so the `+1` bound is exact.

`inflight` is how many datagrams were sent but not yet counted at the moment of the report. It is shown for information only. It is normally 0 to 2; a value that keeps growing means the RX side is falling behind.

## Quick checks in two-board mode

- **Reboot one board.** The other board's `restart` goes up by 1, and its `gap` does not jump.
- **Switch the access point off and on.** Each sender's `drop` rises while the link is down and stops rising once it is back. `tx err` stays 0, and both directions carry data again **without** reopening anything.
- **Move a board far from the access point.** `gap` rises, while `bad` and `rej` stay 0.

## Stress

In menuconfig, set *Send period* to 1 ms (one tick at 1000 Hz) and *Payload length* towards 1460. Then:

- `tx drop` shows where the stack starts refusing datagrams;
- `rx gap` shows where the socket's receive mailbox overflows (`CONFIG_LWIP_UDP_RECVMBOX_SIZE`).

In loopback, nonzero values here mean the verdict will report `FAIL`. That is expected under stress, so read the counters rather than the verdict.

Each task keeps a payload-sized buffer on its stack, and the stack size is set to 4096 + payload length. After raising the payload length, check the tasks' high-water marks with `uxTaskGetStackHighWaterMark()`.

## Comparing udp_link's two receive waits

udp_link waits for data with `select()` by default. To build the `SO_RCVTIMEO` variant instead:

```sh
idf.py -DUDP_LINK_WAIT_SO_RCVTIMEO=1 fullclean build flash monitor
```

Run both builds for the same duration and compare their counters, CPU load and free heap. Only one internal function in `udp_link.c` differs between the two builds.

## Troubleshooting

| Symptom | Cause |
| --- | --- |
| `#error "udp_link: verified on ESP-IDF 6.0.x only"` | Wrong ESP-IDF release. |
| `#error "udp_link: requires UDP checksums..."` | `CONFIG_LWIP_CHECKSUM_CHECK_UDP` is off. Run `idf.py fullclean`, or enable it in menuconfig (Component config → LWIP → Checksums). |
| `Failed to resolve component 'udp_link'` | The component folder is not named `udp_link`, or it is not at `../..` relative to this folder. |
| `udp_link_open` fails at startup (`ESP_ERROR_CHECK` abort) | `ESP_ERR_INVALID_STATE`: the port is already in use. `ESP_ERR_NO_MEM`: out of sockets (`CONFIG_LWIP_MAX_SOCKETS`) or heap. |
| Two boards: `tx ok` rises but the peer's `rx ok` stays 0 | Peer IP or port mismatch, a changed DHCP address, AP client isolation, or a `max_payload` too large for the path. None of these produce an error on either side. |
| Two boards: bursty `gap` | Wi-Fi power save is active; check the mode logged at startup. |

## Files

| File | What |
| --- | --- |
| `main/counter_stream.c` | the app: TX and RX tasks, payload pattern, reporter |
| `main/Kconfig.projbuild` | menuconfig options (mode, peer, port, payload, period) |
| `sdkconfig.defaults` | settings udp_link and loopback mode need |
| `CMakeLists.txt` | finds udp_link at `../..` and Wi-Fi bring-up in `protocol_examples_common` |

To create a project from this example, run:

idf.py create-project-from-example "embedblocks/udp_link=0.1.0:counter_stream"

or download archive (~9.08 KB)