@sirena-lwm2m/coap 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +78 -0
- package/dist/api/exchange-context.d.ts +34 -0
- package/dist/api/exchange-context.d.ts.map +1 -0
- package/dist/api/exchange-context.js +2 -0
- package/dist/api/exchange-context.js.map +1 -0
- package/dist/api/headers.d.ts +10 -0
- package/dist/api/headers.d.ts.map +1 -0
- package/dist/api/headers.js +69 -0
- package/dist/api/headers.js.map +1 -0
- package/dist/api/index.d.ts +5 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +4 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/request.d.ts +11 -0
- package/dist/api/request.d.ts.map +1 -0
- package/dist/api/request.js +49 -0
- package/dist/api/request.js.map +1 -0
- package/dist/api/response.d.ts +11 -0
- package/dist/api/response.d.ts.map +1 -0
- package/dist/api/response.js +19 -0
- package/dist/api/response.js.map +1 -0
- package/dist/blockwise/block1-assembler.d.ts +26 -0
- package/dist/blockwise/block1-assembler.d.ts.map +1 -0
- package/dist/blockwise/block1-assembler.js +94 -0
- package/dist/blockwise/block1-assembler.js.map +1 -0
- package/dist/blockwise/block2-chunker.d.ts +23 -0
- package/dist/blockwise/block2-chunker.d.ts.map +1 -0
- package/dist/blockwise/block2-chunker.js +99 -0
- package/dist/blockwise/block2-chunker.js.map +1 -0
- package/dist/blockwise/index.d.ts +8 -0
- package/dist/blockwise/index.d.ts.map +1 -0
- package/dist/blockwise/index.js +5 -0
- package/dist/blockwise/index.js.map +1 -0
- package/dist/blockwise/middleware.d.ts +7 -0
- package/dist/blockwise/middleware.d.ts.map +1 -0
- package/dist/blockwise/middleware.js +120 -0
- package/dist/blockwise/middleware.js.map +1 -0
- package/dist/blockwise/option.d.ts +12 -0
- package/dist/blockwise/option.d.ts.map +1 -0
- package/dist/blockwise/option.js +55 -0
- package/dist/blockwise/option.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/message/index.d.ts +21 -0
- package/dist/message/index.d.ts.map +1 -0
- package/dist/message/index.js +79 -0
- package/dist/message/index.js.map +1 -0
- package/dist/observability.d.ts +44 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +71 -0
- package/dist/observability.js.map +1 -0
- package/dist/observe-notification.d.ts +21 -0
- package/dist/observe-notification.d.ts.map +1 -0
- package/dist/observe-notification.js +87 -0
- package/dist/observe-notification.js.map +1 -0
- package/dist/observe.d.ts +10 -0
- package/dist/observe.d.ts.map +1 -0
- package/dist/observe.js +3 -0
- package/dist/observe.js.map +1 -0
- package/dist/option-parser/index.d.ts +30 -0
- package/dist/option-parser/index.d.ts.map +1 -0
- package/dist/option-parser/index.js +125 -0
- package/dist/option-parser/index.js.map +1 -0
- package/dist/router/app.d.ts +8 -0
- package/dist/router/app.d.ts.map +1 -0
- package/dist/router/app.js +25 -0
- package/dist/router/app.js.map +1 -0
- package/dist/router/content-format.d.ts +7 -0
- package/dist/router/content-format.d.ts.map +1 -0
- package/dist/router/content-format.js +23 -0
- package/dist/router/content-format.js.map +1 -0
- package/dist/router/index.d.ts +9 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +5 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/radix.d.ts +16 -0
- package/dist/router/radix.d.ts.map +1 -0
- package/dist/router/radix.js +69 -0
- package/dist/router/radix.js.map +1 -0
- package/dist/router/router.d.ts +38 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/router.js +111 -0
- package/dist/router/router.js.map +1 -0
- package/dist/router.d.ts +2 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +2 -0
- package/dist/router.js.map +1 -0
- package/dist/security.d.ts +3 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +2 -0
- package/dist/security.js.map +1 -0
- package/dist/store/index.d.ts +26 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +68 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/sqlite-store.d.ts +10 -0
- package/dist/store/sqlite-store.d.ts.map +1 -0
- package/dist/store/sqlite-store.js +75 -0
- package/dist/store/sqlite-store.js.map +1 -0
- package/dist/store.d.ts +5 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +3 -0
- package/dist/store.js.map +1 -0
- package/dist/transport/dtls.d.ts +38 -0
- package/dist/transport/dtls.d.ts.map +1 -0
- package/dist/transport/dtls.js +151 -0
- package/dist/transport/dtls.js.map +1 -0
- package/dist/transport/events.d.ts +34 -0
- package/dist/transport/events.d.ts.map +1 -0
- package/dist/transport/events.js +2 -0
- package/dist/transport/events.js.map +1 -0
- package/dist/transport/index.d.ts +10 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +5 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/tcp-connection.d.ts +18 -0
- package/dist/transport/tcp-connection.d.ts.map +1 -0
- package/dist/transport/tcp-connection.js +72 -0
- package/dist/transport/tcp-connection.js.map +1 -0
- package/dist/transport/tcp-frame.d.ts +17 -0
- package/dist/transport/tcp-frame.d.ts.map +1 -0
- package/dist/transport/tcp-frame.js +112 -0
- package/dist/transport/tcp-frame.js.map +1 -0
- package/dist/transport/tcp-stream.d.ts +21 -0
- package/dist/transport/tcp-stream.d.ts.map +1 -0
- package/dist/transport/tcp-stream.js +52 -0
- package/dist/transport/tcp-stream.js.map +1 -0
- package/dist/transport/udp.d.ts +97 -0
- package/dist/transport/udp.d.ts.map +1 -0
- package/dist/transport/udp.js +994 -0
- package/dist/transport/udp.js.map +1 -0
- package/dist/transport.d.ts +3 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +2 -0
- package/dist/transport.js.map +1 -0
- package/docs/api-reference.md +536 -0
- package/docs/blockwise.md +117 -0
- package/docs/observability.md +206 -0
- package/docs/observe.md +203 -0
- package/docs/releasing.md +106 -0
- package/docs/router.md +170 -0
- package/docs/store.md +212 -0
- package/docs/tcp-transport.md +89 -0
- package/docs/udp-transport.md +81 -0
- package/examples/slice-1.ts +12 -0
- package/examples/slice-2.ts +96 -0
- package/examples/slice-3-psk.ts +70 -0
- package/examples/slice-3-x509.ts +85 -0
- package/examples/slice-4.ts +45 -0
- package/examples/slice-5-firmware.ts +207 -0
- package/examples/slice-6-temperature.ts +163 -0
- package/examples/slice-7-file-store.ts +92 -0
- package/examples/slice-7-otel.ts +123 -0
- package/package.json +73 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import tls from 'node:tls';
|
|
2
|
+
import { createServer, Response, stringToBytes, encodeMessage, decodeMessage, stringToCode } from '@sirena-lwm2m/coap';
|
|
3
|
+
|
|
4
|
+
const PSK_IDENTITY = 'device-001';
|
|
5
|
+
const PSK_KEY = Buffer.from('s3cr3t');
|
|
6
|
+
|
|
7
|
+
const server = createServer({
|
|
8
|
+
listen: ['coaps://127.0.0.1:5684'],
|
|
9
|
+
security: {
|
|
10
|
+
mode: 'psk',
|
|
11
|
+
psk: (identity) => identity === PSK_IDENTITY ? PSK_KEY : null,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
server.on('request', async (_req, ctx) => {
|
|
16
|
+
console.log('ctx.peer:', ctx.peer);
|
|
17
|
+
const identity = ctx.peer.mode === 'psk' ? ctx.peer.identity : 'unknown';
|
|
18
|
+
await ctx.respond(Response.ok(stringToBytes(identity)));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await server.start();
|
|
22
|
+
console.log('Server listening on:', server.listenerSummary());
|
|
23
|
+
|
|
24
|
+
const port = server.listenerSummary()[0].port;
|
|
25
|
+
|
|
26
|
+
function pskGet(host: string, port: number): Promise<string> {
|
|
27
|
+
const packet = Buffer.from(encodeMessage({
|
|
28
|
+
version: 1,
|
|
29
|
+
type: 'CON',
|
|
30
|
+
token: new Uint8Array([0x01]),
|
|
31
|
+
code: stringToCode('0.01'),
|
|
32
|
+
messageId: 1,
|
|
33
|
+
options: [],
|
|
34
|
+
payload: new Uint8Array(0),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const sock = tls.connect({
|
|
39
|
+
host,
|
|
40
|
+
port,
|
|
41
|
+
pskCallback: () => ({ psk: PSK_KEY, identity: PSK_IDENTITY }),
|
|
42
|
+
rejectUnauthorized: false,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const timer = setTimeout(() => { sock.destroy(); reject(new Error('timeout')); }, 5000);
|
|
46
|
+
|
|
47
|
+
sock.once('secureConnect', () => {
|
|
48
|
+
sock.write(packet);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
sock.once('data', (chunk: Buffer) => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
sock.destroy();
|
|
54
|
+
const decoded = decodeMessage(new Uint8Array(chunk));
|
|
55
|
+
resolve(Buffer.from(decoded.payload).toString('utf8'));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
sock.once('error', (err) => { clearTimeout(timer); reject(err); });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const body = await pskGet('127.0.0.1', port);
|
|
63
|
+
console.log('Response body:', body);
|
|
64
|
+
|
|
65
|
+
if (body !== PSK_IDENTITY) {
|
|
66
|
+
throw new Error(`Expected '${PSK_IDENTITY}', got '${body}'`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await server.stop();
|
|
70
|
+
console.log('Server stopped cleanly.');
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import tls from 'node:tls';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createServer, Response, stringToBytes, stringToCode } from '@sirena-lwm2m/coap';
|
|
6
|
+
import { encodeTcpFrame, decodeTcpFrame } from '../dist/transport/index.js';
|
|
7
|
+
|
|
8
|
+
const FIXTURES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'test', 'fixtures', 'tls');
|
|
9
|
+
|
|
10
|
+
const CA_CERT = fs.readFileSync(path.join(FIXTURES_DIR, 'ca.cert.pem'));
|
|
11
|
+
const SERVER_KEY = fs.readFileSync(path.join(FIXTURES_DIR, 'server.key.pem'));
|
|
12
|
+
const SERVER_CERT = fs.readFileSync(path.join(FIXTURES_DIR, 'server.cert.pem'));
|
|
13
|
+
const CLIENT_KEY = fs.readFileSync(path.join(FIXTURES_DIR, 'client.key.pem'));
|
|
14
|
+
const CLIENT_CERT = fs.readFileSync(path.join(FIXTURES_DIR, 'client.cert.pem'));
|
|
15
|
+
|
|
16
|
+
const server = createServer({
|
|
17
|
+
listen: ['coaps+tcp://127.0.0.1:5685'],
|
|
18
|
+
security: {
|
|
19
|
+
mode: 'x509',
|
|
20
|
+
key: SERVER_KEY,
|
|
21
|
+
cert: SERVER_CERT,
|
|
22
|
+
ca: CA_CERT,
|
|
23
|
+
requestClientCert: true,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
server.on('request', async (_req, ctx) => {
|
|
28
|
+
console.log('ctx.peer:', ctx.peer);
|
|
29
|
+
await ctx.respond(Response.ok(stringToBytes('authenticated')));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await server.start();
|
|
33
|
+
console.log('Server listening on:', server.listenerSummary());
|
|
34
|
+
|
|
35
|
+
const port = server.listenerSummary()[0].port;
|
|
36
|
+
|
|
37
|
+
function x509Get(host: string, port: number): Promise<string> {
|
|
38
|
+
const frame = Buffer.from(encodeTcpFrame({
|
|
39
|
+
code: stringToCode('0.01'),
|
|
40
|
+
token: new Uint8Array([0x01]),
|
|
41
|
+
options: [],
|
|
42
|
+
payload: new Uint8Array(0),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const sock = tls.connect({
|
|
47
|
+
host,
|
|
48
|
+
port,
|
|
49
|
+
key: CLIENT_KEY,
|
|
50
|
+
cert: CLIENT_CERT,
|
|
51
|
+
ca: CA_CERT,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const timer = setTimeout(() => { sock.destroy(); reject(new Error('timeout')); }, 5000);
|
|
55
|
+
|
|
56
|
+
sock.once('secureConnect', () => {
|
|
57
|
+
sock.write(frame);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let buf = Buffer.alloc(0);
|
|
61
|
+
const onData = (chunk: Buffer) => {
|
|
62
|
+
buf = Buffer.concat([buf, chunk]);
|
|
63
|
+
const result = decodeTcpFrame(buf);
|
|
64
|
+
if (result !== null && result.frame.payload.length > 0) {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
sock.removeListener('data', onData);
|
|
67
|
+
sock.destroy();
|
|
68
|
+
resolve(Buffer.from(result.frame.payload).toString('utf8'));
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
sock.on('data', onData);
|
|
72
|
+
|
|
73
|
+
sock.once('error', (err) => { clearTimeout(timer); reject(err); });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const body = await x509Get('127.0.0.1', port);
|
|
78
|
+
console.log('Response body:', body);
|
|
79
|
+
|
|
80
|
+
if (body !== 'authenticated') {
|
|
81
|
+
throw new Error(`Expected 'authenticated', got '${body}'`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await server.stop();
|
|
85
|
+
console.log('Server stopped cleanly.');
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createServer, stringToCode, Response, stringToBytes } from '@sirena-lwm2m/coap';
|
|
2
|
+
import { createApp } from '@sirena-lwm2m/coap/router';
|
|
3
|
+
import type { Middleware } from '@sirena-lwm2m/coap/router';
|
|
4
|
+
|
|
5
|
+
// Custom option number for Authorization (non-critical, safe to use in demo)
|
|
6
|
+
const OPTION_AUTHORIZATION = 65000;
|
|
7
|
+
|
|
8
|
+
const server = createServer({
|
|
9
|
+
listen: [{ host: '127.0.0.1', port: 5683 }],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const router = createApp({ server, discovery: true });
|
|
13
|
+
|
|
14
|
+
// Auth middleware: rejects requests missing the Authorization option with 4.01
|
|
15
|
+
const authMiddleware: Middleware = (req, ctx, next) => {
|
|
16
|
+
const authOpt = ctx.options.find(o => o.number === OPTION_AUTHORIZATION);
|
|
17
|
+
if (authOpt === undefined) {
|
|
18
|
+
ctx.abort(stringToCode('4.01'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
return next();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
router.use(authMiddleware);
|
|
25
|
+
|
|
26
|
+
router.add({
|
|
27
|
+
path: '/3/{iid}',
|
|
28
|
+
method: 'GET',
|
|
29
|
+
handler: (_req, ctx) => {
|
|
30
|
+
ctx.respond(new Response(stringToCode('2.05'), stringToBytes('device-object')));
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
router.add({
|
|
35
|
+
path: '/3/{iid}/{rid}',
|
|
36
|
+
method: 'PUT',
|
|
37
|
+
handler: (_req, ctx) => {
|
|
38
|
+
ctx.respond(new Response(stringToCode('2.04')));
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await server.start();
|
|
43
|
+
console.log('CoAP server listening on:', server.listenerSummary());
|
|
44
|
+
console.log('Registered routes: GET /3/{iid}, PUT /3/{iid}/{rid}');
|
|
45
|
+
console.log('Discovery enabled at /.well-known/core');
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import dgram from 'node:dgram';
|
|
2
|
+
import { createServer, stringToCode, Response } from '@sirena-lwm2m/coap';
|
|
3
|
+
import { createApp } from '@sirena-lwm2m/coap/router';
|
|
4
|
+
import { blockwiseMiddleware } from '@sirena-lwm2m/coap/blockwise';
|
|
5
|
+
import { encodeMessage, decodeMessage } from '@sirena-lwm2m/coap';
|
|
6
|
+
import { OptionNumber, uintToBytes } from '@sirena-lwm2m/coap';
|
|
7
|
+
import { encodeBlockOption, decodeBlockOption } from '@sirena-lwm2m/coap/blockwise';
|
|
8
|
+
|
|
9
|
+
const PORT = 5685;
|
|
10
|
+
const HOST = '127.0.0.1';
|
|
11
|
+
|
|
12
|
+
// ── Server ────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const server = createServer({
|
|
15
|
+
listen: [{ host: HOST, port: PORT }],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const router = createApp({ server });
|
|
19
|
+
|
|
20
|
+
router.use(blockwiseMiddleware());
|
|
21
|
+
|
|
22
|
+
// PUT /firmware — handler only sees the fully-assembled body
|
|
23
|
+
router.add({
|
|
24
|
+
path: '/firmware',
|
|
25
|
+
method: 'PUT',
|
|
26
|
+
handler: async (req, ctx) => {
|
|
27
|
+
let byteCount = 0;
|
|
28
|
+
if (req.body) {
|
|
29
|
+
const reader = req.body.getReader();
|
|
30
|
+
while (true) {
|
|
31
|
+
const { done, value } = await reader.read();
|
|
32
|
+
if (done) break;
|
|
33
|
+
byteCount += value?.length ?? 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
console.log(`firmware bytes received: ${byteCount}`);
|
|
37
|
+
await ctx.respond(new Response(stringToCode('2.04')));
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// GET /manifest — 500-byte JSON manifest, chunked transparently via Block2
|
|
42
|
+
const MANIFEST = Buffer.from(
|
|
43
|
+
JSON.stringify({ version: '1.2.3', size: 8_388_608, crc32: 'deadbeef' }).padEnd(500, ' '),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
router.add({
|
|
47
|
+
path: '/manifest',
|
|
48
|
+
method: 'GET',
|
|
49
|
+
handler: async (_req, ctx) => {
|
|
50
|
+
await ctx.respond(new Response(stringToCode('2.05'), MANIFEST));
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await server.start();
|
|
55
|
+
console.log('CoAP server listening on:', server.listenerSummary());
|
|
56
|
+
|
|
57
|
+
// ── Self-test client ──────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
// CON PUT with Block1 option — sends one UDP datagram and awaits ACK/response
|
|
60
|
+
function sendBlock1(
|
|
61
|
+
sock: dgram.Socket,
|
|
62
|
+
token: Uint8Array,
|
|
63
|
+
msgId: number,
|
|
64
|
+
num: number,
|
|
65
|
+
szx: number,
|
|
66
|
+
m: boolean,
|
|
67
|
+
chunk: Uint8Array,
|
|
68
|
+
): Promise<{ code: number; options: { number: number; value: Uint8Array }[] }> {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const uriPathBytes = new TextEncoder().encode('firmware');
|
|
71
|
+
const packet = encodeMessage({
|
|
72
|
+
version: 1,
|
|
73
|
+
type: 'CON',
|
|
74
|
+
token,
|
|
75
|
+
code: stringToCode('0.03'), // PUT
|
|
76
|
+
messageId: msgId,
|
|
77
|
+
options: [
|
|
78
|
+
{ number: OptionNumber.UriPath, value: uriPathBytes },
|
|
79
|
+
{ number: OptionNumber.Block1, value: encodeBlockOption({ num, szx, m }) },
|
|
80
|
+
],
|
|
81
|
+
payload: chunk,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const timer = setTimeout(() => reject(new Error(`Block1 NUM=${num} timeout`)), 5000);
|
|
85
|
+
|
|
86
|
+
const onMessage = (msg: Buffer) => {
|
|
87
|
+
const decoded = decodeMessage(msg);
|
|
88
|
+
if (decoded.messageId === msgId) {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
sock.off('message', onMessage);
|
|
91
|
+
resolve({ code: decoded.code, options: decoded.options });
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
sock.on('message', onMessage);
|
|
96
|
+
sock.send(packet, PORT, HOST);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// GET with Block2 option — requests a specific block number
|
|
101
|
+
function sendBlock2Get(
|
|
102
|
+
sock: dgram.Socket,
|
|
103
|
+
token: Uint8Array,
|
|
104
|
+
msgId: number,
|
|
105
|
+
path: string,
|
|
106
|
+
num: number,
|
|
107
|
+
szx: number,
|
|
108
|
+
): Promise<{ code: number; payload: Uint8Array; m: boolean; options: { number: number; value: Uint8Array }[] }> {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const uriPathBytes = new TextEncoder().encode(path);
|
|
111
|
+
const packet = encodeMessage({
|
|
112
|
+
version: 1,
|
|
113
|
+
type: 'CON',
|
|
114
|
+
token,
|
|
115
|
+
code: stringToCode('0.01'), // GET
|
|
116
|
+
messageId: msgId,
|
|
117
|
+
options: [
|
|
118
|
+
{ number: OptionNumber.UriPath, value: uriPathBytes },
|
|
119
|
+
{ number: OptionNumber.Block2, value: encodeBlockOption({ num, szx, m: false }) },
|
|
120
|
+
],
|
|
121
|
+
payload: new Uint8Array(0),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const timer = setTimeout(() => reject(new Error(`Block2 NUM=${num} timeout`)), 5000);
|
|
125
|
+
|
|
126
|
+
const onMessage = (msg: Buffer) => {
|
|
127
|
+
const decoded = decodeMessage(msg);
|
|
128
|
+
if (decoded.messageId === msgId) {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
sock.off('message', onMessage);
|
|
131
|
+
const block2Opt = decoded.options.find(o => o.number === OptionNumber.Block2);
|
|
132
|
+
const m = block2Opt ? decodeBlockOption(block2Opt.value).m : false;
|
|
133
|
+
resolve({ code: decoded.code, payload: decoded.payload, m, options: decoded.options });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
sock.on('message', onMessage);
|
|
138
|
+
sock.send(packet, PORT, HOST);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const sock = dgram.createSocket('udp4');
|
|
143
|
+
await new Promise<void>((resolve) => sock.bind(0, HOST, resolve));
|
|
144
|
+
|
|
145
|
+
// Test 1: 8 MiB Block1 PUT /firmware
|
|
146
|
+
const FIRMWARE_SIZE = 8 * 1024 * 1024; // 8 MiB
|
|
147
|
+
const firmware = Buffer.alloc(FIRMWARE_SIZE, 0xab);
|
|
148
|
+
|
|
149
|
+
const SZX = 6; // block size = 2^(6+4) = 1024 bytes
|
|
150
|
+
const BLOCK_SIZE = 1024;
|
|
151
|
+
const totalChunks = Math.ceil(FIRMWARE_SIZE / BLOCK_SIZE);
|
|
152
|
+
|
|
153
|
+
const block1Token = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
|
|
154
|
+
let msgId = 1;
|
|
155
|
+
|
|
156
|
+
console.log(`Uploading ${FIRMWARE_SIZE} bytes in ${totalChunks} × ${BLOCK_SIZE}-byte blocks…`);
|
|
157
|
+
|
|
158
|
+
for (let num = 0; num < totalChunks; num++) {
|
|
159
|
+
const start = num * BLOCK_SIZE;
|
|
160
|
+
const end = Math.min(start + BLOCK_SIZE, FIRMWARE_SIZE);
|
|
161
|
+
const chunk = firmware.subarray(start, end);
|
|
162
|
+
const m = num < totalChunks - 1;
|
|
163
|
+
|
|
164
|
+
const resp = await sendBlock1(sock, block1Token, msgId++, num, SZX, m, chunk);
|
|
165
|
+
|
|
166
|
+
if (m) {
|
|
167
|
+
// Expect 2.31 Continue (0x5f)
|
|
168
|
+
if (resp.code !== 0x5f) {
|
|
169
|
+
throw new Error(`Expected 2.31 Continue for chunk ${num}, got 0x${resp.code.toString(16)}`);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
// Final chunk — expect 2.04 Changed (0x44)
|
|
173
|
+
if (resp.code !== stringToCode('2.04')) {
|
|
174
|
+
throw new Error(`Expected 2.04 Changed for final chunk, got 0x${resp.code.toString(16)}`);
|
|
175
|
+
}
|
|
176
|
+
console.log('Block1 PUT /firmware: 2.04 Changed ✓');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Test 2: Block2 GET /manifest — reassemble all chunks
|
|
181
|
+
const block2Token = new Uint8Array([0x05, 0x06, 0x07, 0x08]);
|
|
182
|
+
const manifestChunks: Uint8Array[] = [];
|
|
183
|
+
let blockNum = 0;
|
|
184
|
+
const GET_SZX = 5; // 512-byte blocks for manifest
|
|
185
|
+
|
|
186
|
+
while (true) {
|
|
187
|
+
const resp = await sendBlock2Get(sock, block2Token, msgId++, 'manifest', blockNum, GET_SZX);
|
|
188
|
+
if (resp.code !== stringToCode('2.05')) {
|
|
189
|
+
throw new Error(`Expected 2.05 Content for manifest block ${blockNum}, got 0x${resp.code.toString(16)}`);
|
|
190
|
+
}
|
|
191
|
+
manifestChunks.push(resp.payload);
|
|
192
|
+
if (!resp.m) break;
|
|
193
|
+
blockNum++;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const manifestBytes = Buffer.concat(manifestChunks);
|
|
197
|
+
if (manifestBytes.length !== MANIFEST.length) {
|
|
198
|
+
throw new Error(`Manifest length mismatch: expected ${MANIFEST.length}, got ${manifestBytes.length}`);
|
|
199
|
+
}
|
|
200
|
+
if (!manifestBytes.equals(MANIFEST)) {
|
|
201
|
+
throw new Error('Manifest content mismatch');
|
|
202
|
+
}
|
|
203
|
+
console.log(`Block2 GET /manifest: ${manifestBytes.length} bytes received, content verified ✓`);
|
|
204
|
+
|
|
205
|
+
sock.close();
|
|
206
|
+
await server.stop();
|
|
207
|
+
console.log('All self-tests passed.');
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// RFC 7641 Observe — temperature sensor example
|
|
2
|
+
//
|
|
3
|
+
// Server: publishes /temperature as an observable resource (fake sensor, updates every 500 ms).
|
|
4
|
+
// Client: registers via GET+Observe:0, logs 5 notifications, then deregisters via GET+Observe:1.
|
|
5
|
+
//
|
|
6
|
+
// Run: node --experimental-strip-types examples/slice-6-temperature.ts
|
|
7
|
+
|
|
8
|
+
import dgram from 'node:dgram';
|
|
9
|
+
import { createServer, Response } from '@sirena-lwm2m/coap';
|
|
10
|
+
import { createApp } from '@sirena-lwm2m/coap/router';
|
|
11
|
+
import { encodeMessage, decodeMessage, stringToCode } from '@sirena-lwm2m/coap';
|
|
12
|
+
import { OptionNumber, uintToBytes, bytesToUint } from '@sirena-lwm2m/coap';
|
|
13
|
+
import type { Observable, Observer, Unsubscribe } from '@sirena-lwm2m/coap';
|
|
14
|
+
|
|
15
|
+
const PORT = 5686;
|
|
16
|
+
const HOST = '127.0.0.1';
|
|
17
|
+
|
|
18
|
+
// ── Observable temperature subject ───────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function makeSubject<T>(): { observable: Observable<T>; next(v: T): void; complete(): void } {
|
|
21
|
+
const subscribers = new Set<Observer<T>>();
|
|
22
|
+
return {
|
|
23
|
+
observable: {
|
|
24
|
+
subscribe(observer: Observer<T>): Unsubscribe {
|
|
25
|
+
subscribers.add(observer);
|
|
26
|
+
return () => { subscribers.delete(observer); };
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
next(v: T) {
|
|
30
|
+
for (const obs of subscribers) obs.next(v);
|
|
31
|
+
},
|
|
32
|
+
complete() {
|
|
33
|
+
for (const obs of subscribers) obs.complete?.();
|
|
34
|
+
subscribers.clear();
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Server ────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const server = createServer({ listen: [{ host: HOST, port: PORT }] });
|
|
42
|
+
const router = createApp({ server });
|
|
43
|
+
|
|
44
|
+
const tempSubject = makeSubject<Uint8Array>();
|
|
45
|
+
|
|
46
|
+
router.add({
|
|
47
|
+
path: '/temperature',
|
|
48
|
+
method: 'GET',
|
|
49
|
+
handler: async (_req, ctx) => {
|
|
50
|
+
const initialReading = encodeTemperature(20.0);
|
|
51
|
+
await ctx.respond(Response.ok(initialReading));
|
|
52
|
+
// ctx.observe() is a no-op for non-Observe GET requests; safe to call unconditionally
|
|
53
|
+
ctx.observe(tempSubject.observable);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await server.start();
|
|
58
|
+
console.log('CoAP server listening on', server.listenerSummary());
|
|
59
|
+
|
|
60
|
+
// ── Fake temperature sensor (updates every 500 ms) ────────────────────────────
|
|
61
|
+
|
|
62
|
+
function encodeTemperature(celsius: number): Uint8Array {
|
|
63
|
+
const buf = Buffer.alloc(4);
|
|
64
|
+
buf.writeFloatBE(celsius, 0);
|
|
65
|
+
return buf;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function decodeTemperature(payload: Uint8Array): number {
|
|
69
|
+
return Buffer.from(payload).readFloatBE(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let temp = 20.0;
|
|
73
|
+
const sensorInterval = setInterval(() => {
|
|
74
|
+
temp += (Math.random() - 0.5) * 0.5;
|
|
75
|
+
tempSubject.next(encodeTemperature(temp));
|
|
76
|
+
}, 500);
|
|
77
|
+
|
|
78
|
+
// ── Client ────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
const sock = dgram.createSocket('udp4');
|
|
81
|
+
await new Promise<void>((resolve) => sock.bind(0, HOST, resolve));
|
|
82
|
+
|
|
83
|
+
const TOKEN = new Uint8Array([0xca, 0xfe, 0xbe, 0xef]);
|
|
84
|
+
let msgId = 100;
|
|
85
|
+
|
|
86
|
+
function buildObservePacket(deregister: boolean): Uint8Array {
|
|
87
|
+
// Options must be in ascending numeric order per RFC 7252.
|
|
88
|
+
// Observe = 6, UriPath = 11 — Observe goes first.
|
|
89
|
+
return encodeMessage({
|
|
90
|
+
version: 1,
|
|
91
|
+
type: 'CON',
|
|
92
|
+
token: TOKEN,
|
|
93
|
+
code: stringToCode('0.01'), // GET
|
|
94
|
+
messageId: msgId++,
|
|
95
|
+
options: [
|
|
96
|
+
{ number: OptionNumber.Observe, value: uintToBytes(deregister ? 1 : 0) },
|
|
97
|
+
{ number: OptionNumber.UriPath, value: new TextEncoder().encode('temperature') },
|
|
98
|
+
],
|
|
99
|
+
payload: new Uint8Array(0),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sendAck(messageId: number): void {
|
|
104
|
+
const ack = encodeMessage({
|
|
105
|
+
version: 1,
|
|
106
|
+
type: 'ACK',
|
|
107
|
+
token: TOKEN,
|
|
108
|
+
code: stringToCode('0.00'), // Empty ACK
|
|
109
|
+
messageId,
|
|
110
|
+
options: [],
|
|
111
|
+
payload: new Uint8Array(0),
|
|
112
|
+
});
|
|
113
|
+
sock.send(ack, PORT, HOST);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Register (GET + Observe:0)
|
|
117
|
+
const registerPacket = buildObservePacket(false);
|
|
118
|
+
sock.send(registerPacket, PORT, HOST);
|
|
119
|
+
console.log('Client: sent Observe registration (GET + Observe:0)');
|
|
120
|
+
|
|
121
|
+
// Collect 5 notifications then deregister
|
|
122
|
+
let notificationCount = 0;
|
|
123
|
+
|
|
124
|
+
await new Promise<void>((resolve, reject) => {
|
|
125
|
+
const timeout = setTimeout(() => reject(new Error('Timed out waiting for 5 notifications')), 15_000);
|
|
126
|
+
|
|
127
|
+
sock.on('message', (msg: Buffer) => {
|
|
128
|
+
const decoded = decodeMessage(msg);
|
|
129
|
+
|
|
130
|
+
// ACK any CON notification so the server can send the next one
|
|
131
|
+
if (decoded.type === 'CON') {
|
|
132
|
+
sendAck(decoded.messageId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const observeOpt = decoded.options.find(o => o.number === OptionNumber.Observe);
|
|
136
|
+
if (observeOpt === undefined) return; // skip non-Observe responses
|
|
137
|
+
|
|
138
|
+
const seq = bytesToUint(observeOpt.value);
|
|
139
|
+
const tempC = decodeTemperature(decoded.payload);
|
|
140
|
+
notificationCount++;
|
|
141
|
+
|
|
142
|
+
console.log(`Notification #${notificationCount}: seq=${seq}, temp=${tempC.toFixed(2)}°C`);
|
|
143
|
+
|
|
144
|
+
if (notificationCount === 5) {
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
resolve();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Deregister (GET + Observe:1)
|
|
152
|
+
const deregisterPacket = buildObservePacket(true);
|
|
153
|
+
sock.send(deregisterPacket, PORT, HOST);
|
|
154
|
+
console.log('Client: sent deregistration (GET + Observe:1)');
|
|
155
|
+
|
|
156
|
+
// Brief pause to allow the server to process the deregistration, then clean up
|
|
157
|
+
await new Promise(r => setTimeout(r, 300));
|
|
158
|
+
|
|
159
|
+
clearInterval(sensorInterval);
|
|
160
|
+
tempSubject.complete();
|
|
161
|
+
sock.close();
|
|
162
|
+
await server.stop();
|
|
163
|
+
console.log('Done — received 5 notifications and deregistered cleanly.');
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Slice 7a — SqliteStore example with file-backed dedup store
|
|
2
|
+
//
|
|
3
|
+
// Demonstrates a CoAP server that uses SqliteStore for durable transaction
|
|
4
|
+
// deduplication across restarts. The SQLite database is stored on disk so
|
|
5
|
+
// the server can survive a crash and still correctly dedup retransmissions
|
|
6
|
+
// that arrived before the restart.
|
|
7
|
+
//
|
|
8
|
+
// Run: node --experimental-strip-types examples/slice-7-file-store.ts
|
|
9
|
+
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import dgram from 'node:dgram';
|
|
14
|
+
import { createServer, Response, encodeMessage, decodeMessage, stringToCode } from '@sirena-lwm2m/coap';
|
|
15
|
+
import { createSqliteStore } from '@sirena-lwm2m/coap/store';
|
|
16
|
+
|
|
17
|
+
// ── Persistent SQLite store ───────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const dbDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sirena-coap-'));
|
|
20
|
+
const dbPath = path.join(dbDir, 'dedup.db');
|
|
21
|
+
|
|
22
|
+
console.log(`Using SQLite dedup store at: ${dbPath}`);
|
|
23
|
+
|
|
24
|
+
const store = await createSqliteStore({ path: dbPath });
|
|
25
|
+
|
|
26
|
+
// ── Server ────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const server = createServer({
|
|
29
|
+
listen: ['coap://127.0.0.1:5690'],
|
|
30
|
+
store,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let requestCount = 0;
|
|
34
|
+
|
|
35
|
+
server.on('request', async (_req, ctx) => {
|
|
36
|
+
requestCount++;
|
|
37
|
+
console.log(`[server] request #${requestCount} from ${ctx.remote}`);
|
|
38
|
+
await ctx.respond(Response.ok(new TextEncoder().encode(`hello #${requestCount}`)));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await server.start();
|
|
42
|
+
console.log('Server started:', server.listenerSummary());
|
|
43
|
+
console.log('Store summary:', server.storeSummary());
|
|
44
|
+
|
|
45
|
+
// ── Client: send same CON twice (dedup demo) ──────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const sock = dgram.createSocket('udp4');
|
|
48
|
+
await new Promise<void>((resolve) => sock.bind(0, '127.0.0.1', resolve));
|
|
49
|
+
|
|
50
|
+
const TOKEN = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
|
|
51
|
+
const MID = 0x1234;
|
|
52
|
+
|
|
53
|
+
const request = encodeMessage({
|
|
54
|
+
version: 1,
|
|
55
|
+
type: 'CON',
|
|
56
|
+
token: TOKEN,
|
|
57
|
+
code: stringToCode('0.01'), // GET
|
|
58
|
+
messageId: MID,
|
|
59
|
+
options: [],
|
|
60
|
+
payload: new Uint8Array(0),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const responses: string[] = [];
|
|
64
|
+
|
|
65
|
+
sock.on('message', (msg) => {
|
|
66
|
+
const decoded = decodeMessage(msg);
|
|
67
|
+
responses.push(new TextDecoder().decode(decoded.payload));
|
|
68
|
+
console.log(`[client] received: ${responses[responses.length - 1]}`);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// First transmission
|
|
72
|
+
sock.send(request, 5690, '127.0.0.1');
|
|
73
|
+
console.log('[client] sent CON request (first)');
|
|
74
|
+
await new Promise(r => setTimeout(r, 50));
|
|
75
|
+
|
|
76
|
+
// Retransmission of same MID — server should dedup and replay the cached response
|
|
77
|
+
sock.send(request, 5690, '127.0.0.1');
|
|
78
|
+
console.log('[client] retransmitted same CON request (should be deduped)');
|
|
79
|
+
await new Promise(r => setTimeout(r, 50));
|
|
80
|
+
|
|
81
|
+
console.log(`\nResult: ${requestCount} handler invocation(s) for 2 transmissions`);
|
|
82
|
+
console.log(requestCount === 1
|
|
83
|
+
? '✓ CON retransmission correctly deduped via SqliteStore'
|
|
84
|
+
: '✗ Dedup failed — handler was invoked more than once');
|
|
85
|
+
|
|
86
|
+
// ── Cleanup ───────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
sock.close();
|
|
89
|
+
store.close();
|
|
90
|
+
await server.stop();
|
|
91
|
+
fs.rmSync(dbDir, { recursive: true, force: true });
|
|
92
|
+
console.log('\nDone.');
|