# CoAP Hello (UDP) — Rust MVP A minimal CoAP server over UDP in Rust, built on [`coap-lite`](https://crates.io/crates/coap-lite) (the RFC 7252 binary codec) and a [`tokio`](https://tokio.rs) `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 ```bash 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, !` | | 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: ```bash cargo run --release --bin coap-hello-udp ``` ```bash 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: ```bash 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: ```bash cargo run --bin client -- get "/hello?name=Imran" ``` ```bash cargo run --bin client -- post /hello "Imran" ``` Or with any other CoAP client, e.g. [`coap-cli`](https://www.npmjs.com/package/coap-cli): ```bash 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 ```bash mkdir -p media ``` ```bash ffmpeg -f lavfi -i "sine=frequency=440:duration=10" -ac 1 -ar 16000 -c:a pcm_s16le media/audio.wav ``` ```bash ffmpeg -f lavfi -i "testsrc=size=320x240" -frames:v 1 -f rawvideo -pix_fmt rgb565le media/image.rgb565 ``` Or convert your own files: ```bash ffmpeg -i input.mp3 -ac 1 -ar 16000 -c:a pcm_s16le media/audio.wav ``` ```bash 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: ```bash cargo run --release --bin stream -- image ``` ```bash 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: ```bash ./coap-inspect.py get /image --block 0 ``` To test streaming end to end (generates its own media and server): ```bash ./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 ```bash 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.