@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 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, fast = true, chunkSize = config.chunkSize);
45
- sendFrame(data, fast = true);
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, true, 0);
72
+ await ble.sendBin(data, { chunkSize: 0 });
68
73
  ```
69
74
 
70
- Для протоколов, где одна BLE write является одним транспортным frame, следует использовать `sendFrame()`:
75
+ Для протоколов, где одна BLE write является одним транспортным frame, отключите дробление для конкретной отправки:
71
76
 
72
77
  ```js
73
- const ok = await ble.sendFrame(frame);
78
+ const ok = await ble.sendBin(frame, { chunkSize: 0 });
74
79
  ```
75
80
 
76
- `sendFrame()` никогда не делит данные. Если characteristic или устройство не принимает запись такого размера, метод вызовет `onerror` и вернёт `false`.
81
+ Опция `withResponse` едина для всех методов отправки:
77
82
 
78
- Параметр `fast` выбирает способ записи:
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
- - `true` `writeValueWithoutResponse`
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
- - Shipment buffering
7
- - Reception buffering, separation of text by separator
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
- // charxUUID: '0000ffe1-0000-1000-8000-00805f9b34fb'
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, fast = true) {
166
- return this.sendBin((new TextEncoder()).encode(text), fast);
186
+ async sendText(text, options) {
187
+ return this.sendBin((new TextEncoder()).encode(text), options);
167
188
  }
168
189
 
169
- async sendFrame(data, fast = true) {
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
- let size = Number(chunkSize);
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 (fast) {
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 t=t=>new Promise(e=>setTimeout(e,t));class e{constructor(){this._session=0,this.reset()}run(t){const e=this._session,s=this._queue.then(()=>{if(e!==this._session)throw new Error("Queue empty");return t()});return this._queue=s.catch(()=>{}),s}runNothrow(t){return this.run(t).catch(()=>null)}reset(){this._session++,this._queue=Promise.resolve()}}class s{static State={Closed:"closed",Opening:"opening",Open:"open",Closing:"closing"};ontext=null;onbin(t){}onopen(){}onclose(){}onchange(t){}onselect(t){}onerror(t){}constructor(t={}){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,...t}}config(t={}){this.cfg={...this.cfg,...t}}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"}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(t){this._error(t),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 t=await this._device.gatt.connect(),e=await t.getPrimaryService(this.cfg.serviceUUID);this._rx=await e.getCharacteristic(this.cfg.rxUUID),this._tx=await e.getCharacteristic(this.cfg.txUUID),await this._tx.startNotifications(),this._tx.addEventListener("characteristicvaluechanged",this._data_h),this._sender.reset(),this._change(s.State.Open)}catch(e){this._error(e),this._sender.reset(),this._change(s.State.Closed),this.retry&&t(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(t){this._error(t),await this._disconnect()}}}async sendText(t,e=!0){return this.sendBin((new TextEncoder).encode(t),e)}async sendFrame(t,e=!0){return this.sendBin(t,e,0)}async sendBin(e,s=!0,i=this.cfg.chunkSize){return!(!this.opened()||!this._rx)&&!0===await this._sender.runNothrow(async()=>{if(!this.opened()||!this._rx)return!1;const n=e instanceof Uint8Array?e:new Uint8Array(e);if(!n.length)return!0;let r=Number(i);r=!Number.isFinite(r)||r<=0?n.length:Math.max(1,Math.floor(r));try{for(let e=0;e<n.length;e+=r){if(!this.opened()||!this._rx)return!1;const i=n.subarray(e,e+r);s?await this._rx.writeValueWithoutResponse(i):await this._rx.writeValueWithResponse(i),e+r<n.length&&this.cfg.chunkDelay>0&&await t(this.cfg.chunkDelay)}}catch(t){return this._error(t),!1}return!0})}_state=s.State.Closed;_device=null;_rx=null;_tx=null;retry=!1;_sender=new e;_lifecycle=new e;async _disconnect(e){if(this._sender.reset(),this._tx)try{this._tx.removeEventListener("characteristicvaluechanged",this._data_h)}catch(e){}this._rx=null,this._tx=null,this._change(s.State.Closed),this.retry&&t(this.cfg.reconnect).then(()=>{this._lifecycle.runNothrow(()=>this._open())}),await t(50)}_disconnect_h=this._disconnect.bind(this);_data(t){try{const e=t.target.value,s=new Uint8Array(e.buffer,e.byteOffset,e.byteLength);this.onbin(s),this.ontext&&this.ontext((new TextDecoder).decode(s))}catch(t){this._error(t)}}_data_h=this._data.bind(this);_error(t){this.onerror("[BLE] "+t)}_change(t){if(this._state!==t)switch(this._state=t,this.onchange(t),t){case s.State.Open:this.onopen();break;case s.State.Closed:this.onclose()}}}export{s as default};
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.9",
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.2",
19
+ "webpack": "^5.110.3",
20
20
  "webpack-cli": "^7.2.3",
21
21
  "webpack-dev-server": "^6.0.0"
22
22
  },