Files
coapwithudp/README.md

9.3 KiB
Raw Blame History

CoAP Hello (UDP) — Rust MVP

A minimal CoAP server over UDP in Rust, built on coap-lite (the RFC 7252 binary codec) and a tokio UdpSocket.

CoAP is "HTTP semantics on a UDP datagram" — the same methods, paths and query strings, but each exchange is one compact binary datagram. Response codes are written class.detail: 2.05 Content, 2.01 Created, 4.00 Bad Request, 4.04 Not Found, 4.05 Method Not Allowed.

coap-lite does message encoding only — the socket, the read loop and the routing are all in src/main.rs, so nothing about the UDP layer is hidden.

Run

cargo run --release

Listens on udp://0.0.0.0:5683 (CoAP's IANA-assigned port).

Routes

Method Path Response
GET /hello 2.05 Hello, World!
GET /hello?name=Imran 2.05 Hello, Imran!
POST /hello 2.01 Hello, <payload>!
GET /health 2.05 {"status":"ok","uptime":12}

POST /hello takes either a raw text payload (Imran) or JSON ({"name":"Imran"}). An empty payload returns 4.00. An unknown path returns 4.04; a known path with the wrong method returns 4.05.

Check it in a browser

Browsers speak HTTP over TCP; this server speaks CoAP over UDP, so coap:// URLs cannot be opened directly. src/bin/bridge.rs is a small HTTP -> CoAP proxy that closes the gap. Run both:

cargo run --release --bin coap-hello-udp
cargo run --release --bin bridge

Every response shows the CoAP code mapped onto an HTTP status, with the raw CoAP code returned verbatim in an X-Coap-Code header.

These are now ordinary URLs — clickable, curl-able, bookmarkable:

URL Response
http://localhost:8080/ the CoAP reference page
http://localhost:8080/hello Hello, World!
http://localhost:8080/hello?name=Imran Hello, Imran!
http://localhost:8080/health {"status":"ok","uptime":12}

POST needs a body, so use curl:

curl -X POST -d "Imran" http://localhost:8080/hello

The bridge is a development convenience: a deliberately small HTTP/1.1 subset with no keep-alive, no chunked bodies and no TLS. Do not expose it. Set HTTP_PORT if 8080 is taken.

Try it from the CLI

With the bundled client:

cargo run --bin client -- get "/hello?name=Imran"
cargo run --bin client -- post /hello "Imran"

Or with any other CoAP client, e.g. coap-cli:

coap get "coap://localhost:5683/hello?name=Imran"

Streaming media

The server also streams two static files, loaded into memory at startup from MEDIA_DIR (default media/):

Route File Format
GET /audio media/audio.wav PCM WAV, mono, 16-bit, 16 kHz
GET /image media/image.rgb565 raw RGB565 little-endian, 320×240 (153,600 B)

A file that is missing or in the wrong format is reported at startup and its route answers 4.04 with the reason; the rest of the server still runs.

Neither file fits in one datagram, so they go out with block-wise transfer (Block2, RFC 7959): the client asks for block 0, 1, 2 … and each reply carries 1024 bytes. The server keeps no per-client state — every request names the block it wants, so it just slices the in-memory file. Every block carries an ETag (so the client can tell the file did not change mid-transfer), block 0 carries Size2 (the total size), and a block past the end gets 4.02.

Make test media

mkdir -p media
ffmpeg -f lavfi -i "sine=frequency=440:duration=10" -ac 1 -ar 16000 -c:a pcm_s16le media/audio.wav
ffmpeg -f lavfi -i "testsrc=size=320x240" -frames:v 1 -f rawvideo -pix_fmt rgb565le media/image.rgb565

Or convert your own files:

ffmpeg -i input.mp3 -ac 1 -ar 16000 -c:a pcm_s16le media/audio.wav
ffmpeg -i photo.png -vf scale=320:240 -f rawvideo -pix_fmt rgb565le media/image.rgb565

media/ is gitignored.

Play it

src/bin/stream.rs downloads a file block by block and pipes it into ffplay (part of ffmpeg), which opens the window:

cargo run --release --bin stream -- image
cargo run --release --bin stream -- audio

The image opens in a 320×240 window that stays until you close it; the audio plays with a waveform window. Closing the window stops the download.

Flag Effect
--out FILE Also save the received bytes
--no-play Don't start ffplay (download only)
--timeout-ms N First retransmission timeout (default 2000, doubles, 4 retries)

Measured on localhost:

File Blocks Download, --no-play Retransmits
image 150 under 1 s 0
audio 313 0.16 s 0

Each block reply is 1050 B on the wire (26 B of CoAP header and options plus 1024 B of data). When playing, blocks are written to ffplay as they arrive and a full pipe slows the download down, but ffplay buffers several seconds ahead: a 10 s clip finishes downloading after about 2.4 s while it plays.

To see one block byte by byte:

./coap-inspect.py get /image --block 0

To test streaming end to end (generates its own media and server):

./test-stream.sh

Limits: at most 65,536 blocks per file (about 67 MB, roughly 35 minutes of this audio), download only, and no encryption.

Test

cargo test

Unit tests cover the greeting, payload parsing (text + JSON), query decoding, block selection, WAV and RGB565 validation, and the Block2/ETag/Size2 reply.

Layout

Path What it is
src/main.rs The server: socket, read loop, route table, helpers.
src/lib.rs Shared CoAP client, including block-wise downloads.
src/bin/client.rs CLI client for smoke tests.
src/bin/bridge.rs HTTP -> CoAP proxy so a browser can reach the server.
src/bin/stream.rs Streams /audio or /image into an ffplay window.
media/ Audio and image files the server streams (gitignored).
coap-inspect.py Sends one request and shows both packets byte by byte.
test-stream.sh End-to-end streaming test with its own server and media.
static/index.html CoAP reference page: routes, codes, copyable commands.
nodejs/ The earlier Node.js version, kept for reference.

Config

Env var Default Used by
COAP_PORT 5683 server, client, bridge
COAP_HOST 0.0.0.0 server, client, bridge
HTTP_PORT 8080 bridge
MEDIA_DIR media server

Design notes

  • One task, no per-request spawn. Handling a datagram is pure CPU work with no .await inside, so it runs inline on the receive loop — no task-spawn overhead, and replies keep their arrival order. Spawn per datagram once a handler does real I/O (a database, an upstream call).
  • A fixed 1152-byte receive buffer, reused across requests, so the steady state allocates only for the response body. 1152 is the RFC 7252 guidance for staying inside an unfragmented IPv6 datagram.
  • Hand-rolled JSON field read. json_string_field handles a flat {"name":"..."} and nothing more; it exists to avoid a serde_json dependency for one field. Swap it out the moment bodies get real structure.
  • Lenient percent-decoding. RFC 7252 §6.4 says the client decodes before splitting a URI into options, so a strict client sends name=Md Imran already decoded. Not all clients do, so the server decodes again — meaning a literal % in a name would be misread. Drop percent_decode if your clients are known-conformant.

Next steps

  • DTLS. Plaintext CoAP has no encryption or authentication. Production uses coaps:// on port 5684 — see webrtc-dtls or a gateway like libcoap.
  • Block-wise upload (Block1, RFC 7959). Downloads already use Block2 (see Streaming media); coap-lite's BlockHandler can reassemble uploads.
  • Observe (RFC 7641) for server-pushed updates; coap-lite provides Subject and create_notification.
  • Deduplication. A CoAP server should track recent message IDs and replay the cached response, since UDP clients retransmit. Fine to skip for idempotent greetings, needed once a POST has side effects.