@oesp/transport-ble-gatt 1.0.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/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @oesp/transport-ble-gatt
2
+
3
+ Transport OESP via BLE GATT (Central <-> Peripheral) avec support asynchrone et fragmentation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @oesp/transport-ble-gatt
9
+ ```
10
+
11
+ ## Fonctionnalités
12
+
13
+ - **Communication P2P Offline** : Échange de données sans internet.
14
+ - **Fragmentation Automatique** : Gestion transparente du MTU BLE.
15
+ - **Protocole Stop-and-Wait** : Fiabilité accrue via acquittements (ACK).
16
+ - **Opérations Asynchrones** : Utilisation de `Promise` pour toutes les opérations I/O.
17
+ - **Crypto Agnostique** : Injection de la fonction de hachage SHA-256 pour compatibilité multi-plateforme (Node.js, React Native, Browser).
18
+
19
+ ## Utilisation (Central)
20
+
21
+ Le mode Central est généralement utilisé par l'application mobile qui scanne et se connecte au périphérique.
22
+
23
+ ```ts
24
+ import { OESPBleGattTransport } from '@oesp/transport-ble-gatt';
25
+ import { MyBleLink } from './MyBleLink'; // Votre implémentation de BleGattLink
26
+ import { sha256 } from 'js-sha256'; // Ou autre provider
27
+
28
+ // 1. Initialiser le transport avec le provider SHA-256
29
+ const transport = new OESPBleGattTransport({
30
+ sha256: async (data) => new Uint8Array(sha256.arrayBuffer(data))
31
+ });
32
+
33
+ const link = new MyBleLink();
34
+
35
+ try {
36
+ // 2. Connexion (géré par votre implémentation Link)
37
+ await link.connect('DEVICE_UUID');
38
+
39
+ // 3. Envoi d'un token OESP de manière asynchrone
40
+ // La méthode gère la fragmentation et les ACK automatiquement
41
+ await transport.sendToken(oespTokenBytes, link);
42
+ console.log("Token envoyé avec succès !");
43
+
44
+ } catch (err) {
45
+ console.error("Erreur de transport:", err);
46
+ }
47
+ ```
48
+
49
+ ## Utilisation (Peripheral)
50
+
51
+ Le mode Peripheral est utilisé par l'appareil IoT ou le receveur.
52
+
53
+ ```ts
54
+ const transport = new OESPBleGattTransport({
55
+ sha256: async (data) => new Uint8Array(sha256.arrayBuffer(data))
56
+ });
57
+ const link = new MyBleLink(); // Implémentation côté périphérique
58
+
59
+ // Boucle de réception asynchrone
60
+ transport.receiveLoop(link, async (token) => {
61
+ console.log("Token OESP complet reçu:", token);
62
+ // Traitement du token...
63
+ });
64
+ ```
65
+
66
+ ## Interface `BleGattLink`
67
+
68
+ Vous devez fournir une implémentation de l'interface `BleGattLink` adaptée à votre environnement (ex: `react-native-ble-plx` ou `noble`).
69
+
70
+ ```ts
71
+ interface BleGattLink {
72
+ // MTU négocié (doit être > 23 pour de meilleures performances)
73
+ mtu: number;
74
+
75
+ // Écrit des données sur la caractéristique RX du pair
76
+ write(data: Uint8Array): Promise<void>;
77
+
78
+ // Lit des données depuis la caractéristique TX du pair
79
+ read(): Promise<Uint8Array>;
80
+
81
+ // Ferme la connexion
82
+ close(): Promise<void>;
83
+ }
84
+ ```
85
+
86
+ ## Notes Techniques
87
+
88
+ - **MTU & Fragmentation** : Le transport découpe les messages OESP (souvent > MTU) en trames. Assurez-vous que `link.mtu` reflète le MTU réel négocié.
89
+ - **Timeout** : Un timeout est appliqué si un ACK n'est pas reçu à temps.
90
+ - **Android** : Nécessite les permissions `BLUETOOTH_SCAN` et `BLUETOOTH_CONNECT`.
package/dist/index.cjs ADDED
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ MockBleGattLink: () => MockBleGattLink,
24
+ OESPBleGattTransport: () => OESPBleGattTransport,
25
+ OESP_BLE_CHAR_META_UUID: () => OESP_BLE_CHAR_META_UUID,
26
+ OESP_BLE_CHAR_RX_UUID: () => OESP_BLE_CHAR_RX_UUID,
27
+ OESP_BLE_CHAR_TX_UUID: () => OESP_BLE_CHAR_TX_UUID,
28
+ OESP_BLE_SERVICE_UUID: () => OESP_BLE_SERVICE_UUID
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/frames.ts
33
+ var OESP_BLE_SERVICE_UUID = "e95f1234-5678-4321-8765-abcdef012345";
34
+ var OESP_BLE_CHAR_RX_UUID = "e95f1235-5678-4321-8765-abcdef012345";
35
+ var OESP_BLE_CHAR_TX_UUID = "e95f1236-5678-4321-8765-abcdef012345";
36
+ var OESP_BLE_CHAR_META_UUID = "e95f1237-5678-4321-8765-abcdef012345";
37
+
38
+ // src/OESPBleGattTransport.ts
39
+ var import_core = require("@oesp/core");
40
+ var OESPBleGattTransport = class {
41
+ constructor(opts) {
42
+ this.maxChunkBytes = opts.maxChunkBytes || 1024;
43
+ this.timeoutMs = opts.timeoutMs || 3e3;
44
+ this.retries = opts.retries || 3;
45
+ this.sha256 = opts.sha256;
46
+ }
47
+ /**
48
+ * Envoie un token OESP via BLE GATT (Central -> Peripheral)
49
+ */
50
+ async sendToken(token, link, sid = Math.random().toString(36).substring(7)) {
51
+ const tokenBytes = new TextEncoder().encode(token);
52
+ const shaBytes = await this.sha256(tokenBytes);
53
+ const sha256 = (0, import_core.base64Encode)(shaBytes);
54
+ const totalLen = tokenBytes.length;
55
+ const chunks = this.fragment(tokenBytes, this.maxChunkBytes);
56
+ const parts = chunks.length;
57
+ const startFrame = {
58
+ t: "START",
59
+ sid,
60
+ mid: Math.random().toString(36).substring(7),
61
+ totalLen,
62
+ parts,
63
+ sha256
64
+ };
65
+ await this.sendFrameWithAck(link, startFrame, -1);
66
+ for (let i = 0; i < chunks.length; i++) {
67
+ const chunkFrame = {
68
+ t: "CHUNK",
69
+ sid,
70
+ seq: i,
71
+ data: (0, import_core.base64Encode)(chunks[i])
72
+ };
73
+ await this.sendFrameWithAck(link, chunkFrame, i);
74
+ }
75
+ await this.sendFrameWithAck(link, { t: "END", sid }, -1);
76
+ }
77
+ /**
78
+ * Boucle de réception pour le Peripheral (GATT Server)
79
+ */
80
+ receiveLoop(link, onToken) {
81
+ let currentSession = null;
82
+ link.onTxNotify(async (data) => {
83
+ try {
84
+ const frame = JSON.parse(new TextDecoder().decode(data));
85
+ switch (frame.t) {
86
+ case "START":
87
+ currentSession = {
88
+ sid: frame.sid,
89
+ expectedSha: frame.sha256,
90
+ expectedParts: frame.parts,
91
+ chunks: new Array(frame.parts),
92
+ receivedParts: /* @__PURE__ */ new Set()
93
+ };
94
+ await this.sendAck(link, frame.sid, -1);
95
+ break;
96
+ case "CHUNK":
97
+ if (currentSession && currentSession.sid === frame.sid) {
98
+ const chunkData = (0, import_core.base64Decode)(frame.data);
99
+ currentSession.chunks[frame.seq] = chunkData;
100
+ currentSession.receivedParts.add(frame.seq);
101
+ await this.sendAck(link, frame.sid, frame.seq);
102
+ }
103
+ break;
104
+ case "END":
105
+ if (currentSession && currentSession.sid === frame.sid) {
106
+ if (currentSession.receivedParts.size === currentSession.expectedParts) {
107
+ const fullTokenBytes = this.reassemble(currentSession.chunks);
108
+ const actualShaBytes = await this.sha256(fullTokenBytes);
109
+ const actualSha = (0, import_core.base64Encode)(actualShaBytes);
110
+ if (actualSha === currentSession.expectedSha) {
111
+ const token = new TextDecoder().decode(fullTokenBytes);
112
+ await this.sendAck(link, frame.sid, -1);
113
+ onToken(token);
114
+ } else {
115
+ await this.sendNack(link, frame.sid, -1, "BAD_HASH");
116
+ }
117
+ } else {
118
+ await this.sendNack(link, frame.sid, -1, "BAD_SEQ");
119
+ }
120
+ currentSession = null;
121
+ }
122
+ break;
123
+ }
124
+ } catch (e) {
125
+ console.error("Failed to parse frame", e);
126
+ }
127
+ });
128
+ }
129
+ fragment(data, maxChunk) {
130
+ const chunks = [];
131
+ for (let i = 0; i < data.length; i += maxChunk) {
132
+ chunks.push(data.slice(i, i + maxChunk));
133
+ }
134
+ return chunks;
135
+ }
136
+ reassemble(chunks) {
137
+ const totalLen = chunks.reduce((acc, c) => acc + c.length, 0);
138
+ const result = new Uint8Array(totalLen);
139
+ let offset = 0;
140
+ for (const chunk of chunks) {
141
+ if (chunk) {
142
+ result.set(chunk, offset);
143
+ offset += chunk.length;
144
+ }
145
+ }
146
+ return result;
147
+ }
148
+ async sendFrameWithAck(link, frame, expectedAck) {
149
+ let attempts = 0;
150
+ const frameBytes = new TextEncoder().encode(JSON.stringify(frame));
151
+ while (attempts < this.retries) {
152
+ await link.writeRx(frameBytes);
153
+ try {
154
+ await this.waitForAck(link, frame.sid, expectedAck);
155
+ return;
156
+ } catch (e) {
157
+ attempts++;
158
+ if (attempts >= this.retries) throw new Error(`Failed to send frame ${frame.t} after ${this.retries} attempts`);
159
+ }
160
+ }
161
+ }
162
+ waitForAck(link, sid, expectedAck) {
163
+ return new Promise((resolve, reject) => {
164
+ const timeout = setTimeout(() => {
165
+ cleanup();
166
+ reject(new Error("Timeout waiting for ACK"));
167
+ }, this.timeoutMs);
168
+ const handler = (data) => {
169
+ try {
170
+ const frame = JSON.parse(new TextDecoder().decode(data));
171
+ if (frame.sid === sid) {
172
+ if (frame.t === "ACK" && frame.ack === expectedAck) {
173
+ cleanup();
174
+ resolve();
175
+ } else if (frame.t === "NACK" && frame.at === expectedAck) {
176
+ cleanup();
177
+ reject(new Error(`Received NACK: ${frame.reason}`));
178
+ }
179
+ }
180
+ } catch (e) {
181
+ }
182
+ };
183
+ const cleanup = () => {
184
+ clearTimeout(timeout);
185
+ if (link.offTxNotify) {
186
+ link.offTxNotify(handler);
187
+ }
188
+ };
189
+ link.onTxNotify(handler);
190
+ });
191
+ }
192
+ async sendAck(link, sid, ack) {
193
+ const ackFrame = { t: "ACK", sid, ack };
194
+ await link.writeRx(new TextEncoder().encode(JSON.stringify(ackFrame)));
195
+ }
196
+ async sendNack(link, sid, at, reason) {
197
+ const nackFrame = { t: "NACK", sid, at, reason };
198
+ await link.writeRx(new TextEncoder().encode(JSON.stringify(nackFrame)));
199
+ }
200
+ };
201
+
202
+ // src/link/mock.ts
203
+ var MockBleGattLink = class {
204
+ async connect(deviceId) {
205
+ console.log(`Mock: Connected to ${deviceId}`);
206
+ }
207
+ async disconnect() {
208
+ console.log("Mock: Disconnected");
209
+ }
210
+ async writeRx(frameBytes) {
211
+ this.lastWrittenRx = frameBytes;
212
+ console.log("Mock: Wrote RX", new TextDecoder().decode(frameBytes));
213
+ }
214
+ onTxNotify(cb) {
215
+ this.notifyCb = cb;
216
+ }
217
+ offTxNotify(cb) {
218
+ if (this.notifyCb === cb) {
219
+ this.notifyCb = void 0;
220
+ }
221
+ }
222
+ async startNotify() {
223
+ console.log("Mock: Started Notify");
224
+ }
225
+ async getMtuHint() {
226
+ return 185;
227
+ }
228
+ // Helper for tests
229
+ simulateTxNotify(data) {
230
+ if (this.notifyCb) {
231
+ this.notifyCb(data);
232
+ }
233
+ }
234
+ };
235
+ // Annotate the CommonJS export names for ESM import in node:
236
+ 0 && (module.exports = {
237
+ MockBleGattLink,
238
+ OESPBleGattTransport,
239
+ OESP_BLE_CHAR_META_UUID,
240
+ OESP_BLE_CHAR_RX_UUID,
241
+ OESP_BLE_CHAR_TX_UUID,
242
+ OESP_BLE_SERVICE_UUID
243
+ });
@@ -0,0 +1,96 @@
1
+ declare const OESP_BLE_SERVICE_UUID = "e95f1234-5678-4321-8765-abcdef012345";
2
+ declare const OESP_BLE_CHAR_RX_UUID = "e95f1235-5678-4321-8765-abcdef012345";
3
+ declare const OESP_BLE_CHAR_TX_UUID = "e95f1236-5678-4321-8765-abcdef012345";
4
+ declare const OESP_BLE_CHAR_META_UUID = "e95f1237-5678-4321-8765-abcdef012345";
5
+ type FrameType = "HELLO" | "START" | "CHUNK" | "END" | "ACK" | "NACK";
6
+ interface BaseFrame {
7
+ t: FrameType;
8
+ sid: string;
9
+ }
10
+ interface HelloFrame extends BaseFrame {
11
+ t: "HELLO";
12
+ ver: number;
13
+ did: string;
14
+ caps: {
15
+ maxChunk: number;
16
+ mtuHint?: number;
17
+ };
18
+ }
19
+ interface StartFrame extends BaseFrame {
20
+ t: "START";
21
+ mid: string;
22
+ totalLen: number;
23
+ parts: number;
24
+ sha256: string;
25
+ }
26
+ interface ChunkFrame extends BaseFrame {
27
+ t: "CHUNK";
28
+ seq: number;
29
+ data: string;
30
+ }
31
+ interface EndFrame extends BaseFrame {
32
+ t: "END";
33
+ }
34
+ interface AckFrame extends BaseFrame {
35
+ t: "ACK";
36
+ ack: number;
37
+ }
38
+ interface NackFrame extends BaseFrame {
39
+ t: "NACK";
40
+ at: number;
41
+ reason: "BAD_HASH" | "TIMEOUT" | "BAD_SEQ" | "UNKNOWN";
42
+ }
43
+ type OESPBleFrame = HelloFrame | StartFrame | ChunkFrame | EndFrame | AckFrame | NackFrame;
44
+
45
+ interface BleGattLink {
46
+ connect(deviceId: string): Promise<void>;
47
+ disconnect(): Promise<void>;
48
+ writeRx(frameBytes: Uint8Array): Promise<void>;
49
+ onTxNotify(cb: (frameBytes: Uint8Array) => void): void;
50
+ offTxNotify?(cb: (frameBytes: Uint8Array) => void): void;
51
+ startNotify(): Promise<void>;
52
+ getMtuHint?(): Promise<number | undefined>;
53
+ }
54
+
55
+ interface TransportOpts {
56
+ maxChunkBytes?: number;
57
+ timeoutMs?: number;
58
+ retries?: number;
59
+ sha256: (data: Uint8Array) => Uint8Array | Promise<Uint8Array>;
60
+ }
61
+ declare class OESPBleGattTransport {
62
+ private maxChunkBytes;
63
+ private timeoutMs;
64
+ private retries;
65
+ private sha256;
66
+ constructor(opts: TransportOpts);
67
+ /**
68
+ * Envoie un token OESP via BLE GATT (Central -> Peripheral)
69
+ */
70
+ sendToken(token: string, link: BleGattLink, sid?: string): Promise<void>;
71
+ /**
72
+ * Boucle de réception pour le Peripheral (GATT Server)
73
+ */
74
+ receiveLoop(link: BleGattLink, onToken: (token: string) => void): void;
75
+ private fragment;
76
+ private reassemble;
77
+ private sendFrameWithAck;
78
+ private waitForAck;
79
+ private sendAck;
80
+ private sendNack;
81
+ }
82
+
83
+ declare class MockBleGattLink implements BleGattLink {
84
+ private notifyCb?;
85
+ lastWrittenRx?: Uint8Array;
86
+ connect(deviceId: string): Promise<void>;
87
+ disconnect(): Promise<void>;
88
+ writeRx(frameBytes: Uint8Array): Promise<void>;
89
+ onTxNotify(cb: (frameBytes: Uint8Array) => void): void;
90
+ offTxNotify(cb: (frameBytes: Uint8Array) => void): void;
91
+ startNotify(): Promise<void>;
92
+ getMtuHint(): Promise<number>;
93
+ simulateTxNotify(data: Uint8Array): void;
94
+ }
95
+
96
+ export { type AckFrame, type BaseFrame, type BleGattLink, type ChunkFrame, type EndFrame, type FrameType, type HelloFrame, MockBleGattLink, type NackFrame, type OESPBleFrame, OESPBleGattTransport, OESP_BLE_CHAR_META_UUID, OESP_BLE_CHAR_RX_UUID, OESP_BLE_CHAR_TX_UUID, OESP_BLE_SERVICE_UUID, type StartFrame, type TransportOpts };
@@ -0,0 +1,96 @@
1
+ declare const OESP_BLE_SERVICE_UUID = "e95f1234-5678-4321-8765-abcdef012345";
2
+ declare const OESP_BLE_CHAR_RX_UUID = "e95f1235-5678-4321-8765-abcdef012345";
3
+ declare const OESP_BLE_CHAR_TX_UUID = "e95f1236-5678-4321-8765-abcdef012345";
4
+ declare const OESP_BLE_CHAR_META_UUID = "e95f1237-5678-4321-8765-abcdef012345";
5
+ type FrameType = "HELLO" | "START" | "CHUNK" | "END" | "ACK" | "NACK";
6
+ interface BaseFrame {
7
+ t: FrameType;
8
+ sid: string;
9
+ }
10
+ interface HelloFrame extends BaseFrame {
11
+ t: "HELLO";
12
+ ver: number;
13
+ did: string;
14
+ caps: {
15
+ maxChunk: number;
16
+ mtuHint?: number;
17
+ };
18
+ }
19
+ interface StartFrame extends BaseFrame {
20
+ t: "START";
21
+ mid: string;
22
+ totalLen: number;
23
+ parts: number;
24
+ sha256: string;
25
+ }
26
+ interface ChunkFrame extends BaseFrame {
27
+ t: "CHUNK";
28
+ seq: number;
29
+ data: string;
30
+ }
31
+ interface EndFrame extends BaseFrame {
32
+ t: "END";
33
+ }
34
+ interface AckFrame extends BaseFrame {
35
+ t: "ACK";
36
+ ack: number;
37
+ }
38
+ interface NackFrame extends BaseFrame {
39
+ t: "NACK";
40
+ at: number;
41
+ reason: "BAD_HASH" | "TIMEOUT" | "BAD_SEQ" | "UNKNOWN";
42
+ }
43
+ type OESPBleFrame = HelloFrame | StartFrame | ChunkFrame | EndFrame | AckFrame | NackFrame;
44
+
45
+ interface BleGattLink {
46
+ connect(deviceId: string): Promise<void>;
47
+ disconnect(): Promise<void>;
48
+ writeRx(frameBytes: Uint8Array): Promise<void>;
49
+ onTxNotify(cb: (frameBytes: Uint8Array) => void): void;
50
+ offTxNotify?(cb: (frameBytes: Uint8Array) => void): void;
51
+ startNotify(): Promise<void>;
52
+ getMtuHint?(): Promise<number | undefined>;
53
+ }
54
+
55
+ interface TransportOpts {
56
+ maxChunkBytes?: number;
57
+ timeoutMs?: number;
58
+ retries?: number;
59
+ sha256: (data: Uint8Array) => Uint8Array | Promise<Uint8Array>;
60
+ }
61
+ declare class OESPBleGattTransport {
62
+ private maxChunkBytes;
63
+ private timeoutMs;
64
+ private retries;
65
+ private sha256;
66
+ constructor(opts: TransportOpts);
67
+ /**
68
+ * Envoie un token OESP via BLE GATT (Central -> Peripheral)
69
+ */
70
+ sendToken(token: string, link: BleGattLink, sid?: string): Promise<void>;
71
+ /**
72
+ * Boucle de réception pour le Peripheral (GATT Server)
73
+ */
74
+ receiveLoop(link: BleGattLink, onToken: (token: string) => void): void;
75
+ private fragment;
76
+ private reassemble;
77
+ private sendFrameWithAck;
78
+ private waitForAck;
79
+ private sendAck;
80
+ private sendNack;
81
+ }
82
+
83
+ declare class MockBleGattLink implements BleGattLink {
84
+ private notifyCb?;
85
+ lastWrittenRx?: Uint8Array;
86
+ connect(deviceId: string): Promise<void>;
87
+ disconnect(): Promise<void>;
88
+ writeRx(frameBytes: Uint8Array): Promise<void>;
89
+ onTxNotify(cb: (frameBytes: Uint8Array) => void): void;
90
+ offTxNotify(cb: (frameBytes: Uint8Array) => void): void;
91
+ startNotify(): Promise<void>;
92
+ getMtuHint(): Promise<number>;
93
+ simulateTxNotify(data: Uint8Array): void;
94
+ }
95
+
96
+ export { type AckFrame, type BaseFrame, type BleGattLink, type ChunkFrame, type EndFrame, type FrameType, type HelloFrame, MockBleGattLink, type NackFrame, type OESPBleFrame, OESPBleGattTransport, OESP_BLE_CHAR_META_UUID, OESP_BLE_CHAR_RX_UUID, OESP_BLE_CHAR_TX_UUID, OESP_BLE_SERVICE_UUID, type StartFrame, type TransportOpts };
package/dist/index.js ADDED
@@ -0,0 +1,211 @@
1
+ // src/frames.ts
2
+ var OESP_BLE_SERVICE_UUID = "e95f1234-5678-4321-8765-abcdef012345";
3
+ var OESP_BLE_CHAR_RX_UUID = "e95f1235-5678-4321-8765-abcdef012345";
4
+ var OESP_BLE_CHAR_TX_UUID = "e95f1236-5678-4321-8765-abcdef012345";
5
+ var OESP_BLE_CHAR_META_UUID = "e95f1237-5678-4321-8765-abcdef012345";
6
+
7
+ // src/OESPBleGattTransport.ts
8
+ import { base64Encode, base64Decode } from "@oesp/core";
9
+ var OESPBleGattTransport = class {
10
+ constructor(opts) {
11
+ this.maxChunkBytes = opts.maxChunkBytes || 1024;
12
+ this.timeoutMs = opts.timeoutMs || 3e3;
13
+ this.retries = opts.retries || 3;
14
+ this.sha256 = opts.sha256;
15
+ }
16
+ /**
17
+ * Envoie un token OESP via BLE GATT (Central -> Peripheral)
18
+ */
19
+ async sendToken(token, link, sid = Math.random().toString(36).substring(7)) {
20
+ const tokenBytes = new TextEncoder().encode(token);
21
+ const shaBytes = await this.sha256(tokenBytes);
22
+ const sha256 = base64Encode(shaBytes);
23
+ const totalLen = tokenBytes.length;
24
+ const chunks = this.fragment(tokenBytes, this.maxChunkBytes);
25
+ const parts = chunks.length;
26
+ const startFrame = {
27
+ t: "START",
28
+ sid,
29
+ mid: Math.random().toString(36).substring(7),
30
+ totalLen,
31
+ parts,
32
+ sha256
33
+ };
34
+ await this.sendFrameWithAck(link, startFrame, -1);
35
+ for (let i = 0; i < chunks.length; i++) {
36
+ const chunkFrame = {
37
+ t: "CHUNK",
38
+ sid,
39
+ seq: i,
40
+ data: base64Encode(chunks[i])
41
+ };
42
+ await this.sendFrameWithAck(link, chunkFrame, i);
43
+ }
44
+ await this.sendFrameWithAck(link, { t: "END", sid }, -1);
45
+ }
46
+ /**
47
+ * Boucle de réception pour le Peripheral (GATT Server)
48
+ */
49
+ receiveLoop(link, onToken) {
50
+ let currentSession = null;
51
+ link.onTxNotify(async (data) => {
52
+ try {
53
+ const frame = JSON.parse(new TextDecoder().decode(data));
54
+ switch (frame.t) {
55
+ case "START":
56
+ currentSession = {
57
+ sid: frame.sid,
58
+ expectedSha: frame.sha256,
59
+ expectedParts: frame.parts,
60
+ chunks: new Array(frame.parts),
61
+ receivedParts: /* @__PURE__ */ new Set()
62
+ };
63
+ await this.sendAck(link, frame.sid, -1);
64
+ break;
65
+ case "CHUNK":
66
+ if (currentSession && currentSession.sid === frame.sid) {
67
+ const chunkData = base64Decode(frame.data);
68
+ currentSession.chunks[frame.seq] = chunkData;
69
+ currentSession.receivedParts.add(frame.seq);
70
+ await this.sendAck(link, frame.sid, frame.seq);
71
+ }
72
+ break;
73
+ case "END":
74
+ if (currentSession && currentSession.sid === frame.sid) {
75
+ if (currentSession.receivedParts.size === currentSession.expectedParts) {
76
+ const fullTokenBytes = this.reassemble(currentSession.chunks);
77
+ const actualShaBytes = await this.sha256(fullTokenBytes);
78
+ const actualSha = base64Encode(actualShaBytes);
79
+ if (actualSha === currentSession.expectedSha) {
80
+ const token = new TextDecoder().decode(fullTokenBytes);
81
+ await this.sendAck(link, frame.sid, -1);
82
+ onToken(token);
83
+ } else {
84
+ await this.sendNack(link, frame.sid, -1, "BAD_HASH");
85
+ }
86
+ } else {
87
+ await this.sendNack(link, frame.sid, -1, "BAD_SEQ");
88
+ }
89
+ currentSession = null;
90
+ }
91
+ break;
92
+ }
93
+ } catch (e) {
94
+ console.error("Failed to parse frame", e);
95
+ }
96
+ });
97
+ }
98
+ fragment(data, maxChunk) {
99
+ const chunks = [];
100
+ for (let i = 0; i < data.length; i += maxChunk) {
101
+ chunks.push(data.slice(i, i + maxChunk));
102
+ }
103
+ return chunks;
104
+ }
105
+ reassemble(chunks) {
106
+ const totalLen = chunks.reduce((acc, c) => acc + c.length, 0);
107
+ const result = new Uint8Array(totalLen);
108
+ let offset = 0;
109
+ for (const chunk of chunks) {
110
+ if (chunk) {
111
+ result.set(chunk, offset);
112
+ offset += chunk.length;
113
+ }
114
+ }
115
+ return result;
116
+ }
117
+ async sendFrameWithAck(link, frame, expectedAck) {
118
+ let attempts = 0;
119
+ const frameBytes = new TextEncoder().encode(JSON.stringify(frame));
120
+ while (attempts < this.retries) {
121
+ await link.writeRx(frameBytes);
122
+ try {
123
+ await this.waitForAck(link, frame.sid, expectedAck);
124
+ return;
125
+ } catch (e) {
126
+ attempts++;
127
+ if (attempts >= this.retries) throw new Error(`Failed to send frame ${frame.t} after ${this.retries} attempts`);
128
+ }
129
+ }
130
+ }
131
+ waitForAck(link, sid, expectedAck) {
132
+ return new Promise((resolve, reject) => {
133
+ const timeout = setTimeout(() => {
134
+ cleanup();
135
+ reject(new Error("Timeout waiting for ACK"));
136
+ }, this.timeoutMs);
137
+ const handler = (data) => {
138
+ try {
139
+ const frame = JSON.parse(new TextDecoder().decode(data));
140
+ if (frame.sid === sid) {
141
+ if (frame.t === "ACK" && frame.ack === expectedAck) {
142
+ cleanup();
143
+ resolve();
144
+ } else if (frame.t === "NACK" && frame.at === expectedAck) {
145
+ cleanup();
146
+ reject(new Error(`Received NACK: ${frame.reason}`));
147
+ }
148
+ }
149
+ } catch (e) {
150
+ }
151
+ };
152
+ const cleanup = () => {
153
+ clearTimeout(timeout);
154
+ if (link.offTxNotify) {
155
+ link.offTxNotify(handler);
156
+ }
157
+ };
158
+ link.onTxNotify(handler);
159
+ });
160
+ }
161
+ async sendAck(link, sid, ack) {
162
+ const ackFrame = { t: "ACK", sid, ack };
163
+ await link.writeRx(new TextEncoder().encode(JSON.stringify(ackFrame)));
164
+ }
165
+ async sendNack(link, sid, at, reason) {
166
+ const nackFrame = { t: "NACK", sid, at, reason };
167
+ await link.writeRx(new TextEncoder().encode(JSON.stringify(nackFrame)));
168
+ }
169
+ };
170
+
171
+ // src/link/mock.ts
172
+ var MockBleGattLink = class {
173
+ async connect(deviceId) {
174
+ console.log(`Mock: Connected to ${deviceId}`);
175
+ }
176
+ async disconnect() {
177
+ console.log("Mock: Disconnected");
178
+ }
179
+ async writeRx(frameBytes) {
180
+ this.lastWrittenRx = frameBytes;
181
+ console.log("Mock: Wrote RX", new TextDecoder().decode(frameBytes));
182
+ }
183
+ onTxNotify(cb) {
184
+ this.notifyCb = cb;
185
+ }
186
+ offTxNotify(cb) {
187
+ if (this.notifyCb === cb) {
188
+ this.notifyCb = void 0;
189
+ }
190
+ }
191
+ async startNotify() {
192
+ console.log("Mock: Started Notify");
193
+ }
194
+ async getMtuHint() {
195
+ return 185;
196
+ }
197
+ // Helper for tests
198
+ simulateTxNotify(data) {
199
+ if (this.notifyCb) {
200
+ this.notifyCb(data);
201
+ }
202
+ }
203
+ };
204
+ export {
205
+ MockBleGattLink,
206
+ OESPBleGattTransport,
207
+ OESP_BLE_CHAR_META_UUID,
208
+ OESP_BLE_CHAR_RX_UUID,
209
+ OESP_BLE_CHAR_TX_UUID,
210
+ OESP_BLE_SERVICE_UUID
211
+ };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@oesp/transport-ble-gatt",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "description": "OESP BLE GATT Transport (P2P offline)",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "scripts": {
17
+ "build": "tsup src/index.ts --dts --format esm,cjs",
18
+ "test": "vitest run",
19
+ "lint": "tsc -p tsconfig.json --noEmit"
20
+ },
21
+ "dependencies": {
22
+ "@oesp/core": "workspace:*"
23
+ },
24
+ "devDependencies": {
25
+ "tsup": "^8.0.1",
26
+ "typescript": "^5.6.3",
27
+ "vitest": "^1.6.0"
28
+ }
29
+ }