Files
coapwithudp/README.md

246 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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, <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:
```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.