functionality updates
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
name = "coap-hello-udp"
|
name = "coap-hello-udp"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
default-run = "coap-hello-udp"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
coap-lite = "0.13.3"
|
coap-lite = "0.13.3"
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -46,20 +46,19 @@ cargo run --release --bin coap-hello-udp
|
|||||||
cargo run --release --bin bridge
|
cargo run --release --bin bridge
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open <http://localhost:8080/> for a test console with a name field and a
|
Every response shows the CoAP code mapped onto an HTTP status, with the raw
|
||||||
button per route. Every response shows both the CoAP code and the HTTP status
|
CoAP code returned verbatim in an `X-Coap-Code` header.
|
||||||
it was mapped to; the raw CoAP code is also returned in an `X-Coap-Code` header.
|
|
||||||
|
|
||||||
These are now ordinary URLs — clickable, curl-able, bookmarkable:
|
These are now ordinary URLs — clickable, curl-able, bookmarkable:
|
||||||
|
|
||||||
| URL | Response |
|
| URL | Response |
|
||||||
|--------------------------------------------------|------------------------------|
|
|--------------------------------------------------|------------------------------|
|
||||||
| `http://localhost:8080/` | the test console |
|
| `http://localhost:8080/` | the CoAP reference page |
|
||||||
| `http://localhost:8080/hello` | `Hello, World!` |
|
| `http://localhost:8080/hello` | `Hello, World!` |
|
||||||
| `http://localhost:8080/hello?name=Imran` | `Hello, Imran!` |
|
| `http://localhost:8080/hello?name=Imran` | `Hello, Imran!` |
|
||||||
| `http://localhost:8080/health` | `{"status":"ok","uptime":12}`|
|
| `http://localhost:8080/health` | `{"status":"ok","uptime":12}`|
|
||||||
|
|
||||||
POST needs a body, so it is easiest from the console page or curl:
|
POST needs a body, so use curl:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST -d "Imran" http://localhost:8080/hello
|
curl -X POST -d "Imran" http://localhost:8080/hello
|
||||||
@@ -104,7 +103,7 @@ decoding.
|
|||||||
| `src/lib.rs` | Shared CoAP client used by the CLI and the bridge. |
|
| `src/lib.rs` | Shared CoAP client used by the CLI and the bridge. |
|
||||||
| `src/bin/client.rs` | CLI client for smoke tests. |
|
| `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/bridge.rs` | HTTP -> CoAP proxy so a browser can reach the server. |
|
||||||
| `static/index.html` | The browser test console the bridge serves at `/`. |
|
| `static/index.html` | CoAP reference page: routes, codes, copyable commands. |
|
||||||
| `nodejs/` | The earlier Node.js version, kept for reference. |
|
| `nodejs/` | The earlier Node.js version, kept for reference. |
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|||||||
BIN
__pycache__/coap-inspect.cpython-312.pyc
Normal file
BIN
__pycache__/coap-inspect.cpython-312.pyc
Normal file
Binary file not shown.
200
coap-inspect.py
Executable file
200
coap-inspect.py
Executable file
@@ -0,0 +1,200 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Send one CoAP request over UDP and show both datagrams byte by byte.
|
||||||
|
|
||||||
|
./coap-inspect.py get "/hello?name=Imran"
|
||||||
|
./coap-inspect.py post /hello Imran
|
||||||
|
./coap-inspect.py get /health
|
||||||
|
|
||||||
|
No CoAP library: the packet is built and decoded right here, so every byte on
|
||||||
|
screen is accounted for. COAP_HOST / COAP_PORT retarget it (127.0.0.1:5683).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
TYPES = {0: "CON confirmable", 1: "NON non-confirmable", 2: "ACK acknowledgement", 3: "RST reset"}
|
||||||
|
METHODS = {"get": 1, "post": 2, "put": 3, "delete": 4}
|
||||||
|
CODES = {
|
||||||
|
"0.00": "Empty", "0.01": "GET", "0.02": "POST", "0.03": "PUT", "0.04": "DELETE",
|
||||||
|
"2.01": "Created", "2.02": "Deleted", "2.03": "Valid", "2.04": "Changed", "2.05": "Content",
|
||||||
|
"4.00": "Bad Request", "4.04": "Not Found", "4.05": "Method Not Allowed",
|
||||||
|
"5.00": "Internal Server Error",
|
||||||
|
}
|
||||||
|
OPTIONS = {3: "Uri-Host", 7: "Uri-Port", 11: "Uri-Path", 12: "Content-Format", 15: "Uri-Query", 17: "Accept"}
|
||||||
|
FORMATS = {0: "text/plain", 50: "application/json"}
|
||||||
|
|
||||||
|
TTY = sys.stdout.isatty()
|
||||||
|
|
||||||
|
|
||||||
|
def bold(text):
|
||||||
|
return f"\033[1m{text}\033[0m" if TTY else text
|
||||||
|
|
||||||
|
|
||||||
|
def encode_option(delta, value):
|
||||||
|
"""One option: a byte of 4-bit delta + 4-bit length, then any extended bytes, then the value."""
|
||||||
|
def nibble(n):
|
||||||
|
if n < 13:
|
||||||
|
return n, b""
|
||||||
|
if n < 269:
|
||||||
|
return 13, bytes([n - 13])
|
||||||
|
return 14, (n - 269).to_bytes(2, "big")
|
||||||
|
|
||||||
|
d, d_ext = nibble(delta)
|
||||||
|
l, l_ext = nibble(len(value))
|
||||||
|
return bytes([(d << 4) | l]) + d_ext + l_ext + value
|
||||||
|
|
||||||
|
|
||||||
|
def build_request(method, path, query, payload):
|
||||||
|
token = os.urandom(4)
|
||||||
|
header = bytes([(1 << 6) | (0 << 4) | len(token), METHODS[method]]) # ver 1, CON, TKL, code
|
||||||
|
header += random.randrange(1 << 16).to_bytes(2, "big") + token # message id, token
|
||||||
|
|
||||||
|
# Options must be in ascending number order; each stores the gap from the previous one.
|
||||||
|
options = [(11, s.encode()) for s in path.split("/") if s]
|
||||||
|
options += [(15, q.encode()) for q in query.split("&") if q]
|
||||||
|
body, last = b"", 0
|
||||||
|
for number, value in options:
|
||||||
|
body += encode_option(number - last, value)
|
||||||
|
last = number
|
||||||
|
if payload:
|
||||||
|
body += b"\xff" + payload
|
||||||
|
return header + body
|
||||||
|
|
||||||
|
|
||||||
|
def decode(pkt):
|
||||||
|
"""Return (rows, meta). Each row is (start, end, field, meaning) over pkt[start:end]."""
|
||||||
|
if len(pkt) < 4:
|
||||||
|
raise ValueError(f"{len(pkt)} bytes is shorter than the 4-byte CoAP header")
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
b0 = pkt[0]
|
||||||
|
ver, typ, tkl = b0 >> 6, (b0 >> 4) & 0b11, b0 & 0b1111
|
||||||
|
rows.append((0, 1, "ver|type|tkl",
|
||||||
|
f"{ver:02b} {typ:02b} {tkl:04b} -> version {ver}, {TYPES[typ]}, token {tkl} bytes"))
|
||||||
|
|
||||||
|
cls, detail = pkt[1] >> 5, pkt[1] & 0b11111
|
||||||
|
code = f"{cls}.{detail:02d}"
|
||||||
|
rows.append((1, 2, "code", f"{cls:03b} {detail:05b} -> {code} {CODES.get(code, '')}"))
|
||||||
|
|
||||||
|
mid = int.from_bytes(pkt[2:4], "big")
|
||||||
|
rows.append((2, 4, "message id", f"{mid} (0x{mid:04x})"))
|
||||||
|
|
||||||
|
token = pkt[4:4 + tkl]
|
||||||
|
if tkl:
|
||||||
|
rows.append((4, 4 + tkl, "token", f"0x{token.hex()}"))
|
||||||
|
|
||||||
|
i, number = 4 + tkl, 0
|
||||||
|
while i < len(pkt):
|
||||||
|
if pkt[i] == 0xFF:
|
||||||
|
body = pkt[i + 1:]
|
||||||
|
rows.append((i, i + 1, "payload marker", "0xff -> the rest is the body"))
|
||||||
|
rows.append((i + 1, len(pkt), "payload",
|
||||||
|
f"{len(body)} bytes: {body.decode('utf-8', 'replace')}"))
|
||||||
|
break
|
||||||
|
|
||||||
|
start = i
|
||||||
|
delta, length = pkt[i] >> 4, pkt[i] & 0b1111
|
||||||
|
i += 1
|
||||||
|
extended = []
|
||||||
|
for which, n in (("delta", delta), ("length", length)):
|
||||||
|
if n == 13:
|
||||||
|
n, i = pkt[i] + 13, i + 1
|
||||||
|
elif n == 14:
|
||||||
|
n, i = int.from_bytes(pkt[i:i + 2], "big") + 269, i + 2
|
||||||
|
extended.append(n)
|
||||||
|
delta, length = extended
|
||||||
|
number += delta
|
||||||
|
value = pkt[i:i + length]
|
||||||
|
i += length
|
||||||
|
|
||||||
|
if number in (12, 17):
|
||||||
|
n = int.from_bytes(value, "big") if value else 0
|
||||||
|
shown = f"{n} = {FORMATS.get(n, 'unknown format')}"
|
||||||
|
else:
|
||||||
|
shown = f'"{value.decode("utf-8", "replace")}"'
|
||||||
|
rows.append((start, i, OPTIONS.get(number, f"option {number}"),
|
||||||
|
f"delta {delta} -> option {number}, {length} bytes: {shown}"))
|
||||||
|
|
||||||
|
return rows, {"type": typ, "code": code, "mid": mid, "token": token}
|
||||||
|
|
||||||
|
|
||||||
|
def show(title, pkt):
|
||||||
|
print(bold(f"\n{title} ({len(pkt)} bytes)"))
|
||||||
|
|
||||||
|
print(bold("\n hex + ascii"))
|
||||||
|
for off in range(0, len(pkt), 16):
|
||||||
|
chunk = pkt[off:off + 16]
|
||||||
|
hexes = " ".join(f"{b:02x}" for b in chunk)
|
||||||
|
text = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
|
||||||
|
print(f" {off:04x} {hexes:<47} |{text}|")
|
||||||
|
|
||||||
|
print(bold("\n binary"))
|
||||||
|
for off in range(0, len(pkt), 4):
|
||||||
|
chunk = pkt[off:off + 4]
|
||||||
|
bits = " ".join(f"{b:08b}" for b in chunk)
|
||||||
|
text = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
|
||||||
|
print(f" {off:04x} {bits:<35} |{text}|")
|
||||||
|
|
||||||
|
rows, meta = decode(pkt)
|
||||||
|
print(bold("\n fields"))
|
||||||
|
print(f" {'byte':<7} {'hex':<24} {'field':<15} meaning")
|
||||||
|
for start, end, field, meaning in rows:
|
||||||
|
span = f"{start}" if end - start == 1 else f"{start}-{end - 1}"
|
||||||
|
hexes = pkt[start:end].hex(" ")
|
||||||
|
if len(hexes) > 23:
|
||||||
|
hexes = hexes[:20] + "..."
|
||||||
|
print(f" {span:<7} {hexes:<24} {field:<15} {meaning}")
|
||||||
|
return meta
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
if not args or args[0].lower() not in METHODS:
|
||||||
|
print(__doc__.strip())
|
||||||
|
return 2
|
||||||
|
|
||||||
|
method = args[0].lower()
|
||||||
|
target = args[1] if len(args) > 1 else "/hello"
|
||||||
|
payload = args[2].encode() if len(args) > 2 else b""
|
||||||
|
path, _, query = target.partition("?")
|
||||||
|
host = os.environ.get("COAP_HOST", "127.0.0.1")
|
||||||
|
port = int(os.environ.get("COAP_PORT", "5683"))
|
||||||
|
|
||||||
|
request = build_request(method, path, query, payload)
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # SOCK_DGRAM = UDP
|
||||||
|
sock.settimeout(3)
|
||||||
|
sock.connect((host, port))
|
||||||
|
local = "%s:%d" % sock.getsockname()
|
||||||
|
|
||||||
|
req = show(f">>> REQUEST udp {local} -> {host}:{port}", request)
|
||||||
|
sent_at = time.perf_counter()
|
||||||
|
sock.send(request)
|
||||||
|
try:
|
||||||
|
response = sock.recv(1152)
|
||||||
|
except socket.timeout:
|
||||||
|
print(f"\nno reply within 3s - is the CoAP server running on {host}:{port}?")
|
||||||
|
return 1
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
print(f"\nport unreachable - nothing is listening on udp {host}:{port}")
|
||||||
|
return 1
|
||||||
|
elapsed = (time.perf_counter() - sent_at) * 1000
|
||||||
|
|
||||||
|
res = show(f"<<< RESPONSE udp {host}:{port} -> {local}", response)
|
||||||
|
|
||||||
|
print(bold("\n exchange"))
|
||||||
|
print(f" request {TYPES[req['type']].split()[0]} {req['code']} mid 0x{req['mid']:04x} "
|
||||||
|
f"token 0x{req['token'].hex()} {len(request)} bytes")
|
||||||
|
print(f" response {TYPES[res['type']].split()[0]} {res['code']} mid 0x{res['mid']:04x} "
|
||||||
|
f"token 0x{res['token'].hex()} {len(response)} bytes after {elapsed:.2f} ms")
|
||||||
|
same_mid = "yes" if req["mid"] == res["mid"] else "NO"
|
||||||
|
same_token = "yes" if req["token"] == res["token"] else "NO"
|
||||||
|
kind = " (piggybacked: the ACK carries the answer)" if res["type"] == 2 else ""
|
||||||
|
print(f" message id matches: {same_mid} token matches: {same_token}{kind}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
31
healthcheck.sh
Executable file
31
healthcheck.sh
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Liveness check for the CoAP server. Exits 0 if healthy, 1 if not.
|
||||||
|
# ./healthcheck.sh quiet, for cron/systemd/monitoring
|
||||||
|
# ./healthcheck.sh -v print what it found
|
||||||
|
# Retargets with COAP_HOST / COAP_PORT.
|
||||||
|
|
||||||
|
CLIENT=${CLIENT:-$(dirname "$0")/target/release/client}
|
||||||
|
verbose=false; [[ $1 == -v ]] && verbose=true
|
||||||
|
say() { $verbose && echo "$@"; }
|
||||||
|
|
||||||
|
host=${COAP_HOST:-127.0.0.1}; port=${COAP_PORT:-5683}
|
||||||
|
|
||||||
|
[[ -x $CLIENT ]] || { echo "healthcheck: no client binary at $CLIENT (cargo build --release)"; exit 1; }
|
||||||
|
|
||||||
|
reply=$("$CLIENT" get /health 2>&1)
|
||||||
|
|
||||||
|
# A bound socket proves nothing on UDP - the round trip is the real test.
|
||||||
|
if [[ $reply != "<- 2.05 "* ]]; then
|
||||||
|
echo "UNHEALTHY: coap://$host:$port/health -> ${reply:-no reply}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
body=${reply#<- 2.05 }
|
||||||
|
if [[ $body != *'"status":"ok"'* ]]; then
|
||||||
|
echo "UNHEALTHY: unexpected body from coap://$host:$port/health -> $body"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
uptime=$(sed -n 's/.*"uptime":\([0-9]*\).*/\1/p' <<<"$body")
|
||||||
|
say "HEALTHY: coap://$host:$port up ${uptime}s (code 2.05)"
|
||||||
|
exit 0
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
// Tiny CLI client:
|
|
||||||
// node client.js get /hello?name=Imran
|
|
||||||
// node client.js post /hello Imran
|
|
||||||
// node client.js get /health
|
|
||||||
|
|
||||||
const coap = require('coap');
|
|
||||||
|
|
||||||
const [, , methodArg = 'get', pathArg = '/hello', payload] = process.argv;
|
|
||||||
|
|
||||||
const req = coap.request({
|
|
||||||
host: process.env.COAP_HOST || 'localhost',
|
|
||||||
port: Number(process.env.COAP_PORT) || 5683,
|
|
||||||
pathname: pathArg.split('?')[0],
|
|
||||||
query: pathArg.split('?')[1] || '',
|
|
||||||
method: methodArg.toUpperCase(),
|
|
||||||
confirmable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('response', (res) => {
|
|
||||||
console.log(`<- ${res.code} ${res.payload.toString()}`);
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', (err) => {
|
|
||||||
console.error('request failed:', err.message);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (payload) req.write(payload);
|
|
||||||
req.end();
|
|
||||||
309
nodejs/package-lock.json
generated
309
nodejs/package-lock.json
generated
@@ -1,309 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "coapwithudp",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "coapwithudp",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"coap": "^1.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
|
||||||
"version": "26.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz",
|
|
||||||
"integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"undici-types": "~8.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/readable-stream": {
|
|
||||||
"version": "4.0.24",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz",
|
|
||||||
"integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/abort-controller": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"event-target-shim": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/base64-js": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/bl": {
|
|
||||||
"version": "6.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
|
|
||||||
"integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/readable-stream": "^4.0.0",
|
|
||||||
"buffer": "^6.0.3",
|
|
||||||
"inherits": "^2.0.4",
|
|
||||||
"readable-stream": "^4.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/buffer": {
|
|
||||||
"version": "6.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
|
||||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"base64-js": "^1.3.1",
|
|
||||||
"ieee754": "^1.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/capitalize": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/capitalize/-/capitalize-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-wcSyiFqXRYyCoqu0o0ekXzJAKCLMkqWS5QWGlgTJFJKwRmI6pzcN2hBl5VPq9RzLW5Uf4FF/V/lcFfjCtVak2w==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/cbor": {
|
|
||||||
"version": "9.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz",
|
|
||||||
"integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"nofilter": "^3.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/coap": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/coap/-/coap-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-AvPHT2yQet0wOhlmIqFk11NQPnsKt30yRk6IGp9Yhb6Xnp3ozExPfklJXiE28Xb47tjU0M2OV2W++h8f350Mjg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/readable-stream": "^4.0.14",
|
|
||||||
"bl": "^6.0.12",
|
|
||||||
"capitalize": "^2.0.4",
|
|
||||||
"coap-oscore": "^2.2.3",
|
|
||||||
"coap-packet": "^1.1.1",
|
|
||||||
"debug": "^4.3.5",
|
|
||||||
"fastseries": "^2.0.0",
|
|
||||||
"lru-cache": "^11.0.2",
|
|
||||||
"readable-stream": "^4.5.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/coap-oscore": {
|
|
||||||
"version": "2.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/coap-oscore/-/coap-oscore-2.2.3.tgz",
|
|
||||||
"integrity": "sha512-uF592/QIDgxbwhvAHCiWJat0xIoWsc8K7ubzYK1jXM4ihe50sHLAM7pl361NzZXLOMkkYnHCBSq07DA56OIs/A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cbor": "^9.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/coap-packet": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/coap-packet/-/coap-packet-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-Bkz2ZKI/7hU2gm6nUuo5l+MBSkdFJx7My1ZgNEhKUC7K2yYfQYVbBPRa64BBYLcEcYgaSlau4A1Uw5xfM2I0zw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
|
||||||
"version": "4.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/event-target-shim": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/events": {
|
|
||||||
"version": "3.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
|
||||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fastseries": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fastseries/-/fastseries-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-XBU9RXeoYc2/VnvMhplAxEmZLfIk7cvTBu+xwoBuTI8pL19E03cmca17QQycKIdxgwCeFA/a4u27gv1h3ya5LQ==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/ieee754": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/inherits": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/lru-cache": {
|
|
||||||
"version": "11.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
|
||||||
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"engines": {
|
|
||||||
"node": "20 || >=22"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/nofilter": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.19"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/process": {
|
|
||||||
"version": "0.11.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
|
||||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/readable-stream": {
|
|
||||||
"version": "4.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
|
||||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"abort-controller": "^3.0.0",
|
|
||||||
"buffer": "^6.0.3",
|
|
||||||
"events": "^3.3.0",
|
|
||||||
"process": "^0.11.10",
|
|
||||||
"string_decoder": "^1.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/safe-buffer": {
|
|
||||||
"version": "5.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/string_decoder": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "~5.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undici-types": {
|
|
||||||
"version": "8.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
|
||||||
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "coap-hello-udp",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "server.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node server.js",
|
|
||||||
"client": "node client.js"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"description": "Minimal CoAP-over-UDP hello world server with GET/POST routes",
|
|
||||||
"dependencies": {
|
|
||||||
"coap": "^1.6.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
102
nodejs/server.js
102
nodejs/server.js
@@ -1,102 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const coap = require('coap');
|
|
||||||
|
|
||||||
const PORT = Number(process.env.COAP_PORT) || 5683;
|
|
||||||
const HOST = process.env.COAP_HOST || '0.0.0.0';
|
|
||||||
|
|
||||||
// --- helpers ---------------------------------------------------------------
|
|
||||||
|
|
||||||
const TEXT = 'text/plain';
|
|
||||||
const JSON_FMT = 'application/json';
|
|
||||||
|
|
||||||
/** Split "/hello?name=Bob" into ["/hello", "name=Bob"]. */
|
|
||||||
function splitUrl(url) {
|
|
||||||
const i = url.indexOf('?');
|
|
||||||
const path = (i === -1 ? url : url.slice(0, i)).replace(/\/+$/, '') || '/';
|
|
||||||
return [path, i === -1 ? '' : url.slice(i + 1)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Pull a name out of "?name=Bob" style queries. */
|
|
||||||
function nameFromQuery(query) {
|
|
||||||
return new URLSearchParams(query || '').get('name');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Pull a name out of a POST body: raw text or {"name":"Bob"}. */
|
|
||||||
function nameFromBody(buf) {
|
|
||||||
const body = buf.toString('utf8').trim();
|
|
||||||
if (!body) return null;
|
|
||||||
if (body.startsWith('{')) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(body);
|
|
||||||
return typeof parsed.name === 'string' ? parsed.name.trim() : null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
function greet(name) {
|
|
||||||
return `Hello, ${name && name.length ? name : 'World'}!`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function send(res, code, payload, format = TEXT) {
|
|
||||||
res.code = code;
|
|
||||||
res.setOption('Content-Format', format);
|
|
||||||
res.end(typeof payload === 'string' ? payload : JSON.stringify(payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- routes ----------------------------------------------------------------
|
|
||||||
// key: "METHOD /path"
|
|
||||||
|
|
||||||
const routes = {
|
|
||||||
'GET /hello': (req, res) => {
|
|
||||||
send(res, '2.05', greet(nameFromQuery(req.query)));
|
|
||||||
},
|
|
||||||
|
|
||||||
'POST /hello': (req, res) => {
|
|
||||||
const name = nameFromBody(req.payload) || nameFromQuery(req.query);
|
|
||||||
if (!name) {
|
|
||||||
return send(res, '4.00', 'Bad Request: send a name as the payload');
|
|
||||||
}
|
|
||||||
send(res, '2.01', greet(name));
|
|
||||||
},
|
|
||||||
|
|
||||||
'GET /health': (_req, res) => {
|
|
||||||
send(res, '2.05', { status: 'ok', uptime: Math.round(process.uptime()) }, JSON_FMT);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- server ----------------------------------------------------------------
|
|
||||||
|
|
||||||
const server = coap.createServer({ type: 'udp4' });
|
|
||||||
|
|
||||||
server.on('request', (req, res) => {
|
|
||||||
const [path, query] = splitUrl(req.url);
|
|
||||||
req.query = query; // node-coap only gives us req.url on the server side
|
|
||||||
const key = `${req.method} ${path}`;
|
|
||||||
const handler = routes[key];
|
|
||||||
|
|
||||||
console.log(`${req.method} ${req.url} from ${req.rsinfo.address}:${req.rsinfo.port}`);
|
|
||||||
|
|
||||||
if (!handler) return send(res, '4.04', `Not Found: ${path}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
handler(req, res);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('handler error:', err);
|
|
||||||
send(res, '5.00', 'Internal Server Error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on('error', (err) => console.error('server error:', err));
|
|
||||||
|
|
||||||
server.listen(PORT, HOST, () => {
|
|
||||||
console.log(`CoAP server (UDP) listening on coap://${HOST}:${PORT}`);
|
|
||||||
console.log(' GET /hello?name=Imran');
|
|
||||||
console.log(' POST /hello (payload: Imran | {"name":"Imran"})');
|
|
||||||
console.log(' GET /health');
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGINT', () => server.close(() => process.exit(0)));
|
|
||||||
@@ -269,6 +269,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
||||||
fn reads_name_from_text_and_json_payloads() {
|
fn reads_name_from_text_and_json_payloads() {
|
||||||
assert_eq!(name_from_payload(b"Imran").as_deref(), Some("Imran"));
|
assert_eq!(name_from_payload(b"Imran").as_deref(), Some("Imran"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>CoAP Hello — Test Console</title>
|
<title>CoAP Hello — UDP Reference</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
@@ -23,112 +23,144 @@
|
|||||||
margin: 0; padding: 2rem 1.25rem; background: var(--bg); color: var(--fg);
|
margin: 0; padding: 2rem 1.25rem; background: var(--bg); color: var(--fg);
|
||||||
font: 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
font: 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
main { max-width: 640px; margin: 0 auto; }
|
main { max-width: 680px; margin: 0 auto; }
|
||||||
h1 { font-size: 1.4rem; margin: 0 0 .35rem; letter-spacing: -.01em; }
|
h1 { font-size: 1.4rem; margin: 0 0 .35rem; letter-spacing: -.01em; }
|
||||||
.sub { color: var(--muted); margin: 0 0 1.75rem; font-size: .9rem; }
|
.sub { color: var(--muted); margin: 0 0 1.75rem; font-size: .9rem; }
|
||||||
.card {
|
.card {
|
||||||
background: var(--card); border: 1px solid var(--border);
|
background: var(--card); border: 1px solid var(--border);
|
||||||
border-radius: 10px; padding: 1.25rem; margin-bottom: 1rem;
|
border-radius: 10px; padding: 1.25rem; margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
h2 { font-size: .82rem; text-transform: uppercase; letter-spacing: .06em;
|
||||||
|
color: var(--muted); margin: 0 0 .7rem; }
|
||||||
label { display: block; font-weight: 600; font-size: .82rem; margin-bottom: .4rem; }
|
label { display: block; font-weight: 600; font-size: .82rem; margin-bottom: .4rem; }
|
||||||
input {
|
input {
|
||||||
width: 100%; padding: .6rem .7rem; font: inherit; border-radius: 7px;
|
width: 100%; padding: .6rem .7rem; font: inherit; border-radius: 7px;
|
||||||
border: 1px solid var(--border); background: var(--bg); color: var(--fg);
|
border: 1px solid var(--border); background: var(--bg); color: var(--fg);
|
||||||
}
|
}
|
||||||
input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||||
.row { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .85rem; }
|
.hint { color: var(--muted); font-size: .82rem; margin: .8rem 0 0; }
|
||||||
button {
|
.cmd { display: flex; gap: .5rem; align-items: stretch; margin-top: .55rem; }
|
||||||
font: inherit; font-weight: 600; padding: .55rem 1rem; border-radius: 7px;
|
.cmd pre { flex: 1; margin: 0; }
|
||||||
border: 1px solid var(--border); background: var(--bg); color: var(--fg); cursor: pointer;
|
|
||||||
}
|
|
||||||
button:hover { border-color: var(--accent); color: var(--accent); }
|
|
||||||
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
||||||
button.primary:hover { opacity: .88; color: #fff; }
|
|
||||||
pre {
|
pre {
|
||||||
background: var(--code-bg); border: 1px solid var(--border); border-radius: 7px;
|
background: var(--code-bg); border: 1px solid var(--border); border-radius: 7px;
|
||||||
padding: .8rem; overflow-x: auto; margin: 0;
|
padding: .7rem .8rem; overflow-x: auto; margin: 0;
|
||||||
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
}
|
}
|
||||||
.status { font-weight: 600; }
|
button {
|
||||||
.status.ok { color: var(--ok); }
|
font: inherit; font-size: .8rem; font-weight: 600; padding: .35rem .7rem;
|
||||||
.status.err { color: var(--err); }
|
border-radius: 7px; border: 1px solid var(--border);
|
||||||
h2 { font-size: .82rem; text-transform: uppercase; letter-spacing: .06em;
|
background: var(--bg); color: var(--muted); cursor: pointer; white-space: nowrap;
|
||||||
color: var(--muted); margin: 0 0 .7rem; }
|
}
|
||||||
.hint { color: var(--muted); font-size: .82rem; margin: .8rem 0 0; }
|
button:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
|
button.copied { color: var(--ok); border-color: var(--ok); }
|
||||||
|
.what { font-size: .8rem; color: var(--muted); margin: 1rem 0 .1rem; }
|
||||||
|
.what:first-of-type { margin-top: 0; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: .87rem; }
|
||||||
|
th, td { text-align: left; padding: .4rem .5rem; border-bottom: 1px solid var(--border); }
|
||||||
|
th { font-size: .74rem; text-transform: uppercase; letter-spacing: .05em;
|
||||||
|
color: var(--muted); font-weight: 600; }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
td.code { font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; white-space: nowrap; }
|
||||||
|
.ok { color: var(--ok); font-weight: 600; }
|
||||||
|
.err { color: var(--err); font-weight: 600; }
|
||||||
code { background: var(--code-bg); padding: .1em .35em; border-radius: 4px;
|
code { background: var(--code-bg); padding: .1em .35em; border-radius: 4px;
|
||||||
font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; }
|
font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||||
|
a { color: var(--accent); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<h1>CoAP Hello — Test Console</h1>
|
<h1>CoAP Hello — UDP Reference</h1>
|
||||||
<p class="sub">This page talks HTTP to a local bridge, which reissues each
|
<p class="sub">This server speaks CoAP over UDP only, so there is nothing here
|
||||||
call as CoAP over UDP and shows you the raw response code.</p>
|
for a browser to call. Type a name and copy the command for the client you
|
||||||
|
use — the page is static and works straight off disk.</p>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input id="name" value="Imran" autocomplete="off" placeholder="leave blank for "World"">
|
<input id="name" value="Imran" autocomplete="off" spellcheck="false"
|
||||||
<div class="row">
|
placeholder="leave blank for "World"">
|
||||||
<button class="primary" data-act="get">GET /hello</button>
|
|
||||||
<button data-act="post">POST /hello</button>
|
<p class="what">Bundled client — <code>cargo build --release</code> first</p>
|
||||||
<button data-act="json">POST (JSON body)</button>
|
<div class="cmd"><pre id="c-get"></pre><button data-copy="c-get">Copy</button></div>
|
||||||
<button data-act="health">GET /health</button>
|
<div class="cmd"><pre id="c-post"></pre><button data-copy="c-post">Copy</button></div>
|
||||||
<button data-act="notfound">GET /nope</button>
|
<div class="cmd"><pre id="c-json"></pre><button data-copy="c-json">Copy</button></div>
|
||||||
</div>
|
<div class="cmd"><pre id="c-health"></pre><button data-copy="c-health">Copy</button></div>
|
||||||
<p class="hint">Or hit an endpoint directly:
|
|
||||||
<a href="/hello?name=Imran">/hello?name=Imran</a> ·
|
<p class="what">Any other CoAP client, e.g. <a href="https://www.npmjs.com/package/coap-cli">coap-cli</a></p>
|
||||||
<a href="/health">/health</a></p>
|
<div class="cmd"><pre id="c-cli"></pre><button data-copy="c-cli">Copy</button></div>
|
||||||
|
|
||||||
|
<p class="hint">Set <code>COAP_HOST</code> / <code>COAP_PORT</code> to point at
|
||||||
|
another server; both default to <code>127.0.0.1:5683</code>.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Response</h2>
|
<h2>Routes</h2>
|
||||||
<p id="status" class="status">—</p>
|
<table>
|
||||||
<pre id="out">Pick an action above.</pre>
|
<thead><tr><th>Request</th><th>Code</th><th>Response</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="code">GET /hello</td><td class="code ok">2.05</td><td class="code">Hello, World!</td></tr>
|
||||||
|
<tr><td class="code">GET /hello?name=…</td><td class="code ok">2.05</td><td class="code">Hello, …!</td></tr>
|
||||||
|
<tr><td class="code">POST /hello</td><td class="code ok">2.01</td><td class="code">Hello, <payload>!</td></tr>
|
||||||
|
<tr><td class="code">GET /health</td><td class="code ok">2.05</td><td class="code">{"status":"ok","uptime":12}</td></tr>
|
||||||
|
<tr><td class="code">POST /hello (empty)</td><td class="code err">4.00</td><td class="code">Bad Request</td></tr>
|
||||||
|
<tr><td class="code">GET /nope</td><td class="code err">4.04</td><td class="code">Not Found</td></tr>
|
||||||
|
<tr><td class="code">PUT /hello</td><td class="code err">4.05</td><td class="code">Method Not Allowed</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="hint">CoAP writes response codes as <code>class.detail</code>:
|
||||||
|
<code>2.05</code> is HTTP's 200, <code>4.04</code> its 404.
|
||||||
|
<code>POST /hello</code> takes raw text or <code>{"name":"…"}</code>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Scripts</h2>
|
||||||
|
<p class="what">Full request/response suite against a running server</p>
|
||||||
|
<div class="cmd"><pre id="c-test">./test-coap.sh</pre><button data-copy="c-test">Copy</button></div>
|
||||||
|
<p class="what">Liveness check — exit 0 healthy, 1 not</p>
|
||||||
|
<div class="cmd"><pre id="c-health-sh">./healthcheck.sh -v</pre><button data-copy="c-health-sh">Copy</button></div>
|
||||||
|
<p class="what">Start the server</p>
|
||||||
|
<div class="cmd"><pre id="c-run">cargo run --release</pre><button data-copy="c-run">Copy</button></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const shell = (s) => "'" + String(s).replace(/'/g, `'\\''`) + "'";
|
||||||
|
|
||||||
async function call(method, path, body) {
|
function render() {
|
||||||
$('status').textContent = '…';
|
|
||||||
$('status').className = 'status';
|
|
||||||
$('out').textContent = '';
|
|
||||||
try {
|
|
||||||
const res = await fetch(path, {
|
|
||||||
method,
|
|
||||||
body,
|
|
||||||
headers: body ? { 'Content-Type': 'text/plain' } : undefined,
|
|
||||||
});
|
|
||||||
const text = await res.text();
|
|
||||||
const coap = res.headers.get('X-Coap-Code') || '—';
|
|
||||||
$('status').textContent = `CoAP ${coap} · HTTP ${res.status}`;
|
|
||||||
$('status').className = 'status ' + (res.ok ? 'ok' : 'err');
|
|
||||||
$('out').textContent = `${method} ${path}\n${body ? 'body: ' + body + '\n' : ''}\n${text || '(empty)'}`;
|
|
||||||
} catch (err) {
|
|
||||||
$('status').textContent = 'Request failed';
|
|
||||||
$('status').className = 'status err';
|
|
||||||
$('out').textContent = String(err) + '\n\nIs the bridge still running?';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll('button[data-act]').forEach((btn) => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const name = $('name').value.trim();
|
const name = $('name').value.trim();
|
||||||
switch (btn.dataset.act) {
|
const q = name ? '/hello?name=' + encodeURIComponent(name) : '/hello';
|
||||||
case 'get':
|
const who = name || 'Imran';
|
||||||
return call('GET', '/hello' + (name ? '?name=' + encodeURIComponent(name) : ''));
|
|
||||||
case 'post':
|
$('c-get').textContent = `./target/release/client get ${shell(q)}`;
|
||||||
return call('POST', '/hello', name || '');
|
$('c-post').textContent = `./target/release/client post /hello ${shell(who)}`;
|
||||||
case 'json':
|
$('c-json').textContent = `./target/release/client post /hello ${shell(JSON.stringify({ name: who }))}`;
|
||||||
return call('POST', '/hello', JSON.stringify({ name }));
|
$('c-health').textContent = `./target/release/client get /health`;
|
||||||
case 'health':
|
$('c-cli').textContent = `coap get ${shell('coap://localhost:5683' + q)}`;
|
||||||
return call('GET', '/health');
|
|
||||||
case 'notfound':
|
|
||||||
return call('GET', '/nope');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('button[data-copy]').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const text = $(btn.dataset.copy).textContent;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
} catch {
|
||||||
|
// file:// and non-secure origins deny the clipboard API; fall back.
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text;
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
ta.remove();
|
||||||
|
}
|
||||||
|
btn.textContent = 'Copied';
|
||||||
|
btn.classList.add('copied');
|
||||||
|
setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('name').addEventListener('input', render);
|
||||||
|
render();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user