Skip to content
Basic

UDP

Connects the host to a device on a network over UDP: low overhead and no connection state, for telemetry-heavy devices, where a lost stream event now and then is better than a stall while TCP retransmits.

Host side

Enable the udp feature of wire_weaver_client and select the device by address:

let blinky = Blinky::config(|c| c.udp_addr("192.168.1.10:9000"))
    .connect()
    .await?;

Host names are resolved when connecting (my-device.local:9000), the first address is used. IPv6 works too ([fe80::1%2]:9000).

Everything above the medium is the same as with USB or WebSocket: link setup, version check, introspection, pings, timeouts, streams. There is no discovery, so device filters (serial, label, ...) are not checked before connecting.

The host socket is connected to the device address: datagrams from anyone else are dropped by the OS, and if nothing listens on the device port, the ICMP error fails the connection right away (on Linux and macOS) instead of after the link setup retries.

Device side

The udp feature of ww_device is the device end: ww_device::udp::UdpSink / UdpSource send and receive datagrams on any DatagramSocket (receive from anyone, send to an address), and FramedTx / FramedRx do the framing on top, as over USB. They run the same Server loop. The embassy-net feature adds EmbassyNetUdpSocket on an embassy-net UDP socket, and udp_server() puts it all together:

let socket = UdpSocket::new(stack, rx_meta, rx_buf, tx_meta, tx_buf);
let socket = EmbassyNetUdpSocket::bind(socket, 9000).unwrap();
let conn = UDP_CONNECTION.init(UdpConnection::new(socket));
let mut server = udp_server(link_config, conn, EmbassyClock, UDP_BUFFERS.init(UdpBuffers::new()));
server.run(&mut state).await;

UDP has no connections, so the device serves one host (peer) at a time and replies to it only:

  • a host is adopted when it starts link setup: the first message of its datagram is Nop or GetDeviceInfo, which the host sends alone before anything else;
  • datagrams from other hosts are dropped, unless they start link setup: then the new host replaces the current one, as if another process opened a USB device. The device reports LinkEvent::Down(DownReason::Transport), starts over and handles the datagram that started the takeover, so the new host connects without waiting for a retry;
  • a host that is gone is noticed by the peer timeout, there is nothing to close.

There is no authentication, anyone who can reach the port can take over. Put the device on a trusted network, or use a medium with access control.

  • No allocation: UdpBuffers<MAX_MESSAGE_LEN> holds the reassembly, tx datagram and scratch buffers (3 * MAX_MESSAGE_LEN + 2 * UDP_MAX_DATAGRAM_LEN bytes). Datagrams are received straight into the framer.
  • Device datagrams are UDP_MAX_DATAGRAM_LEN at most too. The socket's own buffers must fit at least one such datagram each way.
  • server.wait() is cancel-safe, so it can be selected on together with anything else.
  • EmbassyNetUdpSocket gives up on a send after 1 s (set_send_timeout), e.g., when the network interface is down, so the link goes down instead of blocking the event loop.
  • The medium is not Send (the peer is kept in a Cell), as with the other embassy based media; on tokio, run it on a LocalSet. wire_weaver_client/src/udp.rs has a device on tokio::net::UdpSocket in its tests.

examples_mcu/rp2 has a complete example, ww_udp_ncm: the board shows up on the host as a USB CDC-NCM Ethernet adapter, runs embassy-net with a static address and a small DHCP server, so the host is configured automatically, and serves the blinky API at 192.168.7.1:9000. Run it with just run rp2 ww_udp_ncm from examples_mcu/rp2, then blink from the host with cargo run -p blinky --features udp --example blinky_udp.

Reliability

Nothing is retransmitted:

  • a lost request (or its reply) times out, as a request to a busy device would;
  • a lost stream event is gone;
  • every datagram starts with a frame counter: a duplicated datagram, one that comes late (after a later one), or one from the previous session is dropped, a gap is counted as lost (ww stats, ww monitor);
  • a message split across datagrams is dropped if any piece of it is lost, the framer detects it from the frame counter, the remaining length and the CRC, and continues with the next one;
  • every message carries a CRC-32: a datagram corrupted on the way (the UDP checksum is a 16-bit sum, and optional over IPv4) is dropped instead of decoded into wrong values;
  • link setup survives losses, GetDeviceInfo is retried;
  • if the device goes away silently, the peer timeout ends the session.

Keep messages that must not be lost small enough to fit in one datagram, or use WebSocket if the device can run TCP.

Wire format

Each datagram is one ww_framer frame, with ww_link::UdpFraming: a one-byte frame prefix (UdpPrefix, SeqPrefix: frame counter mod 16 and session epoch mod 4), then the messages with UdpHead (U2Head) heads and UdpChecksum, a CRC-32/ISO-HDLC (the CRC-32 of zip and Ethernet, computed by ww_block) after every message, covering the prefix, the head and the message, and UdpTail (none). Same as over USB, small messages accumulated by the event loop share a datagram and big ones are split across several. Byte examples are on framer wire format.

The device starts a new session of its framer on every GetDeviceInfo (the epoch goes up), and the host drops everything before the DeviceInfo with its setup token. A datagram only starts link setup (and takes the device over from another host) when its CRC is good.

The host sends datagrams of at most ww_link::UDP_MAX_DATAGRAM_LEN (1452 B: Ethernet MTU minus IPv6 and UDP headers, so that nothing is fragmented over IPv4 or IPv6), the device must be able to receive datagrams of this size. The host accepts datagrams of any size, the device picks its own frame size.