unified-ble-manager 4.0.23 → 4.0.24
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/CHANGELOG.md +15 -0
- package/MIGRATION_4.0.md +1 -1
- package/README.md +1 -1
- package/RELEASE.md +11 -3
- package/SBOM.cdx.json +4 -4
- package/THIRD_PARTY_LICENSES.json +1 -1
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt +5 -3
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt +196 -60
- package/android/src/test/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcherLifecycleTest.kt +202 -0
- package/docs/ELECTRON.md +1 -1
- package/docs/EXPO_PLUGIN.md +1 -1
- package/docs/GAPS.4.0.md +1 -1
- package/docs/GETTING_STARTED.md +3 -3
- package/docs/NODE.md +1 -1
- package/docs/WEB.md +1 -1
- package/docs/generated/PLATFORM_SUPPORT.md +1 -1
- package/lib/commonjs/implementation-version.js +1 -1
- package/lib/module/implementation-version.js +1 -1
- package/lib/typescript/commonjs/src/backends/bluez/bluez-backend-provider.d.ts +1 -1
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
- package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-provider.d.ts +1 -1
- package/lib/typescript/commonjs/src/backends/reactnative/react-native-apple-provider.d.ts +1 -1
- package/lib/typescript/commonjs/src/backends/winrt/winrt-provider.d.ts +1 -1
- package/lib/typescript/commonjs/src/implementation-version.d.ts +1 -1
- package/lib/typescript/module/src/backends/bluez/bluez-backend-provider.d.ts +1 -1
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
- package/lib/typescript/module/src/backends/reactnative/react-native-android-provider.d.ts +1 -1
- package/lib/typescript/module/src/backends/reactnative/react-native-apple-provider.d.ts +1 -1
- package/lib/typescript/module/src/backends/winrt/winrt-provider.d.ts +1 -1
- package/lib/typescript/module/src/implementation-version.d.ts +1 -1
- package/native/PREBUILDS.json +5 -5
- package/native/electron/corebluetooth/prebuilds/darwin-arm64/unified_ble_corebluetooth.node +0 -0
- package/native/electron/corebluetooth/prebuilds/darwin-x64/unified_ble_corebluetooth.node +0 -0
- package/native/electron/winrt/prebuilds/win32-arm64/unified_ble_winrt.node +0 -0
- package/native/electron/winrt/prebuilds/win32-x64/unified_ble_winrt.node +0 -0
- package/package.json +1 -1
- package/src/implementation-version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@ All notable changes to `unified-ble-manager` are documented here.
|
|
|
6
6
|
|
|
7
7
|
No changes yet.
|
|
8
8
|
|
|
9
|
+
## [4.0.24] - 2026-09-05
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
- Arbitrate an Android characteristic-write callback failure against an
|
|
14
|
+
already-in-flight disconnect callback. A provisional write status such as
|
|
15
|
+
133 is retained for a bounded 250 ms evidence window, allowing the
|
|
16
|
+
authoritative connection-state callback to report typed `connection.lost`
|
|
17
|
+
without relabeling an isolated status 133 as link loss.
|
|
18
|
+
- Preserve the Android GATT operation and status for synchronous and
|
|
19
|
+
asynchronous characteristic-write failures that are not proven link loss.
|
|
20
|
+
- Make exact and UUID-keyed pending-write cleanup identity-safe so stale
|
|
21
|
+
teardown and late callbacks cannot settle a replacement write or remove its
|
|
22
|
+
payload.
|
|
23
|
+
|
|
9
24
|
## [4.0.23] - 2026-09-05
|
|
10
25
|
|
|
11
26
|
### Fixes
|
package/MIGRATION_4.0.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Migrating from react-native-ble-plx
|
|
4
4
|
|
|
5
|
-
This source targets `4.0.
|
|
5
|
+
This source targets `4.0.24`. The `4.0.0` release is a new package and a new contract. It is **not a source-compatible rename**. There is no `new BleManager()` facade, no Base64 characteristic values, and no public transaction IDs.
|
|
6
6
|
|
|
7
7
|
This page is for a React Native app that already uses `react-native-ble-plx`. Web, Electron, Node, and Tauri are new hosts — use those pages after you understand the RN rewrite.
|
|
8
8
|
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ root import does not pick a radio. Package SemVer and backend support labels are
|
|
|
18
18
|
independent: each radio backend keeps its evidence-derived label. See
|
|
19
19
|
[`docs/PLATFORMS.md`](docs/PLATFORMS.md).
|
|
20
20
|
|
|
21
|
-
This source tree is versioned `4.0.
|
|
21
|
+
This source tree is versioned `4.0.24`. Install the exact version shown in the npm
|
|
22
22
|
registry. During release preparation, the version in `package.json` can be ahead
|
|
23
23
|
of npm until the matching tag-driven workflow publishes it; the registry and
|
|
24
24
|
GitHub release remain authoritative.
|
package/RELEASE.md
CHANGED
|
@@ -82,8 +82,8 @@ Active `4.0.0-rc.*` release-train candidates publish to npm `latest` so a bare `
|
|
|
82
82
|
On release day, set `release_candidate` to the exact candidate required by the
|
|
83
83
|
release plan. RC2, RC3, RC4, `4.0.0-rc.4.1`, and RC5 are already immutable
|
|
84
84
|
once tagged. Stable `4.0.0` through `4.0.20` are immutable. The unpublished
|
|
85
|
-
`v4.0.21` tag is also immutable after its cancelled workflow. `4.0.22`
|
|
86
|
-
current train head.
|
|
85
|
+
`v4.0.21` tag is also immutable after its cancelled workflow. `4.0.22` and
|
|
86
|
+
`4.0.23` are immutable published history. `4.0.24` is the current train head.
|
|
87
87
|
|
|
88
88
|
```sh
|
|
89
89
|
release_candidate=4.0.0-rc.N
|
|
@@ -111,6 +111,14 @@ The first stable tag `v4.0.0` is immutable published history. Do not recreate or
|
|
|
111
111
|
git tag -a v4.0.0 -m "v4.0.0"
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
## Releasing 4.0.24
|
|
115
|
+
|
|
116
|
+
Release `v4.0.24` only from the exact current `main` commit after its canonical
|
|
117
|
+
CI succeeds. Verify `package.json` is `4.0.24`, the worktree is clean, and
|
|
118
|
+
release-note extraction finds `## [4.0.24]`. Push a new annotated `v4.0.24`
|
|
119
|
+
tag; never move an earlier immutable tag. Follow the required local validation,
|
|
120
|
+
publish workflow, and registry verification below.
|
|
121
|
+
|
|
114
122
|
## Releasing 4.0.23
|
|
115
123
|
|
|
116
124
|
Release `v4.0.23` only from the exact current `main` commit after its canonical
|
|
@@ -483,7 +491,7 @@ a green publish job and a package a consumer can actually install are not the
|
|
|
483
491
|
same claim.
|
|
484
492
|
|
|
485
493
|
```sh
|
|
486
|
-
version=4.0.
|
|
494
|
+
version=4.0.24
|
|
487
495
|
|
|
488
496
|
npm view "unified-ble-manager@$version" version
|
|
489
497
|
npm view unified-ble-manager dist-tags --json
|
package/SBOM.cdx.json
CHANGED
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
"metadata": {
|
|
7
7
|
"component": {
|
|
8
8
|
"type": "library",
|
|
9
|
-
"bom-ref": "pkg:npm/unified-ble-manager@4.0.
|
|
9
|
+
"bom-ref": "pkg:npm/unified-ble-manager@4.0.24",
|
|
10
10
|
"name": "unified-ble-manager",
|
|
11
|
-
"version": "4.0.
|
|
11
|
+
"version": "4.0.24",
|
|
12
12
|
"licenses": [
|
|
13
13
|
{
|
|
14
14
|
"expression": "Apache-2.0"
|
|
15
15
|
}
|
|
16
16
|
],
|
|
17
|
-
"purl": "pkg:npm/unified-ble-manager@4.0.
|
|
17
|
+
"purl": "pkg:npm/unified-ble-manager@4.0.24"
|
|
18
18
|
},
|
|
19
19
|
"properties": [
|
|
20
20
|
{
|
|
@@ -537,7 +537,7 @@
|
|
|
537
537
|
"dependsOn": []
|
|
538
538
|
},
|
|
539
539
|
{
|
|
540
|
-
"ref": "pkg:npm/unified-ble-manager@4.0.
|
|
540
|
+
"ref": "pkg:npm/unified-ble-manager@4.0.24",
|
|
541
541
|
"dependsOn": [
|
|
542
542
|
"pkg:npm/%40babel/runtime@7.29.7",
|
|
543
543
|
"pkg:npm/node-addon-api@8.9.0",
|
|
@@ -409,7 +409,9 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
409
409
|
) { result ->
|
|
410
410
|
result.fold(
|
|
411
411
|
onSuccess = { emitSuccess(command, "write") },
|
|
412
|
-
onFailure = { error ->
|
|
412
|
+
onFailure = { error ->
|
|
413
|
+
emitGattOperationFailure(command, "writeFailed", error, "Android GATT write failed")
|
|
414
|
+
}
|
|
413
415
|
)
|
|
414
416
|
}
|
|
415
417
|
radioOperationIds[operationKey(command)] = radioOperationId
|
|
@@ -880,8 +882,8 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
880
882
|
androidGattStatus: Int? = null
|
|
881
883
|
) {
|
|
882
884
|
// Claim before constructing or emitting the terminal. Link loss can race a
|
|
883
|
-
// native
|
|
884
|
-
// publish a result for the same operation.
|
|
885
|
+
// native GATT operation callback; a check followed by a later claim lets
|
|
886
|
+
// both paths publish a result for the same operation.
|
|
885
887
|
if (!claimExactPendingCommand(pendingCommands, operationKey(command), command)) return
|
|
886
888
|
val errorFields = mutableMapOf<Int, ProtocolWireValue>(
|
|
887
889
|
1 to ProtocolWireValue.StringValue(code),
|
package/android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt
CHANGED
|
@@ -49,6 +49,61 @@ internal data class OwnedRadioTeardownResult(
|
|
|
49
49
|
get() = failures.isEmpty()
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** Identity-bearing owner for the legacy UUID-keyed characteristic-write path. */
|
|
53
|
+
internal class AndroidPendingWriteOwner<Key>(
|
|
54
|
+
val deviceKeyUpper: String,
|
|
55
|
+
val attribute: Key,
|
|
56
|
+
val value: ByteArray,
|
|
57
|
+
val callback: (Result<ByteArray?>) -> Unit
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Keeps replacement writes isolated when their legacy string key is reused.
|
|
62
|
+
* Teardown must remove the exact owner it observed, never a newer operation.
|
|
63
|
+
*/
|
|
64
|
+
internal class AndroidPendingWriteRegistry<Key> {
|
|
65
|
+
private val entries = ConcurrentHashMap<String, AndroidPendingWriteOwner<Key>>()
|
|
66
|
+
|
|
67
|
+
fun putIfAbsent(key: String, owner: AndroidPendingWriteOwner<Key>): Boolean =
|
|
68
|
+
entries.putIfAbsent(key, owner) == null
|
|
69
|
+
|
|
70
|
+
fun get(key: String): AndroidPendingWriteOwner<Key>? = entries[key]
|
|
71
|
+
|
|
72
|
+
fun remove(key: String, owner: AndroidPendingWriteOwner<Key>): Boolean =
|
|
73
|
+
entries.remove(key, owner)
|
|
74
|
+
|
|
75
|
+
/** Completes only the currently registered attribute, rejecting stale callbacks. */
|
|
76
|
+
fun complete(
|
|
77
|
+
key: String,
|
|
78
|
+
attribute: Key,
|
|
79
|
+
result: Result<ByteArray?>
|
|
80
|
+
): Boolean {
|
|
81
|
+
val owner = entries[key] ?: return false
|
|
82
|
+
if (owner.attribute !== attribute) return false
|
|
83
|
+
if (!entries.remove(key, owner)) return false
|
|
84
|
+
owner.callback(result)
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fun remove(key: String): AndroidPendingWriteOwner<Key>? = entries.remove(key)
|
|
89
|
+
|
|
90
|
+
fun entriesForDevice(deviceKeyUpper: String): List<Pair<String, AndroidPendingWriteOwner<Key>>> =
|
|
91
|
+
entries.entries
|
|
92
|
+
.filter { entry -> entry.value.deviceKeyUpper == deviceKeyUpper }
|
|
93
|
+
.map { entry -> entry.key to entry.value }
|
|
94
|
+
|
|
95
|
+
fun clear() {
|
|
96
|
+
entries.clear()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Removes an exact-write payload only when it is still the observed payload. */
|
|
101
|
+
internal fun <Key> removeAndroidWritePayloadIfSame(
|
|
102
|
+
values: ConcurrentHashMap<Key, ByteArray>,
|
|
103
|
+
key: Key,
|
|
104
|
+
expected: ByteArray?
|
|
105
|
+
): Boolean = expected != null && values.remove(key, expected)
|
|
106
|
+
|
|
52
107
|
/**
|
|
53
108
|
* Tracks exact native notification registrations by GATT generation.
|
|
54
109
|
*
|
|
@@ -112,7 +167,7 @@ internal class OwnedAndroidSubscriptionOwnership<K> {
|
|
|
112
167
|
/**
|
|
113
168
|
* Preserves the Android GATT callback status when an asynchronous operation
|
|
114
169
|
* terminates. Status 19 means that the peer terminated the link; keeping that
|
|
115
|
-
* distinction typed prevents a
|
|
170
|
+
* distinction typed prevents a GATT operation race from being flattened to a generic
|
|
116
171
|
* platform failure before the later connection-state callback arrives.
|
|
117
172
|
*/
|
|
118
173
|
internal class AndroidGattOperationFailure(
|
|
@@ -135,35 +190,49 @@ internal fun classifyAndroidNotificationRegistrationFailure(
|
|
|
135
190
|
): AndroidGattOperationFailure = AndroidGattOperationFailure(operation, null, isLinkLoss = true)
|
|
136
191
|
|
|
137
192
|
/**
|
|
138
|
-
* Android may deliver an ordinary CCCD
|
|
139
|
-
* authoritative
|
|
140
|
-
* Keep only that provisional result pending long enough for
|
|
141
|
-
* already in flight to claim it. Descriptor writes and
|
|
142
|
-
* are never delayed.
|
|
193
|
+
* Android may deliver an ordinary CCCD or characteristic-write callback
|
|
194
|
+
* immediately before authoritative connection-loss evidence for the same GATT
|
|
195
|
+
* generation. Keep only that provisional result pending long enough for the
|
|
196
|
+
* lifecycle evidence already in flight to claim it. Descriptor writes and
|
|
197
|
+
* already-typed link loss are never delayed.
|
|
143
198
|
*/
|
|
144
199
|
internal fun shouldAwaitAndroidCccdDisconnectEvidence(failure: Throwable): Boolean =
|
|
145
200
|
failure is AndroidGattOperationFailure &&
|
|
146
201
|
failure.operation == "cccd-write" &&
|
|
147
202
|
!failure.isLinkLoss
|
|
148
203
|
|
|
149
|
-
|
|
204
|
+
internal fun shouldAwaitAndroidWriteDisconnectEvidence(failure: Throwable): Boolean =
|
|
205
|
+
failure is AndroidGattOperationFailure &&
|
|
206
|
+
failure.operation == "characteristic-write" &&
|
|
207
|
+
!failure.isLinkLoss
|
|
208
|
+
|
|
209
|
+
private const val ANDROID_GATT_DISCONNECT_EVIDENCE_GRACE_MS = 250L
|
|
150
210
|
|
|
151
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Holds an asynchronous GATT callback failure briefly so a connection-state
|
|
213
|
+
* callback already in flight can provide the authoritative terminal. The
|
|
214
|
+
* first failure is retained and each key can be claimed only once.
|
|
215
|
+
*/
|
|
216
|
+
internal class AndroidGattTerminalArbiter<Key>(
|
|
152
217
|
private val schedule: (delayMs: Long, action: () -> Unit) -> Boolean,
|
|
153
218
|
private val onFallback: (key: Key, failure: AndroidGattOperationFailure) -> Unit
|
|
154
219
|
) {
|
|
155
|
-
private
|
|
220
|
+
private data class DeferredFailure(
|
|
221
|
+
val failure: AndroidGattOperationFailure
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
private val provisionalFailures = ConcurrentHashMap<Key, DeferredFailure>()
|
|
156
225
|
|
|
157
226
|
fun defer(key: Key, failure: AndroidGattOperationFailure) {
|
|
158
|
-
|
|
227
|
+
val deferred = DeferredFailure(failure)
|
|
228
|
+
if (provisionalFailures.putIfAbsent(key, deferred) != null) return
|
|
159
229
|
val fallback: () -> Unit = {
|
|
160
|
-
|
|
161
|
-
if (retained != null) onFallback(key, retained)
|
|
230
|
+
if (provisionalFailures.remove(key, deferred)) onFallback(key, deferred.failure)
|
|
162
231
|
}
|
|
163
|
-
if (!schedule(
|
|
232
|
+
if (!schedule(ANDROID_GATT_DISCONNECT_EVIDENCE_GRACE_MS, fallback)) fallback()
|
|
164
233
|
}
|
|
165
234
|
|
|
166
|
-
fun claim(key: Key): AndroidGattOperationFailure? = provisionalFailures.remove(key)
|
|
235
|
+
fun claim(key: Key): AndroidGattOperationFailure? = provisionalFailures.remove(key)?.failure
|
|
167
236
|
|
|
168
237
|
fun isPending(key: Key): Boolean = provisionalFailures.containsKey(key)
|
|
169
238
|
|
|
@@ -172,6 +241,26 @@ internal class AndroidCccdTerminalArbiter<Key>(
|
|
|
172
241
|
}
|
|
173
242
|
}
|
|
174
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Applies the characteristic-write callback policy at the radio boundary.
|
|
246
|
+
* Returns true when the callback was deferred for lifecycle arbitration.
|
|
247
|
+
*/
|
|
248
|
+
internal fun <Key> AndroidGattTerminalArbiter<Key>.deferCharacteristicWriteFailure(
|
|
249
|
+
key: Key,
|
|
250
|
+
status: Int
|
|
251
|
+
): Boolean {
|
|
252
|
+
val failure = classifyAndroidGattOperationFailure("characteristic-write", status)
|
|
253
|
+
if (!shouldAwaitAndroidWriteDisconnectEvidence(failure)) return false
|
|
254
|
+
defer(key, failure)
|
|
255
|
+
return true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Kept as a descriptive alias for existing CCCD callers and tests. */
|
|
259
|
+
internal typealias AndroidCccdTerminalArbiter<Key> = AndroidGattTerminalArbiter<Key>
|
|
260
|
+
|
|
261
|
+
/** Descriptive alias for characteristic-write terminal arbitration. */
|
|
262
|
+
internal typealias AndroidWriteTerminalArbiter<Key> = AndroidGattTerminalArbiter<Key>
|
|
263
|
+
|
|
175
264
|
/** Synchronous API rejection after local CCCD registration, before Android starts ATT work. */
|
|
176
265
|
internal class AndroidCccdSubmissionFailure(
|
|
177
266
|
val platformStatus: Int?
|
|
@@ -361,8 +450,8 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
361
450
|
private val pendingPhyRequests = ConcurrentHashMap<String, (Result<OwnedAndroidPhy?>) -> Unit>()
|
|
362
451
|
private val pendingDesc = ConcurrentHashMap<String, (Result<Unit>) -> Unit>()
|
|
363
452
|
private val pendingDescRead = ConcurrentHashMap<String, (Result<ByteArray?>) -> Unit>()
|
|
364
|
-
/**
|
|
365
|
-
private val
|
|
453
|
+
/** Identity-bearing owners for the legacy UUID-keyed write callback path. */
|
|
454
|
+
private val pendingWriteRegistry = AndroidPendingWriteRegistry<BluetoothGattCharacteristic>()
|
|
366
455
|
private val pendingBondPairs =
|
|
367
456
|
ConcurrentHashMap<String, (String, OwnedAndroidSecurityState) -> Unit>()
|
|
368
457
|
|
|
@@ -404,6 +493,22 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
404
493
|
completeExactUnit(pending, Result.failure(failure))
|
|
405
494
|
}
|
|
406
495
|
)
|
|
496
|
+
private val exactWriteTerminalArbiter = AndroidWriteTerminalArbiter<BluetoothGattCharacteristic>(
|
|
497
|
+
schedule = { delayMs, action -> mainHandler.postDelayed(action, delayMs) },
|
|
498
|
+
onFallback = fallback@ { characteristic, failure ->
|
|
499
|
+
val observedPayload = exactWriteValues[characteristic]
|
|
500
|
+
val pending = exactWritePending.remove(characteristic) ?: return@fallback
|
|
501
|
+
removeAndroidWritePayloadIfSame(exactWriteValues, characteristic, observedPayload)
|
|
502
|
+
completeExactByte(pending, Result.failure(failure))
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
private val writeTerminalArbiter = AndroidWriteTerminalArbiter<String>(
|
|
506
|
+
schedule = { delayMs, action -> mainHandler.postDelayed(action, delayMs) },
|
|
507
|
+
onFallback = fallback@ { key, failure ->
|
|
508
|
+
val owner = pendingWriteRegistry.remove(key) ?: return@fallback
|
|
509
|
+
owner.callback(Result.failure(failure))
|
|
510
|
+
}
|
|
511
|
+
)
|
|
407
512
|
private val exactDescriptorReadPending = ConcurrentHashMap<BluetoothGattDescriptor, ExactBytePending>()
|
|
408
513
|
private val exactDescriptorWritePending = ConcurrentHashMap<BluetoothGattDescriptor, ExactUnitPending>()
|
|
409
514
|
private val activeNativeSubscriptionOwnership =
|
|
@@ -1302,23 +1407,29 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
1302
1407
|
else BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
1303
1408
|
ch.writeType = writeType
|
|
1304
1409
|
val key = pendingCharKey("write", deviceId, serviceUuid, charUuid)
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1410
|
+
val owner = AndroidPendingWriteOwner(
|
|
1411
|
+
deviceKeyUpper = deviceId.uppercase(),
|
|
1412
|
+
attribute = ch,
|
|
1413
|
+
value = value.copyOf()
|
|
1414
|
+
) { r ->
|
|
1308
1415
|
if (!token.isPubliclySettled()) {
|
|
1309
1416
|
onResult(r)
|
|
1310
1417
|
}
|
|
1311
1418
|
done()
|
|
1312
1419
|
}
|
|
1420
|
+
if (!pendingWriteRegistry.putIfAbsent(key, owner)) {
|
|
1421
|
+
onResult(Result.failure(IllegalStateException("characteristic write is already pending")))
|
|
1422
|
+
done()
|
|
1423
|
+
return@enqueue
|
|
1424
|
+
}
|
|
1313
1425
|
if (Build.VERSION.SDK_INT >= 33) {
|
|
1314
1426
|
val status = gatt.writeCharacteristic(ch, value, writeType)
|
|
1315
1427
|
if (!acceptApi33WriteStatus(status)) {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
done()
|
|
1428
|
+
if (pendingWriteRegistry.remove(key, owner)) {
|
|
1429
|
+
owner.callback(
|
|
1430
|
+
Result.failure(classifyAndroidGattOperationFailure("characteristic-write", status))
|
|
1431
|
+
)
|
|
1432
|
+
}
|
|
1322
1433
|
}
|
|
1323
1434
|
} else {
|
|
1324
1435
|
@Suppress("DEPRECATION")
|
|
@@ -1326,10 +1437,9 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
1326
1437
|
@Suppress("DEPRECATION")
|
|
1327
1438
|
val started = gatt.writeCharacteristic(ch)
|
|
1328
1439
|
if (!started) {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
done()
|
|
1440
|
+
if (pendingWriteRegistry.remove(key, owner)) {
|
|
1441
|
+
owner.callback(Result.failure(IllegalStateException("writeCharacteristic failed to start")))
|
|
1442
|
+
}
|
|
1333
1443
|
}
|
|
1334
1444
|
}
|
|
1335
1445
|
}
|
|
@@ -1416,11 +1526,12 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
1416
1526
|
if (Build.VERSION.SDK_INT >= 33) {
|
|
1417
1527
|
val status = gatt.writeCharacteristic(characteristic, value, writeType)
|
|
1418
1528
|
if (!acceptApi33WriteStatus(status)) {
|
|
1419
|
-
exactWriteValues
|
|
1529
|
+
val observedPayload = exactWriteValues[characteristic]
|
|
1420
1530
|
if (exactWritePending.remove(characteristic, pending)) {
|
|
1531
|
+
removeAndroidWritePayloadIfSame(exactWriteValues, characteristic, observedPayload)
|
|
1421
1532
|
completeExactByte(
|
|
1422
1533
|
pending,
|
|
1423
|
-
Result.failure(
|
|
1534
|
+
Result.failure(classifyAndroidGattOperationFailure("characteristic-write", status))
|
|
1424
1535
|
)
|
|
1425
1536
|
}
|
|
1426
1537
|
}
|
|
@@ -1430,8 +1541,9 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
1430
1541
|
@Suppress("DEPRECATION")
|
|
1431
1542
|
val started = gatt.writeCharacteristic(characteristic)
|
|
1432
1543
|
if (!started) {
|
|
1433
|
-
exactWriteValues
|
|
1544
|
+
val observedPayload = exactWriteValues[characteristic]
|
|
1434
1545
|
if (exactWritePending.remove(characteristic, pending)) {
|
|
1546
|
+
removeAndroidWritePayloadIfSame(exactWriteValues, characteristic, observedPayload)
|
|
1435
1547
|
completeExactByte(
|
|
1436
1548
|
pending,
|
|
1437
1549
|
Result.failure(IllegalStateException("writeCharacteristic failed to start"))
|
|
@@ -2166,12 +2278,14 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2166
2278
|
pendingPhyRequests.clear()
|
|
2167
2279
|
pendingDesc.clear()
|
|
2168
2280
|
pendingDescRead.clear()
|
|
2169
|
-
|
|
2281
|
+
pendingWriteRegistry.clear()
|
|
2170
2282
|
exactReadPending.clear()
|
|
2171
2283
|
exactWritePending.clear()
|
|
2172
2284
|
exactWriteValues.clear()
|
|
2173
2285
|
exactCccdPending.clear()
|
|
2174
2286
|
exactCccdTerminalArbiter.clear()
|
|
2287
|
+
exactWriteTerminalArbiter.clear()
|
|
2288
|
+
writeTerminalArbiter.clear()
|
|
2175
2289
|
exactDescriptorReadPending.clear()
|
|
2176
2290
|
exactDescriptorWritePending.clear()
|
|
2177
2291
|
activeNativeSubscriptionOwnership.clear()
|
|
@@ -2240,9 +2354,10 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2240
2354
|
val failBytes = Result.failure<ByteArray?>(IllegalStateException(reason))
|
|
2241
2355
|
val failInt = Result.failure<Int>(IllegalStateException(reason))
|
|
2242
2356
|
val failUnit = Result.failure<Unit>(IllegalStateException(reason))
|
|
2243
|
-
// A peer link-loss callback can win the race with
|
|
2244
|
-
// that exact Android status on pending CCCD work so the
|
|
2245
|
-
// reports connectionLost instead of flattening the race
|
|
2357
|
+
// A peer link-loss callback can win the race with an asynchronous GATT
|
|
2358
|
+
// callback. Keep that exact Android status on pending CCCD work so the
|
|
2359
|
+
// protocol boundary reports connectionLost instead of flattening the race
|
|
2360
|
+
// to platformFailure.
|
|
2246
2361
|
val failCccd: Result<Unit> = if (gattStatus == ANDROID_GATT_LINK_LOSS_STATUS) {
|
|
2247
2362
|
Result.failure(classifyAndroidGattOperationFailure("cccd-write", gattStatus))
|
|
2248
2363
|
} else {
|
|
@@ -2251,11 +2366,12 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2251
2366
|
pending.keys
|
|
2252
2367
|
.filter { key ->
|
|
2253
2368
|
keyBelongsToDevice(key, "discover", deviceKeyUpper) ||
|
|
2254
|
-
keyBelongsToDevice(key, "read", deviceKeyUpper)
|
|
2255
|
-
keyBelongsToDevice(key, "write", deviceKeyUpper)
|
|
2369
|
+
keyBelongsToDevice(key, "read", deviceKeyUpper)
|
|
2256
2370
|
}
|
|
2257
2371
|
.toList()
|
|
2258
|
-
.forEach { key ->
|
|
2372
|
+
.forEach { key ->
|
|
2373
|
+
pending.remove(key)?.invoke(failBytes)
|
|
2374
|
+
}
|
|
2259
2375
|
pendingMtu.remove("mtu:$deviceKeyUpper")?.invoke(failInt)
|
|
2260
2376
|
pendingRssi.remove("rssi:$deviceKeyUpper")?.invoke(failInt)
|
|
2261
2377
|
pendingPhyReads.remove("phyRead:$deviceKeyUpper")?.invoke(
|
|
@@ -2277,10 +2393,14 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2277
2393
|
.filter { key -> keyBelongsToDevice(key, "descRead", deviceKeyUpper) }
|
|
2278
2394
|
.toList()
|
|
2279
2395
|
.forEach { key -> pendingDescRead.remove(key)?.invoke(failBytes) }
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2396
|
+
// Remove only the owner observed by this teardown. A queued replacement
|
|
2397
|
+
// may reuse the same UUID key while the old disconnect callback drains.
|
|
2398
|
+
pendingWriteRegistry.entriesForDevice(deviceKeyUpper).forEach { (key, owner) ->
|
|
2399
|
+
if (pendingWriteRegistry.remove(key, owner)) {
|
|
2400
|
+
writeTerminalArbiter.claim(key)
|
|
2401
|
+
owner.callback(failBytes)
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2284
2404
|
exactReadPending.entries
|
|
2285
2405
|
.filter { it.value.deviceKeyUpper == deviceKeyUpper }
|
|
2286
2406
|
.forEach { entry ->
|
|
@@ -2291,8 +2411,10 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2291
2411
|
exactWritePending.entries
|
|
2292
2412
|
.filter { it.value.deviceKeyUpper == deviceKeyUpper }
|
|
2293
2413
|
.forEach { entry ->
|
|
2294
|
-
exactWriteValues
|
|
2414
|
+
val observedPayload = exactWriteValues[entry.key]
|
|
2295
2415
|
if (exactWritePending.remove(entry.key, entry.value)) {
|
|
2416
|
+
removeAndroidWritePayloadIfSame(exactWriteValues, entry.key, observedPayload)
|
|
2417
|
+
exactWriteTerminalArbiter.claim(entry.key)
|
|
2296
2418
|
completeExactByte(entry.value, failBytes)
|
|
2297
2419
|
}
|
|
2298
2420
|
}
|
|
@@ -2826,34 +2948,48 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2826
2948
|
status: Int
|
|
2827
2949
|
) {
|
|
2828
2950
|
if (!isCurrentGattCallback(gatt)) return
|
|
2829
|
-
|
|
2951
|
+
// A provisional callback failure must remain pending while lifecycle
|
|
2952
|
+
// evidence already in flight gets a chance to claim the terminal.
|
|
2953
|
+
if (exactWriteTerminalArbiter.isPending(characteristic)) return
|
|
2954
|
+
exactWritePending[characteristic]?.let { pending ->
|
|
2955
|
+
val observedPayload = exactWriteValues[characteristic]
|
|
2830
2956
|
if (!isCurrentGatt(pending.deviceKeyUpper, gatt, pending.gattGeneration)) {
|
|
2831
|
-
|
|
2832
|
-
|
|
2957
|
+
if (exactWritePending.remove(characteristic, pending)) {
|
|
2958
|
+
exactWriteTerminalArbiter.claim(characteristic)
|
|
2959
|
+
removeAndroidWritePayloadIfSame(exactWriteValues, characteristic, observedPayload)
|
|
2960
|
+
pending.done()
|
|
2961
|
+
}
|
|
2833
2962
|
return@let
|
|
2834
2963
|
}
|
|
2835
|
-
val written = exactWriteValues.remove(characteristic)
|
|
2836
2964
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2837
|
-
|
|
2965
|
+
if (exactWritePending.remove(characteristic, pending)) {
|
|
2966
|
+
removeAndroidWritePayloadIfSame(exactWriteValues, characteristic, observedPayload)
|
|
2967
|
+
completeExactByte(pending, Result.success(observedPayload?.copyOf()))
|
|
2968
|
+
}
|
|
2838
2969
|
} else {
|
|
2839
|
-
|
|
2970
|
+
if (exactWriteTerminalArbiter.deferCharacteristicWriteFailure(characteristic, status)) {
|
|
2971
|
+
return@let
|
|
2972
|
+
}
|
|
2973
|
+
val failure = classifyAndroidGattOperationFailure("characteristic-write", status)
|
|
2974
|
+
if (exactWritePending.remove(characteristic, pending)) {
|
|
2975
|
+
removeAndroidWritePayloadIfSame(exactWriteValues, characteristic, observedPayload)
|
|
2976
|
+
completeExactByte(pending, Result.failure(failure))
|
|
2977
|
+
}
|
|
2840
2978
|
}
|
|
2841
2979
|
return
|
|
2842
2980
|
}
|
|
2843
2981
|
val id = gatt.device.address.uppercase()
|
|
2844
2982
|
val key = charPendingKeyFromGatt("write", id, characteristic) ?: return
|
|
2845
|
-
|
|
2983
|
+
if (writeTerminalArbiter.isPending(key)) return
|
|
2846
2984
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2847
|
-
//
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
@Suppress("DEPRECATION")
|
|
2852
|
-
characteristic.value
|
|
2853
|
-
}
|
|
2854
|
-
pending.remove(key)?.invoke(Result.success(value))
|
|
2985
|
+
// The owner retains the API-33 payload and rejects stale callbacks
|
|
2986
|
+
// after a same-key replacement has taken ownership.
|
|
2987
|
+
val owner = pendingWriteRegistry.get(key) ?: return
|
|
2988
|
+
pendingWriteRegistry.complete(key, characteristic, Result.success(owner.value.copyOf()))
|
|
2855
2989
|
} else {
|
|
2856
|
-
|
|
2990
|
+
if (writeTerminalArbiter.deferCharacteristicWriteFailure(key, status)) return
|
|
2991
|
+
val failure = classifyAndroidGattOperationFailure("characteristic-write", status)
|
|
2992
|
+
pendingWriteRegistry.complete(key, characteristic, Result.failure(failure))
|
|
2857
2993
|
}
|
|
2858
2994
|
}
|
|
2859
2995
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
package com.sfourdrinier.unifiedblemanager.protocol
|
|
4
4
|
|
|
5
5
|
import com.sfourdrinier.unifiedblemanager.protocol.generated.RecordKind
|
|
6
|
+
import com.sfourdrinier.unifiedblemanager.radio.deferCharacteristicWriteFailure
|
|
6
7
|
import com.sfourdrinier.unifiedblemanager.radio.nextUuidOccurrence
|
|
7
8
|
import com.sfourdrinier.unifiedblemanager.radio.resolveUuidOccurrence
|
|
8
9
|
import com.sfourdrinier.unifiedblemanager.radio.OwnedAndroidGattRadio
|
|
@@ -18,9 +19,14 @@ import com.sfourdrinier.unifiedblemanager.radio.AndroidGattOperationFailure
|
|
|
18
19
|
import com.sfourdrinier.unifiedblemanager.radio.AndroidCccdSubmissionFailure
|
|
19
20
|
import com.sfourdrinier.unifiedblemanager.radio.AndroidNotificationRollbackRejected
|
|
20
21
|
import com.sfourdrinier.unifiedblemanager.radio.AndroidCccdTerminalArbiter
|
|
22
|
+
import com.sfourdrinier.unifiedblemanager.radio.AndroidWriteTerminalArbiter
|
|
23
|
+
import com.sfourdrinier.unifiedblemanager.radio.AndroidPendingWriteOwner
|
|
24
|
+
import com.sfourdrinier.unifiedblemanager.radio.AndroidPendingWriteRegistry
|
|
25
|
+
import com.sfourdrinier.unifiedblemanager.radio.removeAndroidWritePayloadIfSame
|
|
21
26
|
import com.sfourdrinier.unifiedblemanager.radio.OwnedRadioTeardownFailure
|
|
22
27
|
import com.sfourdrinier.unifiedblemanager.radio.androidGattTerminalResult
|
|
23
28
|
import com.sfourdrinier.unifiedblemanager.radio.shouldAwaitAndroidCccdDisconnectEvidence
|
|
29
|
+
import com.sfourdrinier.unifiedblemanager.radio.shouldAwaitAndroidWriteDisconnectEvidence
|
|
24
30
|
import org.junit.Assert.assertEquals
|
|
25
31
|
import org.junit.Assert.assertArrayEquals
|
|
26
32
|
import org.junit.Assert.assertFalse
|
|
@@ -132,6 +138,202 @@ class UnifiedBleProtocolAndroidDispatcherLifecycleTest {
|
|
|
132
138
|
assertFalse(arbiter.isPending("cccd"))
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
@Test
|
|
142
|
+
fun pendingWrite133ThenAuthoritativeDisconnectStatus8EmitsOneConnectionLostTerminal() {
|
|
143
|
+
val scheduled = mutableListOf<() -> Unit>()
|
|
144
|
+
val terminalKinds = mutableListOf<String>()
|
|
145
|
+
val arbiter = AndroidWriteTerminalArbiter<String>(
|
|
146
|
+
schedule = { _, action -> scheduled.add(action) },
|
|
147
|
+
onFallback = { _, failure ->
|
|
148
|
+
terminalKinds.add(androidGattOperationFailureCode(failure, "writeFailed"))
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
val writeFailure = classifyAndroidGattOperationFailure("characteristic-write", 133)
|
|
152
|
+
|
|
153
|
+
assertTrue(shouldAwaitAndroidWriteDisconnectEvidence(writeFailure))
|
|
154
|
+
assertFalse(
|
|
155
|
+
shouldAwaitAndroidWriteDisconnectEvidence(
|
|
156
|
+
classifyAndroidGattOperationFailure("characteristic-write", 19)
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
assertFalse(
|
|
160
|
+
shouldAwaitAndroidWriteDisconnectEvidence(
|
|
161
|
+
classifyAndroidGattOperationFailure("descriptor-write", 133)
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
assertTrue(arbiter.deferCharacteristicWriteFailure("write", writeFailure.gattStatus!!))
|
|
165
|
+
|
|
166
|
+
// The lifecycle callback is authoritative even though Android reports the
|
|
167
|
+
// transport timeout as status 8 rather than the write callback's 133.
|
|
168
|
+
val pendingCommands = mutableSetOf("write")
|
|
169
|
+
val disconnectStatus = 8
|
|
170
|
+
if (disconnectStatus == 8 && pendingCommands.remove("write")) {
|
|
171
|
+
terminalKinds.add("connectionLost")
|
|
172
|
+
}
|
|
173
|
+
val claimedFailure = arbiter.claim("write")
|
|
174
|
+
assertEquals("characteristic-write", claimedFailure?.operation)
|
|
175
|
+
assertEquals(133, claimedFailure?.gattStatus)
|
|
176
|
+
scheduled.single().invoke()
|
|
177
|
+
|
|
178
|
+
assertEquals(listOf("connectionLost"), terminalKinds)
|
|
179
|
+
val connection = ProtocolWireRecord(
|
|
180
|
+
RecordKind.CONNECTION_PATH,
|
|
181
|
+
mapOf(1 to ProtocolWireValue.RecordValue(ProtocolWireRecord(RecordKind.ATTACHMENT, emptyMap())))
|
|
182
|
+
)
|
|
183
|
+
val event = connectionLostEvent(1L, connection, disconnectStatus, 0L, 0L)
|
|
184
|
+
val error = event.requiredRecord(14)
|
|
185
|
+
assertEquals(ProtocolWireValue.StringValue("connectionLost"), event.fields[3])
|
|
186
|
+
assertEquals(ProtocolWireValue.SignedIntegerValue(8L), error.fields[8])
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@Test
|
|
190
|
+
fun isolatedWrite133AfterGraceEmitsPlatformFailureRetainingOperationAndStatus() {
|
|
191
|
+
val scheduled = mutableListOf<() -> Unit>()
|
|
192
|
+
val terminalKinds = mutableListOf<String>()
|
|
193
|
+
var fallback: AndroidGattOperationFailure? = null
|
|
194
|
+
val arbiter = AndroidWriteTerminalArbiter<String>(
|
|
195
|
+
schedule = { _, action -> scheduled.add(action) },
|
|
196
|
+
onFallback = { _, failure ->
|
|
197
|
+
fallback = failure
|
|
198
|
+
terminalKinds.add(androidGattOperationFailureCode(failure, "writeFailed"))
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
val writeFailure = classifyAndroidGattOperationFailure("characteristic-write", 133)
|
|
202
|
+
|
|
203
|
+
assertTrue(arbiter.deferCharacteristicWriteFailure("write", writeFailure.gattStatus!!))
|
|
204
|
+
scheduled.single().invoke()
|
|
205
|
+
|
|
206
|
+
assertEquals(listOf("writeFailed"), terminalKinds)
|
|
207
|
+
assertTrue(fallback != null)
|
|
208
|
+
assertEquals("characteristic-write", fallback?.operation)
|
|
209
|
+
assertEquals(133, fallback?.gattStatus)
|
|
210
|
+
assertEquals(133, androidGattOperationFailureStatus(fallback!!))
|
|
211
|
+
assertEquals("writeFailed", androidGattOperationFailureCode(fallback!!, "writeFailed"))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@Test
|
|
215
|
+
fun writeTerminalArbiterPreventsDuplicateTerminalAfterDisconnectAndLateWriteCallback() {
|
|
216
|
+
val scheduled = mutableListOf<() -> Unit>()
|
|
217
|
+
val terminalKinds = mutableListOf<String>()
|
|
218
|
+
val arbiter = AndroidWriteTerminalArbiter<String>(
|
|
219
|
+
schedule = { _, action -> scheduled.add(action) },
|
|
220
|
+
onFallback = { _, failure ->
|
|
221
|
+
terminalKinds.add(androidGattOperationFailureCode(failure, "writeFailed"))
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
val writeFailure = classifyAndroidGattOperationFailure("characteristic-write", 133)
|
|
225
|
+
|
|
226
|
+
assertTrue(arbiter.deferCharacteristicWriteFailure("write", writeFailure.gattStatus!!))
|
|
227
|
+
assertTrue(arbiter.isPending("write"))
|
|
228
|
+
val claimedFailure = arbiter.claim("write")
|
|
229
|
+
assertEquals("characteristic-write", claimedFailure?.operation)
|
|
230
|
+
assertEquals(133, claimedFailure?.gattStatus)
|
|
231
|
+
assertTrue(!arbiter.isPending("write"))
|
|
232
|
+
scheduled.single().invoke()
|
|
233
|
+
// A late duplicate callback cannot reclaim the already-settled terminal.
|
|
234
|
+
assertTrue(arbiter.claim("write") == null)
|
|
235
|
+
|
|
236
|
+
val pendingCommands = mutableSetOf("write")
|
|
237
|
+
if (pendingCommands.remove("write")) terminalKinds.add("connectionLost")
|
|
238
|
+
if (pendingCommands.remove("write")) terminalKinds.add("writeFailed")
|
|
239
|
+
assertEquals(listOf("connectionLost"), terminalKinds)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@Test
|
|
243
|
+
fun pendingWriteReplacementIsIdentitySafeDuringOldDisconnectTeardownAndLateCallback() {
|
|
244
|
+
val registry = AndroidPendingWriteRegistry<String>()
|
|
245
|
+
val terminals = mutableListOf<String>()
|
|
246
|
+
val oldAttribute = "old-characteristic"
|
|
247
|
+
val replacementAttribute = "replacement-characteristic"
|
|
248
|
+
val oldOwner = AndroidPendingWriteOwner(
|
|
249
|
+
deviceKeyUpper = "AA:BB",
|
|
250
|
+
attribute = oldAttribute,
|
|
251
|
+
value = byteArrayOf(1)
|
|
252
|
+
) { terminals.add("old") }
|
|
253
|
+
val replacementOwner = AndroidPendingWriteOwner(
|
|
254
|
+
deviceKeyUpper = "AA:BB",
|
|
255
|
+
attribute = replacementAttribute,
|
|
256
|
+
value = byteArrayOf(2)
|
|
257
|
+
) { terminals.add("replacement") }
|
|
258
|
+
|
|
259
|
+
assertTrue(registry.putIfAbsent("write:key", oldOwner))
|
|
260
|
+
// Teardown observed the old owner, but a replacement acquired the key
|
|
261
|
+
// before the stale disconnect work finished.
|
|
262
|
+
assertTrue(registry.remove("write:key", oldOwner))
|
|
263
|
+
assertTrue(registry.putIfAbsent("write:key", replacementOwner))
|
|
264
|
+
assertFalse(registry.remove("write:key", oldOwner))
|
|
265
|
+
assertFalse(
|
|
266
|
+
registry.complete(
|
|
267
|
+
"write:key",
|
|
268
|
+
oldAttribute,
|
|
269
|
+
Result.success(byteArrayOf(9))
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
assertEquals(listOf<String>(), terminals)
|
|
273
|
+
assertTrue(registry.complete("write:key", replacementAttribute, Result.success(byteArrayOf(2))))
|
|
274
|
+
assertEquals(listOf("replacement"), terminals)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@Test
|
|
278
|
+
fun exactWriteTeardownOrRejectionCannotDeleteReplacementPayload() {
|
|
279
|
+
val values = ConcurrentHashMap<String, ByteArray>()
|
|
280
|
+
val oldPayload = byteArrayOf(1)
|
|
281
|
+
val replacementPayload = byteArrayOf(2)
|
|
282
|
+
values["write:key"] = oldPayload
|
|
283
|
+
|
|
284
|
+
// Capture the old payload before conditional owner removal, then let the
|
|
285
|
+
// replacement acquire the same key before stale cleanup runs.
|
|
286
|
+
val observed = values["write:key"]
|
|
287
|
+
values["write:key"] = replacementPayload
|
|
288
|
+
assertFalse(removeAndroidWritePayloadIfSame(values, "write:key", observed))
|
|
289
|
+
assertTrue(values["write:key"] === replacementPayload)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
@Test
|
|
293
|
+
fun writeArbiterClearCannotLetPostedFallbackSettleReusedKey() {
|
|
294
|
+
val scheduled = mutableListOf<() -> Unit>()
|
|
295
|
+
val fallbacks = mutableListOf<AndroidGattOperationFailure>()
|
|
296
|
+
val arbiter = AndroidWriteTerminalArbiter<String>(
|
|
297
|
+
schedule = { _, action -> scheduled.add(action) },
|
|
298
|
+
onFallback = { _, failure -> fallbacks.add(failure) }
|
|
299
|
+
)
|
|
300
|
+
val first = classifyAndroidGattOperationFailure("characteristic-write", 133)
|
|
301
|
+
val replacement = classifyAndroidGattOperationFailure("characteristic-write", 257)
|
|
302
|
+
|
|
303
|
+
arbiter.defer("write", first)
|
|
304
|
+
arbiter.clear()
|
|
305
|
+
arbiter.defer("write", replacement)
|
|
306
|
+
scheduled[0].invoke()
|
|
307
|
+
assertTrue(arbiter.isPending("write"))
|
|
308
|
+
assertEquals(listOf<AndroidGattOperationFailure>(), fallbacks)
|
|
309
|
+
scheduled[1].invoke()
|
|
310
|
+
assertEquals(listOf(replacement), fallbacks)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@Test
|
|
314
|
+
fun characteristicWriteCallbacksAndDispatcherPreserveTypedFailurePolicy() {
|
|
315
|
+
val radio = readAndroidSource(
|
|
316
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
|
|
317
|
+
)
|
|
318
|
+
val callback = radio.substring(
|
|
319
|
+
radio.indexOf("override fun onCharacteristicWrite"),
|
|
320
|
+
radio.indexOf("@Deprecated", radio.indexOf("override fun onCharacteristicWrite"))
|
|
321
|
+
)
|
|
322
|
+
assertEquals(2, Regex("deferCharacteristicWriteFailure\\(").findAll(callback).count())
|
|
323
|
+
assertTrue(callback.contains("pendingWriteRegistry.complete(key, characteristic"))
|
|
324
|
+
assertTrue(callback.contains("classifyAndroidGattOperationFailure(\"characteristic-write\", status)"))
|
|
325
|
+
|
|
326
|
+
val dispatcher = readAndroidSource(
|
|
327
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt"
|
|
328
|
+
)
|
|
329
|
+
val write = dispatcher.substring(
|
|
330
|
+
dispatcher.indexOf("private fun write(command: ProtocolWireRecord)"),
|
|
331
|
+
dispatcher.indexOf("private fun writeDescriptor(command: ProtocolWireRecord)")
|
|
332
|
+
)
|
|
333
|
+
assertTrue(write.contains("emitGattOperationFailure(command, \"writeFailed\", error"))
|
|
334
|
+
assertFalse(write.contains("emitFailure(command, \"writeFailed\", error.message"))
|
|
335
|
+
}
|
|
336
|
+
|
|
135
337
|
@Test
|
|
136
338
|
fun dispatcherOnlyMapsTypedLinkLossToConnectionLost() {
|
|
137
339
|
val linkLoss = classifyAndroidGattOperationFailure("cccd-write", 19)
|
package/docs/ELECTRON.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Main owns the radio. The renderer uses a versioned IPC client and never loads a native addon.
|
|
6
6
|
|
|
7
|
-
This source targets `4.0.
|
|
7
|
+
This source targets `4.0.24`. Tagged releases ship Node-API v8 prebuilds covering macOS and Windows `arm64`/`x64` for both Node and modern Electron.
|
|
8
8
|
|
|
9
9
|
`unified-ble-manager/electron/main` and
|
|
10
10
|
`unified-ble-manager/electron/renderer` are the only Electron entrypoints.
|
package/docs/EXPO_PLUGIN.md
CHANGED
|
@@ -7,7 +7,7 @@ start a radio, request runtime permissions during prebuild, or prove physical
|
|
|
7
7
|
radio/restoration reliability. Expo Go is not a supported BLE execution
|
|
8
8
|
environment because it cannot contain this native module.
|
|
9
9
|
|
|
10
|
-
Use the v2 plugin options in this `4.0.
|
|
10
|
+
Use the v2 plugin options in this `4.0.24` source. Those options match
|
|
11
11
|
the schema introduced at `4.0.0-rc.4`. Expo Go cannot load this native module.
|
|
12
12
|
|
|
13
13
|
## Installation and development build
|
package/docs/GAPS.4.0.md
CHANGED
|
@@ -14,7 +14,7 @@ This file tracks platform code, CI, package, lab, and live-radio evidence. It do
|
|
|
14
14
|
|
|
15
15
|
The clean-baseline contract, unified core, public manager, deterministic backend, TCK, native protocol, first-party backend implementations, host-isolated package exports, SDK/CLI, and legacy-absence gates exist in the 4.0 source. Passing deterministic, compile, ABI, or package tests are implementation proof; they do not become physical-radio support evidence unless a retained record proves the corresponding live scenario.
|
|
16
16
|
|
|
17
|
-
This source targets `unified-ble-manager@4.0.
|
|
17
|
+
This source targets `unified-ble-manager@4.0.24`; the npm registry and release
|
|
18
18
|
provenance, not a source version string, determine whether it is published.
|
|
19
19
|
Earlier stable releases remain immutable published history. Backend support labels remain
|
|
20
20
|
evidence-derived. This package does not rewrite the evidence inventory: a backend remains at the support level
|
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
This page gets you to a first scan, connect, read, notify, and teardown on React Native. Other hosts are linked at the bottom. The root import does not turn Bluetooth on.
|
|
6
6
|
|
|
7
|
-
This source targets `4.0.
|
|
7
|
+
This source targets `4.0.24`; verify the published version in the npm registry.
|
|
8
8
|
|
|
9
9
|
## Pick a host
|
|
10
10
|
|
|
@@ -38,11 +38,11 @@ request runtime permissions on Android 12+, add
|
|
|
38
38
|
#### Expo / CNG v2
|
|
39
39
|
|
|
40
40
|
The Expo v2 schema and `unified-ble-manager/expo` factory are in this source.
|
|
41
|
-
After the npm registry lists `4.0.
|
|
41
|
+
After the npm registry lists `4.0.24`, install that exact version and keep it in
|
|
42
42
|
your lockfile while validating the native build:
|
|
43
43
|
|
|
44
44
|
```sh
|
|
45
|
-
pnpm add unified-ble-manager@4.0.
|
|
45
|
+
pnpm add unified-ble-manager@4.0.24
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
The package does not run in Expo Go.
|
package/docs/NODE.md
CHANGED
|
@@ -10,7 +10,7 @@ The root import does not open an adapter. Pick one backend:
|
|
|
10
10
|
| `unified-ble-manager/node/winrt` | Windows |
|
|
11
11
|
| `unified-ble-manager/node/bluez` | Linux (needs `dbus-next@^0.10.2` in the app) |
|
|
12
12
|
|
|
13
|
-
This source targets `4.0.
|
|
13
|
+
This source targets `4.0.24`. Tagged releases ship Node-API v8 prebuilds for macOS and Windows on `arm64` and `x64`. A normal install should not compile native code. BlueZ talks D-Bus and has no addon.
|
|
14
14
|
|
|
15
15
|
## One-call factories
|
|
16
16
|
|
package/docs/WEB.md
CHANGED
|
@@ -6,7 +6,7 @@ Use `unified-ble-manager/web` to run the public UBM manager directly in a browse
|
|
|
6
6
|
|
|
7
7
|
The complete runnable TypeScript/Vite application is in [`example-web/`](../example-web/).
|
|
8
8
|
|
|
9
|
-
This guide targets `4.0.
|
|
9
|
+
This guide targets `4.0.24`. Web Bluetooth support still depends on the browser, operating system, adapter, and peripheral. UBM reports those runtime boundaries; it does not fabricate a fallback backend.
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
@@ -6,7 +6,7 @@ This page is generated by `scripts/docs/generate-platform-support.js` from valid
|
|
|
6
6
|
|
|
7
7
|
## Current package
|
|
8
8
|
|
|
9
|
-
`unified-ble-manager@4.0.
|
|
9
|
+
`unified-ble-manager@4.0.24`
|
|
10
10
|
|
|
11
11
|
No evidence record is bound to this exact package version and artifact. Consequently, this generated page makes no Preview, Live Preview, Supported, or Reliability-qualified platform claim for the current package.
|
|
12
12
|
|
|
@@ -7,5 +7,5 @@ exports.UNIFIED_BLE_IMPLEMENTATION_VERSION = void 0;
|
|
|
7
7
|
// src/implementation-version.ts
|
|
8
8
|
|
|
9
9
|
/** The one production version stamped into every first-party backend identity. */
|
|
10
|
-
const UNIFIED_BLE_IMPLEMENTATION_VERSION = exports.UNIFIED_BLE_IMPLEMENTATION_VERSION = '4.0.
|
|
10
|
+
const UNIFIED_BLE_IMPLEMENTATION_VERSION = exports.UNIFIED_BLE_IMPLEMENTATION_VERSION = '4.0.24';
|
|
11
11
|
//# sourceMappingURL=implementation-version.js.map
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
// src/implementation-version.ts
|
|
4
4
|
|
|
5
5
|
/** The one production version stamped into every first-party backend identity. */
|
|
6
|
-
export const UNIFIED_BLE_IMPLEMENTATION_VERSION = '4.0.
|
|
6
|
+
export const UNIFIED_BLE_IMPLEMENTATION_VERSION = '4.0.24';
|
|
7
7
|
//# sourceMappingURL=implementation-version.js.map
|
|
@@ -4,7 +4,7 @@ import { type BackendCompatibilityOffer } from '../../backend-contract/primitive
|
|
|
4
4
|
import { type BluezBusKind, type BluezDbusBoundaryFactory } from './bluez-dbus-contract';
|
|
5
5
|
export declare const BLUEZ_BACKEND_ID = "unified-ble:bluez-dbus";
|
|
6
6
|
export declare const BLUEZ_PLATFORM_ID = "unified-ble:linux-bluez";
|
|
7
|
-
export declare const BLUEZ_IMPLEMENTATION_VERSION = "4.0.
|
|
7
|
+
export declare const BLUEZ_IMPLEMENTATION_VERSION = "4.0.24";
|
|
8
8
|
export declare const bluezCompatibility: BackendCompatibilityOffer;
|
|
9
9
|
export interface BluezBackendProviderOptions {
|
|
10
10
|
readonly busKind: BluezBusKind;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type FeatureRegistry } from '../../backend-contract/capabilities';
|
|
2
2
|
export declare const COREBLUETOOTH_BACKEND_ID = "unified-ble:corebluetooth";
|
|
3
3
|
export declare const COREBLUETOOTH_PLATFORM_ID = "unified-ble:macos-corebluetooth";
|
|
4
|
-
export declare const COREBLUETOOTH_IMPLEMENTATION_VERSION = "4.0.
|
|
4
|
+
export declare const COREBLUETOOTH_IMPLEMENTATION_VERSION = "4.0.24";
|
|
5
5
|
/** Identity metadata for a direct-GATT boundary that shares this backend core. */
|
|
6
6
|
export interface DirectGattBackendIdentityOptions {
|
|
7
7
|
readonly registeredBackendId: string;
|
|
@@ -9,7 +9,7 @@ import { ReactNativeAndroidProtocolBoundary } from '../../native-protocol/rn-and
|
|
|
9
9
|
import { ReactNativeRestorationCoordinator, type ReactNativeRestorationActivation, type ReactNativeRestorationBackendProvider } from './react-native-restoration';
|
|
10
10
|
export declare const REACT_NATIVE_ANDROID_BACKEND_ID = "unified-ble:react-native-android";
|
|
11
11
|
export declare const REACT_NATIVE_ANDROID_PLATFORM_ID = "unified-ble:android-gatt";
|
|
12
|
-
export declare const REACT_NATIVE_ANDROID_IMPLEMENTATION_VERSION = "4.0.
|
|
12
|
+
export declare const REACT_NATIVE_ANDROID_IMPLEMENTATION_VERSION = "4.0.24";
|
|
13
13
|
export declare const REACT_NATIVE_ANDROID_DEFAULT_ADAPTER_NATIVE_ID = "android-default-adapter";
|
|
14
14
|
export declare const reactNativeAndroidCompatibility: NativeCompatibilityOffer;
|
|
15
15
|
export interface ReactNativeAndroidBackendProviderOptions {
|
|
@@ -3,7 +3,7 @@ import type { Spec as NativeProtocolControl } from '../../NativeUnifiedBleProtoc
|
|
|
3
3
|
import { type ReactNativeRestorationBackendProvider } from './react-native-restoration';
|
|
4
4
|
export declare const REACT_NATIVE_APPLE_BACKEND_ID = "unified-ble:react-native-apple";
|
|
5
5
|
export declare const REACT_NATIVE_APPLE_PLATFORM_ID = "unified-ble:apple-corebluetooth";
|
|
6
|
-
export declare const REACT_NATIVE_APPLE_IMPLEMENTATION_VERSION = "4.0.
|
|
6
|
+
export declare const REACT_NATIVE_APPLE_IMPLEMENTATION_VERSION = "4.0.24";
|
|
7
7
|
export declare const REACT_NATIVE_APPLE_DEFAULT_ADAPTER_NATIVE_ID = "apple-corebluetooth-default-adapter";
|
|
8
8
|
export declare const reactNativeAppleCompatibility: NativeCompatibilityOffer;
|
|
9
9
|
export interface ReactNativeAppleBackendProviderOptions {
|
|
@@ -3,7 +3,7 @@ import { type BackendCompatibilityOffer } from '../../backend-contract/primitive
|
|
|
3
3
|
import { type WinRtAdapterRecord, type WinRtBoundary } from './winrt-boundary';
|
|
4
4
|
export declare const WINRT_BACKEND_ID = "unified-ble:winrt";
|
|
5
5
|
export declare const WINRT_PLATFORM_ID = "unified-ble:windows-winrt";
|
|
6
|
-
export declare const WINRT_IMPLEMENTATION_VERSION = "4.0.
|
|
6
|
+
export declare const WINRT_IMPLEMENTATION_VERSION = "4.0.24";
|
|
7
7
|
export declare const winRtCompatibility: BackendCompatibilityOffer;
|
|
8
8
|
export interface WinRtBackendProviderOptions {
|
|
9
9
|
readonly boundaryFactory: () => WinRtBoundary;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** The one production version stamped into every first-party backend identity. */
|
|
2
|
-
export declare const UNIFIED_BLE_IMPLEMENTATION_VERSION = "4.0.
|
|
2
|
+
export declare const UNIFIED_BLE_IMPLEMENTATION_VERSION = "4.0.24";
|
|
3
3
|
//# sourceMappingURL=implementation-version.d.ts.map
|
|
@@ -4,7 +4,7 @@ import { type BackendCompatibilityOffer } from '../../backend-contract/primitive
|
|
|
4
4
|
import { type BluezBusKind, type BluezDbusBoundaryFactory } from './bluez-dbus-contract.js';
|
|
5
5
|
export declare const BLUEZ_BACKEND_ID = "unified-ble:bluez-dbus";
|
|
6
6
|
export declare const BLUEZ_PLATFORM_ID = "unified-ble:linux-bluez";
|
|
7
|
-
export declare const BLUEZ_IMPLEMENTATION_VERSION = "4.0.
|
|
7
|
+
export declare const BLUEZ_IMPLEMENTATION_VERSION = "4.0.24";
|
|
8
8
|
export declare const bluezCompatibility: BackendCompatibilityOffer;
|
|
9
9
|
export interface BluezBackendProviderOptions {
|
|
10
10
|
readonly busKind: BluezBusKind;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type FeatureRegistry } from '../../backend-contract/capabilities.js';
|
|
2
2
|
export declare const COREBLUETOOTH_BACKEND_ID = "unified-ble:corebluetooth";
|
|
3
3
|
export declare const COREBLUETOOTH_PLATFORM_ID = "unified-ble:macos-corebluetooth";
|
|
4
|
-
export declare const COREBLUETOOTH_IMPLEMENTATION_VERSION = "4.0.
|
|
4
|
+
export declare const COREBLUETOOTH_IMPLEMENTATION_VERSION = "4.0.24";
|
|
5
5
|
/** Identity metadata for a direct-GATT boundary that shares this backend core. */
|
|
6
6
|
export interface DirectGattBackendIdentityOptions {
|
|
7
7
|
readonly registeredBackendId: string;
|
|
@@ -9,7 +9,7 @@ import { ReactNativeAndroidProtocolBoundary } from '../../native-protocol/rn-and
|
|
|
9
9
|
import { ReactNativeRestorationCoordinator, type ReactNativeRestorationActivation, type ReactNativeRestorationBackendProvider } from './react-native-restoration.js';
|
|
10
10
|
export declare const REACT_NATIVE_ANDROID_BACKEND_ID = "unified-ble:react-native-android";
|
|
11
11
|
export declare const REACT_NATIVE_ANDROID_PLATFORM_ID = "unified-ble:android-gatt";
|
|
12
|
-
export declare const REACT_NATIVE_ANDROID_IMPLEMENTATION_VERSION = "4.0.
|
|
12
|
+
export declare const REACT_NATIVE_ANDROID_IMPLEMENTATION_VERSION = "4.0.24";
|
|
13
13
|
export declare const REACT_NATIVE_ANDROID_DEFAULT_ADAPTER_NATIVE_ID = "android-default-adapter";
|
|
14
14
|
export declare const reactNativeAndroidCompatibility: NativeCompatibilityOffer;
|
|
15
15
|
export interface ReactNativeAndroidBackendProviderOptions {
|
|
@@ -3,7 +3,7 @@ import type { Spec as NativeProtocolControl } from '../../NativeUnifiedBleProtoc
|
|
|
3
3
|
import { type ReactNativeRestorationBackendProvider } from './react-native-restoration.js';
|
|
4
4
|
export declare const REACT_NATIVE_APPLE_BACKEND_ID = "unified-ble:react-native-apple";
|
|
5
5
|
export declare const REACT_NATIVE_APPLE_PLATFORM_ID = "unified-ble:apple-corebluetooth";
|
|
6
|
-
export declare const REACT_NATIVE_APPLE_IMPLEMENTATION_VERSION = "4.0.
|
|
6
|
+
export declare const REACT_NATIVE_APPLE_IMPLEMENTATION_VERSION = "4.0.24";
|
|
7
7
|
export declare const REACT_NATIVE_APPLE_DEFAULT_ADAPTER_NATIVE_ID = "apple-corebluetooth-default-adapter";
|
|
8
8
|
export declare const reactNativeAppleCompatibility: NativeCompatibilityOffer;
|
|
9
9
|
export interface ReactNativeAppleBackendProviderOptions {
|
|
@@ -3,7 +3,7 @@ import { type BackendCompatibilityOffer } from '../../backend-contract/primitive
|
|
|
3
3
|
import { type WinRtAdapterRecord, type WinRtBoundary } from './winrt-boundary.js';
|
|
4
4
|
export declare const WINRT_BACKEND_ID = "unified-ble:winrt";
|
|
5
5
|
export declare const WINRT_PLATFORM_ID = "unified-ble:windows-winrt";
|
|
6
|
-
export declare const WINRT_IMPLEMENTATION_VERSION = "4.0.
|
|
6
|
+
export declare const WINRT_IMPLEMENTATION_VERSION = "4.0.24";
|
|
7
7
|
export declare const winRtCompatibility: BackendCompatibilityOffer;
|
|
8
8
|
export interface WinRtBackendProviderOptions {
|
|
9
9
|
readonly boundaryFactory: () => WinRtBoundary;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** The one production version stamped into every first-party backend identity. */
|
|
2
|
-
export declare const UNIFIED_BLE_IMPLEMENTATION_VERSION = "4.0.
|
|
2
|
+
export declare const UNIFIED_BLE_IMPLEMENTATION_VERSION = "4.0.24";
|
|
3
3
|
//# sourceMappingURL=implementation-version.d.ts.map
|
package/native/PREBUILDS.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"package": "unified-ble-manager@4.0.
|
|
3
|
+
"package": "unified-ble-manager@4.0.24",
|
|
4
4
|
"nodeApiVersion": 8,
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"nodeApiVersion": 8,
|
|
11
11
|
"path": "native/electron/corebluetooth/prebuilds/darwin-arm64/unified_ble_corebluetooth.node",
|
|
12
12
|
"bytes": 332368,
|
|
13
|
-
"sha256": "
|
|
13
|
+
"sha256": "5167159b256e1c76ffc2e4080f124785cebfe3e598451ac0ece0eec74cc97d90"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"backend": "corebluetooth",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"nodeApiVersion": 8,
|
|
20
20
|
"path": "native/electron/corebluetooth/prebuilds/darwin-x64/unified_ble_corebluetooth.node",
|
|
21
21
|
"bytes": 314872,
|
|
22
|
-
"sha256": "
|
|
22
|
+
"sha256": "9deda9a0f8c4e6f07e688fdbeb5b6b631773863dc8b4de4c5bfaae8d4aaa3c9c"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"backend": "winrt",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"nodeApiVersion": 8,
|
|
29
29
|
"path": "native/electron/winrt/prebuilds/win32-arm64/unified_ble_winrt.node",
|
|
30
30
|
"bytes": 644608,
|
|
31
|
-
"sha256": "
|
|
31
|
+
"sha256": "474be5c5179de1eff20f87f88756f4d115412570483a51d78d66605a0f05292d"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"backend": "winrt",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"nodeApiVersion": 8,
|
|
38
38
|
"path": "native/electron/winrt/prebuilds/win32-x64/unified_ble_winrt.node",
|
|
39
39
|
"bytes": 614400,
|
|
40
|
-
"sha256": "
|
|
40
|
+
"sha256": "48d3be097c0faa66ef6cdc53b8fb7b6543e5d71cfa327aaa07b358d2472e3383"
|
|
41
41
|
}
|
|
42
42
|
]
|
|
43
43
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED