@alexgyver/ble 1.0.9 → 1.0.10
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 +41 -10
- package/README_EN.md +42 -7
- package/ble.js +54 -11
- package/ble.min.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ config(params = {});
|
|
|
22
22
|
// reconnect: 1000
|
|
23
23
|
// chunkSize: 500
|
|
24
24
|
// chunkDelay: 0
|
|
25
|
+
// withResponse: false
|
|
25
26
|
|
|
26
27
|
onbin(b);
|
|
27
28
|
ontext(t);
|
|
@@ -36,14 +37,18 @@ static supported();
|
|
|
36
37
|
opened();
|
|
37
38
|
selected();
|
|
38
39
|
getName();
|
|
40
|
+
getRxProperties();
|
|
41
|
+
getTxProperties();
|
|
42
|
+
canWriteWithResponse();
|
|
43
|
+
canWriteWithoutResponse();
|
|
44
|
+
canIndicate();
|
|
39
45
|
|
|
40
46
|
select();
|
|
41
47
|
open();
|
|
42
48
|
close();
|
|
43
49
|
|
|
44
|
-
sendBin(data,
|
|
45
|
-
|
|
46
|
-
sendText(text, fast = true);
|
|
50
|
+
sendBin(data, options = {});
|
|
51
|
+
sendText(text, options = {});
|
|
47
52
|
```
|
|
48
53
|
|
|
49
54
|
Все методы отправки возвращают `Promise<boolean>`.
|
|
@@ -64,18 +69,44 @@ await ble.sendBin(data);
|
|
|
64
69
|
`chunkSize <= 0` отключает дробление и выполняет одну characteristic write:
|
|
65
70
|
|
|
66
71
|
```js
|
|
67
|
-
await ble.sendBin(data,
|
|
72
|
+
await ble.sendBin(data, { chunkSize: 0 });
|
|
68
73
|
```
|
|
69
74
|
|
|
70
|
-
Для протоколов, где одна BLE write является одним транспортным frame,
|
|
75
|
+
Для протоколов, где одна BLE write является одним транспортным frame, отключите дробление для конкретной отправки:
|
|
71
76
|
|
|
72
77
|
```js
|
|
73
|
-
const ok = await ble.
|
|
78
|
+
const ok = await ble.sendBin(frame, { chunkSize: 0 });
|
|
74
79
|
```
|
|
75
80
|
|
|
76
|
-
`
|
|
81
|
+
Опция `withResponse` едина для всех методов отправки:
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
```js
|
|
84
|
+
await ble.sendBin(data, { withResponse: true });
|
|
85
|
+
await ble.sendText('hello', { withResponse: true });
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Для атомарного фрейма с GATT response обе опции можно совместить:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
await ble.sendBin(frame, { withResponse: true, chunkSize: 0 });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
При `withResponse: true` используется `writeValueWithResponse()`. Promise отправки завершается после ответа GATT-сервера, поэтому режим подходит для последовательной передачи с транспортным backpressure. Значение по умолчанию задаётся одноимённой настройкой конструктора.
|
|
95
|
+
|
|
96
|
+
Для обратной совместимости второй boolean-аргумент продолжает трактоваться как старый `fast`: `true` выбирает Write without Response, `false` — Write with Response. Третий аргумент старой формы задаёт `chunkSize`:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
await ble.sendBin(data, false, 100);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Перед использованием можно проверить свойства найденных характеристик:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
if (!ble.canWriteWithResponse()) throw new Error('Write with Response is unavailable');
|
|
106
|
+
if (!ble.canWriteWithoutResponse()) throw new Error('Write without Response is unavailable');
|
|
107
|
+
if (!ble.canIndicate()) throw new Error('Indications are unavailable');
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`getRxProperties()` и `getTxProperties()` возвращают объект `BluetoothCharacteristicProperties` либо `null`, пока соединение не открыто. `startNotifications()` включает доступный режим, но не позволяет JavaScript принудительно выбрать Indication вместо Notification. Если протоколу нужны именно Indication, TX characteristic на устройстве следует объявлять как indicate-only.
|
|
79
111
|
|
|
80
|
-
|
|
81
|
-
- `false` — `writeValueWithResponse`
|
|
112
|
+
Проверка поддержки выбранного способа записи выполняется внутри `sendBin()`. При несовместимой RX characteristic метод вызывает `onerror` и возвращает `false`.
|
package/README_EN.md
CHANGED
|
@@ -3,8 +3,9 @@ This is an automatic translation and may be incorrect in some places. See the so
|
|
|
3
3
|
# BLE.js
|
|
4
4
|
Wrapper on Web Bluetooth API
|
|
5
5
|
- Automatic reconnection
|
|
6
|
-
-
|
|
7
|
-
-
|
|
6
|
+
- Sequential writes without overlapping operations
|
|
7
|
+
- Configurable splitting of large writes
|
|
8
|
+
- Atomic sending of a single BLE frame
|
|
8
9
|
|
|
9
10
|
[demo](https://gyverlibs.github.io/BLE.js/test/)
|
|
10
11
|
|
|
@@ -16,12 +17,14 @@ Wrapper on Web Bluetooth API
|
|
|
16
17
|
```js
|
|
17
18
|
constructor(params = {});
|
|
18
19
|
config(params = {});
|
|
19
|
-
// eol: /\r?\n/
|
|
20
20
|
// serviceUUID: '0000ffe0-0000-1000-8000-00805f9b34fb'
|
|
21
|
-
//
|
|
21
|
+
// rxUUID: '0000ffe1-0000-1000-8000-00805f9b34fb'
|
|
22
|
+
// txUUID: '0000ffe2-0000-1000-8000-00805f9b34fb'
|
|
22
23
|
// auto_open: false
|
|
23
|
-
// max_tx: 20
|
|
24
24
|
// reconnect: 1000
|
|
25
|
+
// chunkSize: 500
|
|
26
|
+
// chunkDelay: 0
|
|
27
|
+
// withResponse: false
|
|
25
28
|
|
|
26
29
|
onbin(b);
|
|
27
30
|
ontext(t);
|
|
@@ -36,11 +39,43 @@ static supported();
|
|
|
36
39
|
opened();
|
|
37
40
|
selected();
|
|
38
41
|
getName();
|
|
42
|
+
getRxProperties();
|
|
43
|
+
getTxProperties();
|
|
44
|
+
canWriteWithResponse();
|
|
45
|
+
canWriteWithoutResponse();
|
|
46
|
+
canIndicate();
|
|
39
47
|
|
|
40
48
|
select();
|
|
41
49
|
open();
|
|
42
50
|
close();
|
|
43
51
|
|
|
44
|
-
sendBin(data);
|
|
45
|
-
sendText(text);
|
|
52
|
+
sendBin(data, options = {});
|
|
53
|
+
sendText(text, options = {});
|
|
46
54
|
```
|
|
55
|
+
|
|
56
|
+
All send methods return `Promise<boolean>`.
|
|
57
|
+
|
|
58
|
+
The `withResponse` option is shared by all send methods:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
await ble.sendBin(data, { withResponse: true });
|
|
62
|
+
await ble.sendText('hello', { withResponse: true });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Set `chunkSize: 0` when one protocol frame must map to exactly one characteristic write:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
await ble.sendBin(frame, { withResponse: true, chunkSize: 0 });
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
With `withResponse: true`, the library uses `writeValueWithResponse()` and validates that the connected RX characteristic supports it. The send Promise resolves after the GATT response, providing transport-level backpressure. The constructor option with the same name selects the default mode.
|
|
72
|
+
|
|
73
|
+
The legacy boolean argument remains supported: `true` means Write without Response and `false` means Write with Response. The legacy third argument still sets `chunkSize`:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
await ble.sendBin(data, false, 100);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`canWriteWithResponse()`, `canWriteWithoutResponse()`, and `canIndicate()` expose the capabilities of the connected RX and TX characteristics. `getRxProperties()` and `getTxProperties()` return their complete `BluetoothCharacteristicProperties`, or `null` while disconnected. Every send validates the selected write mode and returns `false` through the usual error path when it is unsupported.
|
|
80
|
+
|
|
81
|
+
Web Bluetooth does not let JavaScript explicitly select Indication instead of Notification. A protocol that requires Indication should expose an indicate-only TX characteristic on the peripheral.
|
package/ble.js
CHANGED
|
@@ -25,6 +25,7 @@ export default class BLEJS {
|
|
|
25
25
|
reconnect: 1000,
|
|
26
26
|
chunkSize: 500,
|
|
27
27
|
chunkDelay: 0,
|
|
28
|
+
withResponse: false,
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
this.cfg = { ...def, ...params };
|
|
@@ -50,6 +51,26 @@ export default class BLEJS {
|
|
|
50
51
|
return this._device ? this._device.name : 'None';
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
getRxProperties() {
|
|
55
|
+
return this._rx?.properties ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getTxProperties() {
|
|
59
|
+
return this._tx?.properties ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
canWriteWithResponse() {
|
|
63
|
+
return !!this._rx?.properties?.write;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
canWriteWithoutResponse() {
|
|
67
|
+
return !!this._rx?.properties?.writeWithoutResponse;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
canIndicate() {
|
|
71
|
+
return !!this._tx?.properties?.indicate;
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
async select() {
|
|
54
75
|
try {
|
|
55
76
|
await this.close();
|
|
@@ -162,24 +183,32 @@ export default class BLEJS {
|
|
|
162
183
|
}
|
|
163
184
|
}
|
|
164
185
|
|
|
165
|
-
async sendText(text,
|
|
166
|
-
return this.sendBin((new TextEncoder()).encode(text),
|
|
186
|
+
async sendText(text, options) {
|
|
187
|
+
return this.sendBin((new TextEncoder()).encode(text), options);
|
|
167
188
|
}
|
|
168
189
|
|
|
169
|
-
async
|
|
170
|
-
return this.sendBin(data, fast, 0);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async sendBin(data, fast = true, chunkSize = this.cfg.chunkSize) {
|
|
190
|
+
async sendBin(data, options, legacyChunkSize) {
|
|
174
191
|
if (!this.opened() || !this._rx) return false;
|
|
175
192
|
|
|
193
|
+
options = this._sendOptions(options, legacyChunkSize);
|
|
194
|
+
|
|
176
195
|
const result = await this._sender.runNothrow(async () => {
|
|
177
196
|
if (!this.opened() || !this._rx) return false;
|
|
178
197
|
|
|
179
198
|
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
180
199
|
if (!bytes.length) return true;
|
|
181
200
|
|
|
182
|
-
|
|
201
|
+
if (options.withResponse) {
|
|
202
|
+
if (!this.canWriteWithResponse()) {
|
|
203
|
+
this._error('RX characteristic does not support Write with Response');
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
} else if (!this.canWriteWithoutResponse()) {
|
|
207
|
+
this._error('RX characteristic does not support Write without Response');
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let size = Number(options.chunkSize);
|
|
183
212
|
if (!Number.isFinite(size) || size <= 0) size = bytes.length;
|
|
184
213
|
else size = Math.max(1, Math.floor(size));
|
|
185
214
|
|
|
@@ -189,10 +218,10 @@ export default class BLEJS {
|
|
|
189
218
|
|
|
190
219
|
const chunk = bytes.subarray(i, i + size);
|
|
191
220
|
|
|
192
|
-
if (
|
|
193
|
-
await this._rx.writeValueWithoutResponse(chunk);
|
|
194
|
-
} else {
|
|
221
|
+
if (options.withResponse) {
|
|
195
222
|
await this._rx.writeValueWithResponse(chunk);
|
|
223
|
+
} else {
|
|
224
|
+
await this._rx.writeValueWithoutResponse(chunk);
|
|
196
225
|
}
|
|
197
226
|
|
|
198
227
|
if (i + size < bytes.length && this.cfg.chunkDelay > 0) {
|
|
@@ -210,6 +239,20 @@ export default class BLEJS {
|
|
|
210
239
|
return result === true;
|
|
211
240
|
}
|
|
212
241
|
|
|
242
|
+
_sendOptions(options, legacyChunkSize) {
|
|
243
|
+
if (typeof options === 'boolean') {
|
|
244
|
+
return {
|
|
245
|
+
withResponse: !options,
|
|
246
|
+
chunkSize: legacyChunkSize ?? this.cfg.chunkSize,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
withResponse: options?.withResponse ?? this.cfg.withResponse,
|
|
252
|
+
chunkSize: options?.chunkSize ?? this.cfg.chunkSize,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
213
256
|
_state = BLEJS.State.Closed;
|
|
214
257
|
_device = null;
|
|
215
258
|
_rx = null;
|
package/ble.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const e=e=>new Promise(t=>setTimeout(t,e));class t{constructor(){this._session=0,this.reset()}run(e){const t=this._session,s=this._queue.then(()=>{if(t!==this._session)throw new Error("Queue empty");return e()});return this._queue=s.catch(()=>{}),s}runNothrow(e){return this.run(e).catch(()=>null)}reset(){this._session++,this._queue=Promise.resolve()}}class s{static State={Closed:"closed",Opening:"opening",Open:"open",Closing:"closing"};ontext=null;onbin(e){}onopen(){}onclose(){}onchange(e){}onselect(e){}onerror(e){}constructor(e={}){this.cfg={serviceUUID:"0000ffe0-0000-1000-8000-00805f9b34fb",rxUUID:"0000ffe1-0000-1000-8000-00805f9b34fb",txUUID:"0000ffe2-0000-1000-8000-00805f9b34fb",auto_open:!1,reconnect:1e3,chunkSize:500,chunkDelay:0,withResponse:!1,...e}}config(e={}){this.cfg={...this.cfg,...e}}static supported(){return"bluetooth"in navigator}opened(){return this._state===s.State.Open}selected(){return!!this._device}getName(){return this._device?this._device.name:"None"}getRxProperties(){return this._rx?.properties??null}getTxProperties(){return this._tx?.properties??null}canWriteWithResponse(){return!!this._rx?.properties?.write}canWriteWithoutResponse(){return!!this._rx?.properties?.writeWithoutResponse}canIndicate(){return!!this._tx?.properties?.indicate}async select(){try{await this.close(),this._device&&this._device.removeEventListener("gattserverdisconnected",this._disconnect_h),this._device=await navigator.bluetooth.requestDevice({filters:[{services:[this.cfg.serviceUUID]}],optionalServices:[this.cfg.serviceUUID]}),this._device.addEventListener("gattserverdisconnected",this._disconnect_h)}catch(e){this._error(e),this._device=null}return this.onselect(this.getName()),this.cfg.auto_open&&this.open(),this.selected()}async open(){return this._lifecycle.runNothrow(async()=>this._device?!!this.opened()||(this.cfg.reconnect&&(this.retry=!0),await this._open(),this.opened()):(this._error("No device"),!1))}async _open(){if(this._state===s.State.Closed){this._change(s.State.Opening);try{const e=await this._device.gatt.connect(),t=await e.getPrimaryService(this.cfg.serviceUUID);this._rx=await t.getCharacteristic(this.cfg.rxUUID),this._tx=await t.getCharacteristic(this.cfg.txUUID),await this._tx.startNotifications(),this._tx.addEventListener("characteristicvaluechanged",this._data_h),this._sender.reset(),this._change(s.State.Open)}catch(t){this._error(t),this._sender.reset(),this._change(s.State.Closed),this.retry&&e(this.cfg.reconnect).then(()=>{this._lifecycle.runNothrow(()=>this._open())})}}}async close(){return this._lifecycle.runNothrow(async()=>(this.retry=!1,this._sender.reset(),this._state!==s.State.Closed&&await this._close(),!0))}async _close(){if(this._state!==s.State.Closed){this._change(s.State.Closing);try{this._device?.gatt?.connected?this._device.gatt.disconnect():await this._disconnect()}catch(e){this._error(e),await this._disconnect()}}}async sendText(e,t){return this.sendBin((new TextEncoder).encode(e),t)}async sendBin(t,s,i){return!(!this.opened()||!this._rx)&&(s=this._sendOptions(s,i),!0===await this._sender.runNothrow(async()=>{if(!this.opened()||!this._rx)return!1;const i=t instanceof Uint8Array?t:new Uint8Array(t);if(!i.length)return!0;if(s.withResponse){if(!this.canWriteWithResponse())return this._error("RX characteristic does not support Write with Response"),!1}else if(!this.canWriteWithoutResponse())return this._error("RX characteristic does not support Write without Response"),!1;let n=Number(s.chunkSize);n=!Number.isFinite(n)||n<=0?i.length:Math.max(1,Math.floor(n));try{for(let t=0;t<i.length;t+=n){if(!this.opened()||!this._rx)return!1;const r=i.subarray(t,t+n);s.withResponse?await this._rx.writeValueWithResponse(r):await this._rx.writeValueWithoutResponse(r),t+n<i.length&&this.cfg.chunkDelay>0&&await e(this.cfg.chunkDelay)}}catch(e){return this._error(e),!1}return!0}))}_sendOptions(e,t){return"boolean"==typeof e?{withResponse:!e,chunkSize:t??this.cfg.chunkSize}:{withResponse:e?.withResponse??this.cfg.withResponse,chunkSize:e?.chunkSize??this.cfg.chunkSize}}_state=s.State.Closed;_device=null;_rx=null;_tx=null;retry=!1;_sender=new t;_lifecycle=new t;async _disconnect(t){if(this._sender.reset(),this._tx)try{this._tx.removeEventListener("characteristicvaluechanged",this._data_h)}catch(t){}this._rx=null,this._tx=null,this._change(s.State.Closed),this.retry&&e(this.cfg.reconnect).then(()=>{this._lifecycle.runNothrow(()=>this._open())}),await e(50)}_disconnect_h=this._disconnect.bind(this);_data(e){try{const t=e.target.value,s=new Uint8Array(t.buffer,t.byteOffset,t.byteLength);this.onbin(s),this.ontext&&this.ontext((new TextDecoder).decode(s))}catch(e){this._error(e)}}_data_h=this._data.bind(this);_error(e){this.onerror("[BLE] "+e)}_change(e){if(this._state!==e)switch(this._state=e,this.onchange(e),e){case s.State.Open:this.onopen();break;case s.State.Closed:this.onclose()}}}export{s as default};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexgyver/ble",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Web BLE Serial",
|
|
5
5
|
"main": "./ble.js",
|
|
6
6
|
"module": "./ble.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"css-loader": "^7.1.5",
|
|
18
18
|
"style-loader": "^4.0.0",
|
|
19
|
-
"webpack": "^5.110.
|
|
19
|
+
"webpack": "^5.110.3",
|
|
20
20
|
"webpack-cli": "^7.2.3",
|
|
21
21
|
"webpack-dev-server": "^6.0.0"
|
|
22
22
|
},
|