@oesp/transport-ble-gatt 2.0.0 → 5.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/dist/OESPBleGattTransport.d.ts +28 -0
- package/dist/OESPBleGattTransport.js +171 -0
- package/dist/frames.d.ts +43 -0
- package/dist/frames.js +4 -0
- package/dist/index.d.ts +4 -96
- package/dist/index.js +4 -211
- package/dist/link/BleGattLink.d.ts +9 -0
- package/dist/link/BleGattLink.js +1 -0
- package/dist/link/mock.d.ts +13 -0
- package/dist/link/mock.js +32 -0
- package/package.json +11 -3
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BleGattLink } from "./link/BleGattLink";
|
|
2
|
+
export interface TransportOpts {
|
|
3
|
+
maxChunkBytes?: number;
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
retries?: number;
|
|
6
|
+
sha256: (data: Uint8Array) => Uint8Array | Promise<Uint8Array>;
|
|
7
|
+
}
|
|
8
|
+
export declare class OESPBleGattTransport {
|
|
9
|
+
private maxChunkBytes;
|
|
10
|
+
private timeoutMs;
|
|
11
|
+
private retries;
|
|
12
|
+
private sha256;
|
|
13
|
+
constructor(opts: TransportOpts);
|
|
14
|
+
/**
|
|
15
|
+
* Envoie un token OESP via BLE GATT (Central -> Peripheral)
|
|
16
|
+
*/
|
|
17
|
+
sendToken(token: string, link: BleGattLink, sid?: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Boucle de réception pour le Peripheral (GATT Server)
|
|
20
|
+
*/
|
|
21
|
+
receiveLoop(link: BleGattLink, onToken: (token: string) => void): void;
|
|
22
|
+
private fragment;
|
|
23
|
+
private reassemble;
|
|
24
|
+
private sendFrameWithAck;
|
|
25
|
+
private waitForAck;
|
|
26
|
+
private sendAck;
|
|
27
|
+
private sendNack;
|
|
28
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { base64Encode, base64Decode } from "@oesp/core";
|
|
2
|
+
export class OESPBleGattTransport {
|
|
3
|
+
constructor(opts) {
|
|
4
|
+
this.maxChunkBytes = opts.maxChunkBytes || 1024;
|
|
5
|
+
this.timeoutMs = opts.timeoutMs || 3000;
|
|
6
|
+
this.retries = opts.retries || 3;
|
|
7
|
+
this.sha256 = opts.sha256;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Envoie un token OESP via BLE GATT (Central -> Peripheral)
|
|
11
|
+
*/
|
|
12
|
+
async sendToken(token, link, sid = Math.random().toString(36).substring(7)) {
|
|
13
|
+
const tokenBytes = new TextEncoder().encode(token);
|
|
14
|
+
const shaBytes = await this.sha256(tokenBytes);
|
|
15
|
+
const sha256 = base64Encode(shaBytes);
|
|
16
|
+
const totalLen = tokenBytes.length;
|
|
17
|
+
const chunks = this.fragment(tokenBytes, this.maxChunkBytes);
|
|
18
|
+
const parts = chunks.length;
|
|
19
|
+
// 1. Send START
|
|
20
|
+
const startFrame = {
|
|
21
|
+
t: "START",
|
|
22
|
+
sid,
|
|
23
|
+
mid: Math.random().toString(36).substring(7),
|
|
24
|
+
totalLen,
|
|
25
|
+
parts,
|
|
26
|
+
sha256
|
|
27
|
+
};
|
|
28
|
+
await this.sendFrameWithAck(link, startFrame, -1);
|
|
29
|
+
// 2. Send CHUNKS (Stop-and-Wait)
|
|
30
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
31
|
+
const chunkFrame = {
|
|
32
|
+
t: "CHUNK",
|
|
33
|
+
sid,
|
|
34
|
+
seq: i,
|
|
35
|
+
data: base64Encode(chunks[i])
|
|
36
|
+
};
|
|
37
|
+
await this.sendFrameWithAck(link, chunkFrame, i);
|
|
38
|
+
}
|
|
39
|
+
// 3. Send END
|
|
40
|
+
await this.sendFrameWithAck(link, { t: "END", sid }, -1);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Boucle de réception pour le Peripheral (GATT Server)
|
|
44
|
+
*/
|
|
45
|
+
receiveLoop(link, onToken) {
|
|
46
|
+
let currentSession = null;
|
|
47
|
+
link.onTxNotify(async (data) => {
|
|
48
|
+
try {
|
|
49
|
+
const frame = JSON.parse(new TextDecoder().decode(data));
|
|
50
|
+
switch (frame.t) {
|
|
51
|
+
case "START":
|
|
52
|
+
currentSession = {
|
|
53
|
+
sid: frame.sid,
|
|
54
|
+
expectedSha: frame.sha256,
|
|
55
|
+
expectedParts: frame.parts,
|
|
56
|
+
chunks: new Array(frame.parts),
|
|
57
|
+
receivedParts: new Set()
|
|
58
|
+
};
|
|
59
|
+
await this.sendAck(link, frame.sid, -1);
|
|
60
|
+
break;
|
|
61
|
+
case "CHUNK":
|
|
62
|
+
if (currentSession && currentSession.sid === frame.sid) {
|
|
63
|
+
const chunkData = base64Decode(frame.data);
|
|
64
|
+
currentSession.chunks[frame.seq] = chunkData;
|
|
65
|
+
currentSession.receivedParts.add(frame.seq);
|
|
66
|
+
await this.sendAck(link, frame.sid, frame.seq);
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
case "END":
|
|
70
|
+
if (currentSession && currentSession.sid === frame.sid) {
|
|
71
|
+
if (currentSession.receivedParts.size === currentSession.expectedParts) {
|
|
72
|
+
const fullTokenBytes = this.reassemble(currentSession.chunks);
|
|
73
|
+
const actualShaBytes = await this.sha256(fullTokenBytes);
|
|
74
|
+
const actualSha = base64Encode(actualShaBytes);
|
|
75
|
+
if (actualSha === currentSession.expectedSha) {
|
|
76
|
+
const token = new TextDecoder().decode(fullTokenBytes);
|
|
77
|
+
await this.sendAck(link, frame.sid, -1);
|
|
78
|
+
onToken(token);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
await this.sendNack(link, frame.sid, -1, "BAD_HASH");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
await this.sendNack(link, frame.sid, -1, "BAD_SEQ");
|
|
86
|
+
}
|
|
87
|
+
currentSession = null;
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
console.error("Failed to parse frame", e);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
fragment(data, maxChunk) {
|
|
98
|
+
const chunks = [];
|
|
99
|
+
for (let i = 0; i < data.length; i += maxChunk) {
|
|
100
|
+
chunks.push(data.slice(i, i + maxChunk));
|
|
101
|
+
}
|
|
102
|
+
return chunks;
|
|
103
|
+
}
|
|
104
|
+
reassemble(chunks) {
|
|
105
|
+
const totalLen = chunks.reduce((acc, c) => acc + c.length, 0);
|
|
106
|
+
const result = new Uint8Array(totalLen);
|
|
107
|
+
let offset = 0;
|
|
108
|
+
for (const chunk of chunks) {
|
|
109
|
+
if (chunk) {
|
|
110
|
+
result.set(chunk, offset);
|
|
111
|
+
offset += chunk.length;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
async sendFrameWithAck(link, frame, expectedAck) {
|
|
117
|
+
let attempts = 0;
|
|
118
|
+
const frameBytes = new TextEncoder().encode(JSON.stringify(frame));
|
|
119
|
+
while (attempts < this.retries) {
|
|
120
|
+
await link.writeRx(frameBytes);
|
|
121
|
+
try {
|
|
122
|
+
await this.waitForAck(link, frame.sid, expectedAck);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
attempts++;
|
|
127
|
+
if (attempts >= this.retries)
|
|
128
|
+
throw new Error(`Failed to send frame ${frame.t} after ${this.retries} attempts`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
waitForAck(link, sid, expectedAck) {
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const timeout = setTimeout(() => {
|
|
135
|
+
cleanup();
|
|
136
|
+
reject(new Error("Timeout waiting for ACK"));
|
|
137
|
+
}, this.timeoutMs);
|
|
138
|
+
const handler = (data) => {
|
|
139
|
+
try {
|
|
140
|
+
const frame = JSON.parse(new TextDecoder().decode(data));
|
|
141
|
+
if (frame.sid === sid) {
|
|
142
|
+
if (frame.t === "ACK" && frame.ack === expectedAck) {
|
|
143
|
+
cleanup();
|
|
144
|
+
resolve();
|
|
145
|
+
}
|
|
146
|
+
else if (frame.t === "NACK" && frame.at === expectedAck) {
|
|
147
|
+
cleanup();
|
|
148
|
+
reject(new Error(`Received NACK: ${frame.reason}`));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (e) { }
|
|
153
|
+
};
|
|
154
|
+
const cleanup = () => {
|
|
155
|
+
clearTimeout(timeout);
|
|
156
|
+
if (link.offTxNotify) {
|
|
157
|
+
link.offTxNotify(handler);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
link.onTxNotify(handler);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async sendAck(link, sid, ack) {
|
|
164
|
+
const ackFrame = { t: "ACK", sid, ack };
|
|
165
|
+
await link.writeRx(new TextEncoder().encode(JSON.stringify(ackFrame)));
|
|
166
|
+
}
|
|
167
|
+
async sendNack(link, sid, at, reason) {
|
|
168
|
+
const nackFrame = { t: "NACK", sid, at, reason };
|
|
169
|
+
await link.writeRx(new TextEncoder().encode(JSON.stringify(nackFrame)));
|
|
170
|
+
}
|
|
171
|
+
}
|
package/dist/frames.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export declare const OESP_BLE_SERVICE_UUID = "e95f1234-5678-4321-8765-abcdef012345";
|
|
2
|
+
export declare const OESP_BLE_CHAR_RX_UUID = "e95f1235-5678-4321-8765-abcdef012345";
|
|
3
|
+
export declare const OESP_BLE_CHAR_TX_UUID = "e95f1236-5678-4321-8765-abcdef012345";
|
|
4
|
+
export declare const OESP_BLE_CHAR_META_UUID = "e95f1237-5678-4321-8765-abcdef012345";
|
|
5
|
+
export type FrameType = "HELLO" | "START" | "CHUNK" | "END" | "ACK" | "NACK";
|
|
6
|
+
export interface BaseFrame {
|
|
7
|
+
t: FrameType;
|
|
8
|
+
sid: string;
|
|
9
|
+
}
|
|
10
|
+
export interface HelloFrame extends BaseFrame {
|
|
11
|
+
t: "HELLO";
|
|
12
|
+
ver: number;
|
|
13
|
+
did: string;
|
|
14
|
+
caps: {
|
|
15
|
+
maxChunk: number;
|
|
16
|
+
mtuHint?: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface StartFrame extends BaseFrame {
|
|
20
|
+
t: "START";
|
|
21
|
+
mid: string;
|
|
22
|
+
totalLen: number;
|
|
23
|
+
parts: number;
|
|
24
|
+
sha256: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ChunkFrame extends BaseFrame {
|
|
27
|
+
t: "CHUNK";
|
|
28
|
+
seq: number;
|
|
29
|
+
data: string;
|
|
30
|
+
}
|
|
31
|
+
export interface EndFrame extends BaseFrame {
|
|
32
|
+
t: "END";
|
|
33
|
+
}
|
|
34
|
+
export interface AckFrame extends BaseFrame {
|
|
35
|
+
t: "ACK";
|
|
36
|
+
ack: number;
|
|
37
|
+
}
|
|
38
|
+
export interface NackFrame extends BaseFrame {
|
|
39
|
+
t: "NACK";
|
|
40
|
+
at: number;
|
|
41
|
+
reason: "BAD_HASH" | "TIMEOUT" | "BAD_SEQ" | "UNKNOWN";
|
|
42
|
+
}
|
|
43
|
+
export type OESPBleFrame = HelloFrame | StartFrame | ChunkFrame | EndFrame | AckFrame | NackFrame;
|
package/dist/frames.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const OESP_BLE_SERVICE_UUID = "e95f1234-5678-4321-8765-abcdef012345";
|
|
2
|
+
export const OESP_BLE_CHAR_RX_UUID = "e95f1235-5678-4321-8765-abcdef012345"; // Central -> Peripheral (Write)
|
|
3
|
+
export const OESP_BLE_CHAR_TX_UUID = "e95f1236-5678-4321-8765-abcdef012345"; // Peripheral -> Central (Notify)
|
|
4
|
+
export const OESP_BLE_CHAR_META_UUID = "e95f1237-5678-4321-8765-abcdef012345"; // Meta (Read)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,96 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 };
|
|
1
|
+
export * from "./frames";
|
|
2
|
+
export * from "./OESPBleGattTransport";
|
|
3
|
+
export * from "./link/BleGattLink";
|
|
4
|
+
export * from "./link/mock";
|
package/dist/index.js
CHANGED
|
@@ -1,211 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
};
|
|
1
|
+
export * from "./frames";
|
|
2
|
+
export * from "./OESPBleGattTransport";
|
|
3
|
+
export * from "./link/BleGattLink";
|
|
4
|
+
export * from "./link/mock";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface BleGattLink {
|
|
2
|
+
connect(deviceId: string): Promise<void>;
|
|
3
|
+
disconnect(): Promise<void>;
|
|
4
|
+
writeRx(frameBytes: Uint8Array): Promise<void>;
|
|
5
|
+
onTxNotify(cb: (frameBytes: Uint8Array) => void): void;
|
|
6
|
+
offTxNotify?(cb: (frameBytes: Uint8Array) => void): void;
|
|
7
|
+
startNotify(): Promise<void>;
|
|
8
|
+
getMtuHint?(): Promise<number | undefined>;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BleGattLink } from "./BleGattLink";
|
|
2
|
+
export declare class MockBleGattLink implements BleGattLink {
|
|
3
|
+
private notifyCb?;
|
|
4
|
+
lastWrittenRx?: Uint8Array;
|
|
5
|
+
connect(deviceId: string): Promise<void>;
|
|
6
|
+
disconnect(): Promise<void>;
|
|
7
|
+
writeRx(frameBytes: Uint8Array): Promise<void>;
|
|
8
|
+
onTxNotify(cb: (frameBytes: Uint8Array) => void): void;
|
|
9
|
+
offTxNotify(cb: (frameBytes: Uint8Array) => void): void;
|
|
10
|
+
startNotify(): Promise<void>;
|
|
11
|
+
getMtuHint(): Promise<number>;
|
|
12
|
+
simulateTxNotify(data: Uint8Array): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class MockBleGattLink {
|
|
2
|
+
async connect(deviceId) {
|
|
3
|
+
console.log(`Mock: Connected to ${deviceId}`);
|
|
4
|
+
}
|
|
5
|
+
async disconnect() {
|
|
6
|
+
console.log("Mock: Disconnected");
|
|
7
|
+
}
|
|
8
|
+
async writeRx(frameBytes) {
|
|
9
|
+
this.lastWrittenRx = frameBytes;
|
|
10
|
+
console.log("Mock: Wrote RX", new TextDecoder().decode(frameBytes));
|
|
11
|
+
}
|
|
12
|
+
onTxNotify(cb) {
|
|
13
|
+
this.notifyCb = cb;
|
|
14
|
+
}
|
|
15
|
+
offTxNotify(cb) {
|
|
16
|
+
if (this.notifyCb === cb) {
|
|
17
|
+
this.notifyCb = undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async startNotify() {
|
|
21
|
+
console.log("Mock: Started Notify");
|
|
22
|
+
}
|
|
23
|
+
async getMtuHint() {
|
|
24
|
+
return 185;
|
|
25
|
+
}
|
|
26
|
+
// Helper for tests
|
|
27
|
+
simulateTxNotify(data) {
|
|
28
|
+
if (this.notifyCb) {
|
|
29
|
+
this.notifyCb(data);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oesp/transport-ble-gatt",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "OESP BLE GATT Transport (P2P offline)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": false,
|
|
10
18
|
"files": [
|
|
11
19
|
"dist"
|
|
12
20
|
],
|
|
@@ -14,7 +22,7 @@
|
|
|
14
22
|
"access": "public"
|
|
15
23
|
},
|
|
16
24
|
"dependencies": {
|
|
17
|
-
"@oesp/core": "
|
|
25
|
+
"@oesp/core": "5.0.0"
|
|
18
26
|
},
|
|
19
27
|
"devDependencies": {
|
|
20
28
|
"tsup": "^8.0.1",
|
|
@@ -22,7 +30,7 @@
|
|
|
22
30
|
"vitest": "^1.6.0"
|
|
23
31
|
},
|
|
24
32
|
"scripts": {
|
|
25
|
-
"build": "
|
|
33
|
+
"build": "tsc -p tsconfig.build.json",
|
|
26
34
|
"test": "vitest run",
|
|
27
35
|
"lint": "tsc -p tsconfig.json --noEmit"
|
|
28
36
|
}
|