unified-ble-manager 4.0.19 → 4.0.22
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 +45 -0
- package/MIGRATION_4.0.md +1 -1
- package/README.md +1 -1
- package/RELEASE.md +53 -2
- package/SBOM.cdx.json +4 -4
- package/THIRD_PARTY_LICENSES.json +1 -1
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt +149 -42
- package/android/src/test/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcherLifecycleTest.kt +291 -11
- 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,51 @@ All notable changes to `unified-ble-manager` are documented here.
|
|
|
6
6
|
|
|
7
7
|
No changes yet.
|
|
8
8
|
|
|
9
|
+
## [4.0.22] - 2026-09-04
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
- Arbitrate an Android asynchronous CCCD failure against an already-in-flight
|
|
14
|
+
disconnect callback for the same GATT generation. Android can deliver status
|
|
15
|
+
133 from `onDescriptorWrite`, accept local notification rollback, and only
|
|
16
|
+
then deliver the authoritative status-19 disconnect; exact subscriptions now
|
|
17
|
+
retain that provisional failure for a bounded 250 ms evidence window so the
|
|
18
|
+
command reports typed `connection.lost` without hiding genuine CCCD errors.
|
|
19
|
+
|
|
20
|
+
## [4.0.21] - 2026-09-04 (unpublished)
|
|
21
|
+
|
|
22
|
+
Publication was cancelled before the npm version check after physical Android
|
|
23
|
+
hardware reproduced a later callback ordering that required the 4.0.22 fix.
|
|
24
|
+
|
|
25
|
+
### Fixes
|
|
26
|
+
|
|
27
|
+
- Preserve Android GATT status 19 when a peer disconnects while an exact or
|
|
28
|
+
non-exact CCCD subscription is awaiting `onDescriptorWrite`. The pending
|
|
29
|
+
subscription now reports typed `connection.lost` instead of generic
|
|
30
|
+
`platform.failure`; asynchronous CCCD status 133 remains an ordinary failure
|
|
31
|
+
when local-registration rollback succeeds.
|
|
32
|
+
- Preserve that typed primary link-loss result when exact CCCD rollback also
|
|
33
|
+
fails; the cleanup failure remains separately reported and retryable.
|
|
34
|
+
- Classify an exact synchronous CCCD descriptor-submission rejection as link
|
|
35
|
+
loss only when the subsequent local-registration rollback is also rejected;
|
|
36
|
+
genuine subscription rejections remain ordinary failures.
|
|
37
|
+
- Apply the same two-signal rule when Android accepts descriptor submission but
|
|
38
|
+
its asynchronous CCCD callback fails before the connection-state callback:
|
|
39
|
+
a rejected rollback proves the link is gone, while a successful rollback
|
|
40
|
+
preserves the original platform failure and GATT status.
|
|
41
|
+
|
|
42
|
+
## [4.0.20] - 2026-09-03
|
|
43
|
+
|
|
44
|
+
### Fixes
|
|
45
|
+
|
|
46
|
+
- Treat Android's synchronous `setCharacteristicNotification(...) == false`
|
|
47
|
+
result as typed `connection.lost` without consulting
|
|
48
|
+
`BluetoothManager.getConnectionState`. Android can reject registration after
|
|
49
|
+
the GATT link is gone while that manager query still reports connected.
|
|
50
|
+
Exact and non-exact notification paths now share this behavior; asynchronous
|
|
51
|
+
CCCD failures retain their native status and remain ordinary subscription
|
|
52
|
+
failures unless Android reports link loss.
|
|
53
|
+
|
|
9
54
|
## [4.0.19] - 2026-09-03
|
|
10
55
|
|
|
11
56
|
### 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.22`. 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.22`. 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
|
@@ -81,7 +81,9 @@ Active `4.0.0-rc.*` release-train candidates publish to npm `latest` so a bare `
|
|
|
81
81
|
|
|
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
|
-
once tagged. Stable `4.0.0` through `4.0.
|
|
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` is the
|
|
86
|
+
current train head.
|
|
85
87
|
|
|
86
88
|
```sh
|
|
87
89
|
release_candidate=4.0.0-rc.N
|
|
@@ -109,6 +111,55 @@ The first stable tag `v4.0.0` is immutable published history. Do not recreate or
|
|
|
109
111
|
git tag -a v4.0.0 -m "v4.0.0"
|
|
110
112
|
```
|
|
111
113
|
|
|
114
|
+
## Releasing 4.0.22
|
|
115
|
+
|
|
116
|
+
The `v4.0.22` tag must identify the exact current `main` commit after canonical
|
|
117
|
+
CI passes. Do not tag the release branch directly.
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
git fetch origin --tags
|
|
121
|
+
git checkout main
|
|
122
|
+
git pull --ff-only origin main
|
|
123
|
+
|
|
124
|
+
test "$(git branch --show-current)" = "main"
|
|
125
|
+
test "$(node -p "require('./package.json').version")" = "4.0.22"
|
|
126
|
+
git diff --exit-code
|
|
127
|
+
git diff --cached --exit-code
|
|
128
|
+
|
|
129
|
+
git tag -a v4.0.22 -m "v4.0.22"
|
|
130
|
+
git push origin v4.0.22
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Before tagging, confirm release-note extraction finds `## [4.0.22]`.
|
|
134
|
+
|
|
135
|
+
## Unpublished 4.0.21 tag
|
|
136
|
+
|
|
137
|
+
The immutable `v4.0.21` tag triggered workflow `33862393779`, which was
|
|
138
|
+
cancelled during native prebuilds before the npm version check or publication
|
|
139
|
+
after physical Android hardware exposed a remaining CCCD callback-order race.
|
|
140
|
+
It must not be recreated, moved, or published manually.
|
|
141
|
+
|
|
142
|
+
## Releasing 4.0.20
|
|
143
|
+
|
|
144
|
+
The `v4.0.20` tag must identify the exact current `main` commit after canonical
|
|
145
|
+
CI passes. Do not tag the release branch directly.
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
git fetch origin --tags
|
|
149
|
+
git checkout main
|
|
150
|
+
git pull --ff-only origin main
|
|
151
|
+
|
|
152
|
+
test "$(git branch --show-current)" = "main"
|
|
153
|
+
test "$(node -p "require('./package.json').version")" = "4.0.20"
|
|
154
|
+
git diff --exit-code
|
|
155
|
+
git diff --cached --exit-code
|
|
156
|
+
|
|
157
|
+
git tag -a v4.0.20 -m "v4.0.20"
|
|
158
|
+
git push origin v4.0.20
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Before tagging, confirm release-note extraction finds `## [4.0.20]`.
|
|
162
|
+
|
|
112
163
|
## Releasing 4.0.19
|
|
113
164
|
|
|
114
165
|
The `v4.0.19` tag must identify the exact current `main` commit after canonical
|
|
@@ -424,7 +475,7 @@ a green publish job and a package a consumer can actually install are not the
|
|
|
424
475
|
same claim.
|
|
425
476
|
|
|
426
477
|
```sh
|
|
427
|
-
version=4.0.
|
|
478
|
+
version=4.0.22
|
|
428
479
|
|
|
429
480
|
npm view "unified-ble-manager@$version" version
|
|
430
481
|
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.22",
|
|
10
10
|
"name": "unified-ble-manager",
|
|
11
|
-
"version": "4.0.
|
|
11
|
+
"version": "4.0.22",
|
|
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.22"
|
|
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.22",
|
|
541
541
|
"dependsOn": [
|
|
542
542
|
"pkg:npm/%40babel/runtime@7.29.7",
|
|
543
543
|
"pkg:npm/node-addon-api@8.9.0",
|
package/android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt
CHANGED
|
@@ -131,12 +131,91 @@ internal fun classifyAndroidGattOperationFailure(
|
|
|
131
131
|
): AndroidGattOperationFailure = AndroidGattOperationFailure(operation, gattStatus)
|
|
132
132
|
|
|
133
133
|
internal fun classifyAndroidNotificationRegistrationFailure(
|
|
134
|
-
operation: String
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
operation: String
|
|
135
|
+
): AndroidGattOperationFailure = AndroidGattOperationFailure(operation, null, isLinkLoss = true)
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Android may deliver an ordinary CCCD callback immediately before the
|
|
139
|
+
* authoritative status-19 connection callback for the same GATT generation.
|
|
140
|
+
* Keep only that provisional result pending long enough for lifecycle evidence
|
|
141
|
+
* already in flight to claim it. Descriptor writes and already-typed link loss
|
|
142
|
+
* are never delayed.
|
|
143
|
+
*/
|
|
144
|
+
internal fun shouldAwaitAndroidCccdDisconnectEvidence(failure: Throwable): Boolean =
|
|
145
|
+
failure is AndroidGattOperationFailure &&
|
|
146
|
+
failure.operation == "cccd-write" &&
|
|
147
|
+
!failure.isLinkLoss
|
|
148
|
+
|
|
149
|
+
private const val ANDROID_CCCD_DISCONNECT_EVIDENCE_GRACE_MS = 250L
|
|
150
|
+
|
|
151
|
+
internal class AndroidCccdTerminalArbiter<Key>(
|
|
152
|
+
private val schedule: (delayMs: Long, action: () -> Unit) -> Boolean,
|
|
153
|
+
private val onFallback: (key: Key, failure: AndroidGattOperationFailure) -> Unit
|
|
154
|
+
) {
|
|
155
|
+
private val provisionalFailures = ConcurrentHashMap<Key, AndroidGattOperationFailure>()
|
|
156
|
+
|
|
157
|
+
fun defer(key: Key, failure: AndroidGattOperationFailure) {
|
|
158
|
+
if (provisionalFailures.putIfAbsent(key, failure) != null) return
|
|
159
|
+
val fallback: () -> Unit = {
|
|
160
|
+
val retained = provisionalFailures.remove(key)
|
|
161
|
+
if (retained != null) onFallback(key, retained)
|
|
162
|
+
}
|
|
163
|
+
if (!schedule(ANDROID_CCCD_DISCONNECT_EVIDENCE_GRACE_MS, fallback)) fallback()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fun claim(key: Key): AndroidGattOperationFailure? = provisionalFailures.remove(key)
|
|
167
|
+
|
|
168
|
+
fun isPending(key: Key): Boolean = provisionalFailures.containsKey(key)
|
|
169
|
+
|
|
170
|
+
fun clear() {
|
|
171
|
+
provisionalFailures.clear()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Synchronous API rejection after local CCCD registration, before Android starts ATT work. */
|
|
176
|
+
internal class AndroidCccdSubmissionFailure(
|
|
177
|
+
val platformStatus: Int?
|
|
178
|
+
) : IllegalStateException(
|
|
179
|
+
if (platformStatus == null) "writeDescriptor failed to start"
|
|
180
|
+
else "writeDescriptor failed to start status=$platformStatus"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
/** Android rejected removal of the exact local notification registration. */
|
|
184
|
+
internal class AndroidNotificationRollbackRejected :
|
|
185
|
+
IllegalStateException("setCharacteristicNotification rollback was rejected")
|
|
186
|
+
|
|
187
|
+
internal fun androidGattTerminalResult(
|
|
188
|
+
result: Result<Unit>,
|
|
189
|
+
rollbackFailure: OwnedRadioTeardownFailure?
|
|
190
|
+
): Result<Unit> {
|
|
191
|
+
val primaryFailure = result.exceptionOrNull()
|
|
192
|
+
val registrationRollbackRejected =
|
|
193
|
+
rollbackFailure?.throwable is AndroidNotificationRollbackRejected
|
|
194
|
+
if (registrationRollbackRejected && primaryFailure is AndroidCccdSubmissionFailure) {
|
|
195
|
+
return Result.failure(classifyAndroidNotificationRegistrationFailure("cccd-write"))
|
|
196
|
+
}
|
|
197
|
+
if (
|
|
198
|
+
registrationRollbackRejected &&
|
|
199
|
+
primaryFailure is AndroidGattOperationFailure &&
|
|
200
|
+
primaryFailure.operation == "cccd-write" &&
|
|
201
|
+
!primaryFailure.isLinkLoss
|
|
202
|
+
) {
|
|
203
|
+
return Result.failure(
|
|
204
|
+
AndroidGattOperationFailure(
|
|
205
|
+
primaryFailure.operation,
|
|
206
|
+
primaryFailure.gattStatus,
|
|
207
|
+
isLinkLoss = true
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
if (rollbackFailure == null || result.isFailure) return result
|
|
212
|
+
return Result.failure(
|
|
213
|
+
IllegalStateException(
|
|
214
|
+
"CCCD operation failed; CCCD rollback failed: " +
|
|
215
|
+
(rollbackFailure.throwable.message ?: "unknown error"),
|
|
216
|
+
rollbackFailure.throwable
|
|
217
|
+
)
|
|
218
|
+
)
|
|
140
219
|
}
|
|
141
220
|
|
|
142
221
|
/** Runtime adapter state derived from Android hardware and granted permissions. */
|
|
@@ -318,6 +397,13 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
318
397
|
private val exactWritePending = ConcurrentHashMap<BluetoothGattCharacteristic, ExactBytePending>()
|
|
319
398
|
private val exactWriteValues = ConcurrentHashMap<BluetoothGattCharacteristic, ByteArray>()
|
|
320
399
|
private val exactCccdPending = ConcurrentHashMap<BluetoothGattDescriptor, ExactUnitPending>()
|
|
400
|
+
private val exactCccdTerminalArbiter = AndroidCccdTerminalArbiter<BluetoothGattDescriptor>(
|
|
401
|
+
schedule = { delayMs, action -> mainHandler.postDelayed(action, delayMs) },
|
|
402
|
+
onFallback = fallback@ { descriptor, failure ->
|
|
403
|
+
val pending = exactCccdPending.remove(descriptor) ?: return@fallback
|
|
404
|
+
completeExactUnit(pending, Result.failure(failure))
|
|
405
|
+
}
|
|
406
|
+
)
|
|
321
407
|
private val exactDescriptorReadPending = ConcurrentHashMap<BluetoothGattDescriptor, ExactBytePending>()
|
|
322
408
|
private val exactDescriptorWritePending = ConcurrentHashMap<BluetoothGattDescriptor, ExactUnitPending>()
|
|
323
409
|
private val activeNativeSubscriptionOwnership =
|
|
@@ -1047,11 +1133,6 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
1047
1133
|
return failures
|
|
1048
1134
|
}
|
|
1049
1135
|
|
|
1050
|
-
fun isConnected(deviceId: String): Boolean {
|
|
1051
|
-
val device = adapter?.getRemoteDevice(deviceId) ?: return false
|
|
1052
|
-
return bluetoothManager.getConnectionState(device, BluetoothProfile.GATT) == BluetoothProfile.STATE_CONNECTED
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
1136
|
fun discover(deviceId: String, onDone: (Boolean) -> Unit): Long {
|
|
1056
1137
|
return enqueue(
|
|
1057
1138
|
deviceId,
|
|
@@ -1565,12 +1646,10 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
1565
1646
|
return@enqueue
|
|
1566
1647
|
}
|
|
1567
1648
|
if (!gatt.setCharacteristicNotification(ch, enable)) {
|
|
1568
|
-
val connected = runCatching { isConnected(deviceId) }.getOrDefault(true)
|
|
1569
1649
|
onResult(
|
|
1570
1650
|
Result.failure(
|
|
1571
1651
|
classifyAndroidNotificationRegistrationFailure(
|
|
1572
|
-
"setCharacteristicNotification"
|
|
1573
|
-
connected
|
|
1652
|
+
"setCharacteristicNotification"
|
|
1574
1653
|
)
|
|
1575
1654
|
)
|
|
1576
1655
|
)
|
|
@@ -1683,14 +1762,12 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
1683
1762
|
return@enqueue
|
|
1684
1763
|
}
|
|
1685
1764
|
if (!gatt.setCharacteristicNotification(characteristic, enable)) {
|
|
1686
|
-
val connected = runCatching { isConnected(deviceId) }.getOrDefault(true)
|
|
1687
1765
|
completeExactUnitDirect(
|
|
1688
1766
|
onResult,
|
|
1689
1767
|
done,
|
|
1690
1768
|
Result.failure(
|
|
1691
1769
|
classifyAndroidNotificationRegistrationFailure(
|
|
1692
|
-
"setCharacteristicNotification"
|
|
1693
|
-
connected
|
|
1770
|
+
"setCharacteristicNotification"
|
|
1694
1771
|
)
|
|
1695
1772
|
),
|
|
1696
1773
|
token
|
|
@@ -1768,7 +1845,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
1768
1845
|
if (exactCccdPending.remove(cccd, pending)) {
|
|
1769
1846
|
completeExactUnit(
|
|
1770
1847
|
pending,
|
|
1771
|
-
Result.failure(
|
|
1848
|
+
Result.failure(AndroidCccdSubmissionFailure(status))
|
|
1772
1849
|
)
|
|
1773
1850
|
}
|
|
1774
1851
|
}
|
|
@@ -1781,7 +1858,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
1781
1858
|
if (exactCccdPending.remove(cccd, pending)) {
|
|
1782
1859
|
completeExactUnit(
|
|
1783
1860
|
pending,
|
|
1784
|
-
Result.failure(
|
|
1861
|
+
Result.failure(AndroidCccdSubmissionFailure(null))
|
|
1785
1862
|
)
|
|
1786
1863
|
}
|
|
1787
1864
|
}
|
|
@@ -2094,6 +2171,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2094
2171
|
exactWritePending.clear()
|
|
2095
2172
|
exactWriteValues.clear()
|
|
2096
2173
|
exactCccdPending.clear()
|
|
2174
|
+
exactCccdTerminalArbiter.clear()
|
|
2097
2175
|
exactDescriptorReadPending.clear()
|
|
2098
2176
|
exactDescriptorWritePending.clear()
|
|
2099
2177
|
activeNativeSubscriptionOwnership.clear()
|
|
@@ -2152,12 +2230,24 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2152
2230
|
}
|
|
2153
2231
|
}
|
|
2154
2232
|
|
|
2155
|
-
private fun failPendingForDevice(
|
|
2233
|
+
private fun failPendingForDevice(
|
|
2234
|
+
deviceKeyUpper: String,
|
|
2235
|
+
reason: String,
|
|
2236
|
+
gattStatus: Int? = null
|
|
2237
|
+
) {
|
|
2156
2238
|
effectiveMtuByDevice.remove(deviceKeyUpper)
|
|
2157
2239
|
deviceQueues.remove(deviceKeyUpper)?.clear(IllegalStateException(reason))
|
|
2158
2240
|
val failBytes = Result.failure<ByteArray?>(IllegalStateException(reason))
|
|
2159
2241
|
val failInt = Result.failure<Int>(IllegalStateException(reason))
|
|
2160
2242
|
val failUnit = Result.failure<Unit>(IllegalStateException(reason))
|
|
2243
|
+
// A peer link-loss callback can win the race with onDescriptorWrite. Keep
|
|
2244
|
+
// that exact Android status on pending CCCD work so the protocol boundary
|
|
2245
|
+
// reports connectionLost instead of flattening the race to platformFailure.
|
|
2246
|
+
val failCccd: Result<Unit> = if (gattStatus == ANDROID_GATT_LINK_LOSS_STATUS) {
|
|
2247
|
+
Result.failure(classifyAndroidGattOperationFailure("cccd-write", gattStatus))
|
|
2248
|
+
} else {
|
|
2249
|
+
failUnit
|
|
2250
|
+
}
|
|
2161
2251
|
pending.keys
|
|
2162
2252
|
.filter { key ->
|
|
2163
2253
|
keyBelongsToDevice(key, "discover", deviceKeyUpper) ||
|
|
@@ -2180,7 +2270,9 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2180
2270
|
keyBelongsToDevice(key, "descWrite", deviceKeyUpper)
|
|
2181
2271
|
}
|
|
2182
2272
|
.toList()
|
|
2183
|
-
.forEach { key ->
|
|
2273
|
+
.forEach { key ->
|
|
2274
|
+
pendingDesc.remove(key)?.invoke(if (key.startsWith("cccd:")) failCccd else failUnit)
|
|
2275
|
+
}
|
|
2184
2276
|
pendingDescRead.keys
|
|
2185
2277
|
.filter { key -> keyBelongsToDevice(key, "descRead", deviceKeyUpper) }
|
|
2186
2278
|
.toList()
|
|
@@ -2208,7 +2300,11 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2208
2300
|
.filter { it.value.deviceKeyUpper == deviceKeyUpper }
|
|
2209
2301
|
.forEach { entry ->
|
|
2210
2302
|
if (exactCccdPending.remove(entry.key, entry.value)) {
|
|
2211
|
-
|
|
2303
|
+
exactCccdTerminalArbiter.claim(entry.key)
|
|
2304
|
+
completeExactUnit(
|
|
2305
|
+
entry.value,
|
|
2306
|
+
if (entry.value.subscriptionEnabled != null) failCccd else failUnit
|
|
2307
|
+
)
|
|
2212
2308
|
}
|
|
2213
2309
|
}
|
|
2214
2310
|
exactDescriptorReadPending.entries
|
|
@@ -2360,17 +2456,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2360
2456
|
pending.done()
|
|
2361
2457
|
return
|
|
2362
2458
|
}
|
|
2363
|
-
val terminalResult =
|
|
2364
|
-
result
|
|
2365
|
-
} else {
|
|
2366
|
-
Result.failure(
|
|
2367
|
-
IllegalStateException(
|
|
2368
|
-
"${result.exceptionOrNull()?.message ?: "CCCD operation failed"}; " +
|
|
2369
|
-
"CCCD rollback failed: ${rollbackFailure.throwable.message ?: "unknown error"}",
|
|
2370
|
-
result.exceptionOrNull() ?: rollbackFailure.throwable
|
|
2371
|
-
)
|
|
2372
|
-
)
|
|
2373
|
-
}
|
|
2459
|
+
val terminalResult = androidGattTerminalResult(result, rollbackFailure)
|
|
2374
2460
|
if (terminalResult.isSuccess && !pending.token.isPubliclySettled()) {
|
|
2375
2461
|
pending.subscriptionEnabled?.let { enabled ->
|
|
2376
2462
|
pending.subscriptionCharacteristic?.let { characteristic ->
|
|
@@ -2417,7 +2503,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2417
2503
|
} else {
|
|
2418
2504
|
OwnedRadioTeardownFailure(
|
|
2419
2505
|
"cccdRollback:$deviceKeyUpper:generation=$generation",
|
|
2420
|
-
|
|
2506
|
+
AndroidNotificationRollbackRejected()
|
|
2421
2507
|
)
|
|
2422
2508
|
}
|
|
2423
2509
|
} catch (throwable: Throwable) {
|
|
@@ -2425,6 +2511,16 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2425
2511
|
}
|
|
2426
2512
|
}
|
|
2427
2513
|
|
|
2514
|
+
private fun deferExactCccdFailure(
|
|
2515
|
+
descriptor: BluetoothGattDescriptor,
|
|
2516
|
+
pending: ExactUnitPending,
|
|
2517
|
+
failure: AndroidGattOperationFailure
|
|
2518
|
+
) {
|
|
2519
|
+
if (exactCccdPending[descriptor] === pending) {
|
|
2520
|
+
exactCccdTerminalArbiter.defer(descriptor, failure)
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2428
2524
|
private fun completeExactUnitDirect(
|
|
2429
2525
|
callback: (Result<Unit>) -> Unit,
|
|
2430
2526
|
done: () -> Unit,
|
|
@@ -2491,7 +2587,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2491
2587
|
// Always pass gatt status: status 133 etc. means failed connect, not clean disconnect.
|
|
2492
2588
|
// R3-F003: close() only after STATE_DISCONNECTED (not from disconnect()/connect prior).
|
|
2493
2589
|
dispatchConnectionState(id, false, status)
|
|
2494
|
-
failPendingForDevice(key, "disconnected status=$status")
|
|
2590
|
+
failPendingForDevice(key, "disconnected status=$status", status)
|
|
2495
2591
|
val teardownFailure = completeGattTeardown(key, gatt, generation)
|
|
2496
2592
|
pendingDisconnectCallbacks.remove(key)?.invoke(teardownFailure)
|
|
2497
2593
|
if (teardownFailure != null) return
|
|
@@ -2528,18 +2624,29 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2528
2624
|
}
|
|
2529
2625
|
return
|
|
2530
2626
|
}
|
|
2531
|
-
exactCccdPending
|
|
2627
|
+
exactCccdPending[descriptor]?.let { pending ->
|
|
2628
|
+
// The first callback owns the terminal. A contradictory duplicate
|
|
2629
|
+
// callback cannot replace a provisional failure during arbitration.
|
|
2630
|
+
if (exactCccdTerminalArbiter.isPending(descriptor)) return
|
|
2532
2631
|
if (!isCurrentGatt(pending.deviceKeyUpper, gatt, pending.gattGeneration)) {
|
|
2533
|
-
|
|
2632
|
+
if (exactCccdPending.remove(descriptor, pending)) {
|
|
2633
|
+
exactCccdTerminalArbiter.claim(descriptor)
|
|
2634
|
+
pending.done()
|
|
2635
|
+
}
|
|
2534
2636
|
return@let
|
|
2535
2637
|
}
|
|
2536
2638
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2537
|
-
|
|
2639
|
+
if (exactCccdPending.remove(descriptor, pending)) {
|
|
2640
|
+
completeExactUnit(pending, Result.success(Unit))
|
|
2641
|
+
}
|
|
2538
2642
|
} else {
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
)
|
|
2643
|
+
val failure = classifyAndroidGattOperationFailure("cccd-write", status)
|
|
2644
|
+
if (shouldAwaitAndroidCccdDisconnectEvidence(failure)) {
|
|
2645
|
+
deferExactCccdFailure(descriptor, pending, failure)
|
|
2646
|
+
} else if (exactCccdPending.remove(descriptor, pending)) {
|
|
2647
|
+
exactCccdTerminalArbiter.claim(descriptor)
|
|
2648
|
+
completeExactUnit(pending, Result.failure(failure))
|
|
2649
|
+
}
|
|
2543
2650
|
}
|
|
2544
2651
|
return
|
|
2545
2652
|
}
|
|
@@ -14,6 +14,13 @@ import com.sfourdrinier.unifiedblemanager.radio.bondedPeerAdapterReadiness
|
|
|
14
14
|
import com.sfourdrinier.unifiedblemanager.radio.requiresImmediateGattTeardownOnAdapterState
|
|
15
15
|
import com.sfourdrinier.unifiedblemanager.radio.classifyAndroidGattOperationFailure
|
|
16
16
|
import com.sfourdrinier.unifiedblemanager.radio.classifyAndroidNotificationRegistrationFailure
|
|
17
|
+
import com.sfourdrinier.unifiedblemanager.radio.AndroidGattOperationFailure
|
|
18
|
+
import com.sfourdrinier.unifiedblemanager.radio.AndroidCccdSubmissionFailure
|
|
19
|
+
import com.sfourdrinier.unifiedblemanager.radio.AndroidNotificationRollbackRejected
|
|
20
|
+
import com.sfourdrinier.unifiedblemanager.radio.AndroidCccdTerminalArbiter
|
|
21
|
+
import com.sfourdrinier.unifiedblemanager.radio.OwnedRadioTeardownFailure
|
|
22
|
+
import com.sfourdrinier.unifiedblemanager.radio.androidGattTerminalResult
|
|
23
|
+
import com.sfourdrinier.unifiedblemanager.radio.shouldAwaitAndroidCccdDisconnectEvidence
|
|
17
24
|
import org.junit.Assert.assertEquals
|
|
18
25
|
import org.junit.Assert.assertArrayEquals
|
|
19
26
|
import org.junit.Assert.assertFalse
|
|
@@ -44,6 +51,87 @@ class UnifiedBleProtocolAndroidDispatcherLifecycleTest {
|
|
|
44
51
|
assertEquals(133, ordinaryFailure.gattStatus)
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
@Test
|
|
55
|
+
fun onlyProvisionalAsyncCccdFailuresAwaitAuthoritativeDisconnectEvidence() {
|
|
56
|
+
assertTrue(
|
|
57
|
+
shouldAwaitAndroidCccdDisconnectEvidence(
|
|
58
|
+
classifyAndroidGattOperationFailure("cccd-write", 133)
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
assertFalse(
|
|
62
|
+
shouldAwaitAndroidCccdDisconnectEvidence(
|
|
63
|
+
classifyAndroidGattOperationFailure("cccd-write", 19)
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
assertFalse(
|
|
67
|
+
shouldAwaitAndroidCccdDisconnectEvidence(
|
|
68
|
+
classifyAndroidGattOperationFailure("descriptor-write", 133)
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
assertFalse(
|
|
72
|
+
shouldAwaitAndroidCccdDisconnectEvidence(
|
|
73
|
+
IllegalStateException("ordinary failure")
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
val radio = readAndroidSource(
|
|
78
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
|
|
79
|
+
)
|
|
80
|
+
val callback = radio.substring(
|
|
81
|
+
radio.indexOf("override fun onDescriptorWrite"),
|
|
82
|
+
radio.indexOf("@Deprecated(\"Deprecated in Java\")")
|
|
83
|
+
)
|
|
84
|
+
assertTrue(callback.contains("deferExactCccdFailure(descriptor, pending, failure)"))
|
|
85
|
+
|
|
86
|
+
val disconnectCleanup = radio.substring(
|
|
87
|
+
radio.indexOf("exactCccdPending.entries"),
|
|
88
|
+
radio.indexOf("exactDescriptorReadPending.entries")
|
|
89
|
+
)
|
|
90
|
+
assertTrue(
|
|
91
|
+
disconnectCleanup.indexOf("exactCccdPending.remove(entry.key, entry.value)") <
|
|
92
|
+
disconnectCleanup.indexOf("exactCccdTerminalArbiter.claim(entry.key)")
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
fun cccdTerminalArbiterLetsDisconnectWinAndPreservesOrdinaryFallbackExactlyOnce() {
|
|
98
|
+
val scheduled = mutableListOf<() -> Unit>()
|
|
99
|
+
val fallbacks = mutableListOf<Pair<String, AndroidGattOperationFailure>>()
|
|
100
|
+
val arbiter = AndroidCccdTerminalArbiter<String>(
|
|
101
|
+
schedule = { _, action -> scheduled.add(action) },
|
|
102
|
+
onFallback = { key, failure -> fallbacks.add(key to failure) }
|
|
103
|
+
)
|
|
104
|
+
val first = classifyAndroidGattOperationFailure("cccd-write", 133)
|
|
105
|
+
|
|
106
|
+
arbiter.defer("disconnect-wins", first)
|
|
107
|
+
arbiter.defer("disconnect-wins", classifyAndroidGattOperationFailure("cccd-write", 257))
|
|
108
|
+
assertTrue(arbiter.isPending("disconnect-wins"))
|
|
109
|
+
assertEquals(1, scheduled.size)
|
|
110
|
+
assertTrue(arbiter.claim("disconnect-wins") === first)
|
|
111
|
+
scheduled.removeAt(0).invoke()
|
|
112
|
+
assertTrue(fallbacks.isEmpty())
|
|
113
|
+
|
|
114
|
+
arbiter.defer("ordinary-fallback", first)
|
|
115
|
+
scheduled.removeAt(0).invoke()
|
|
116
|
+
assertEquals(listOf("ordinary-fallback" to first), fallbacks)
|
|
117
|
+
assertFalse(arbiter.isPending("ordinary-fallback"))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Test
|
|
121
|
+
fun cccdTerminalArbiterCannotStrandFailureWhenSchedulingIsRejected() {
|
|
122
|
+
val fallbacks = mutableListOf<AndroidGattOperationFailure>()
|
|
123
|
+
val arbiter = AndroidCccdTerminalArbiter<String>(
|
|
124
|
+
schedule = { _, _ -> false },
|
|
125
|
+
onFallback = { _, failure -> fallbacks.add(failure) }
|
|
126
|
+
)
|
|
127
|
+
val failure = classifyAndroidGattOperationFailure("cccd-write", 133)
|
|
128
|
+
|
|
129
|
+
arbiter.defer("cccd", failure)
|
|
130
|
+
|
|
131
|
+
assertEquals(listOf(failure), fallbacks)
|
|
132
|
+
assertFalse(arbiter.isPending("cccd"))
|
|
133
|
+
}
|
|
134
|
+
|
|
47
135
|
@Test
|
|
48
136
|
fun dispatcherOnlyMapsTypedLinkLossToConnectionLost() {
|
|
49
137
|
val linkLoss = classifyAndroidGattOperationFailure("cccd-write", 19)
|
|
@@ -64,20 +152,212 @@ class UnifiedBleProtocolAndroidDispatcherLifecycleTest {
|
|
|
64
152
|
}
|
|
65
153
|
|
|
66
154
|
@Test
|
|
67
|
-
fun
|
|
68
|
-
val
|
|
69
|
-
operation = "notification-registration"
|
|
70
|
-
|
|
155
|
+
fun synchronousNotificationRegistrationFailureIsTypedAsLinkLossDespiteStaleManagerState() {
|
|
156
|
+
val failure = classifyAndroidNotificationRegistrationFailure(
|
|
157
|
+
operation = "notification-registration"
|
|
158
|
+
)
|
|
159
|
+
assertEquals("connectionLost", androidGattOperationFailureCode(failure, "subscriptionFailed"))
|
|
160
|
+
assertNull(androidGattOperationFailureStatus(failure))
|
|
161
|
+
|
|
162
|
+
val ordinaryCccdFailure = classifyAndroidGattOperationFailure("cccd-write", 133)
|
|
163
|
+
assertEquals(
|
|
164
|
+
"subscriptionFailed",
|
|
165
|
+
androidGattOperationFailureCode(ordinaryCccdFailure, "subscriptionFailed")
|
|
166
|
+
)
|
|
167
|
+
assertEquals(133, androidGattOperationFailureStatus(ordinaryCccdFailure))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@Test
|
|
171
|
+
fun exactAndNonExactNotificationRegistrationFailuresShareTheTypedLinkLossClassifier() {
|
|
172
|
+
val radio = readAndroidSource(
|
|
173
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
|
|
174
|
+
)
|
|
175
|
+
val nonExact = radio.substring(
|
|
176
|
+
radio.indexOf("private fun setNotify("),
|
|
177
|
+
radio.indexOf("/** Enables or disables an exact duplicate-safe")
|
|
178
|
+
)
|
|
179
|
+
val exact = radio.substring(
|
|
180
|
+
radio.indexOf("private fun setNotifyTarget("),
|
|
181
|
+
radio.indexOf("private fun rollbackNotifyRegistration")
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
assertTrue(nonExact.contains("classifyAndroidNotificationRegistrationFailure("))
|
|
185
|
+
assertTrue(exact.contains("classifyAndroidNotificationRegistrationFailure("))
|
|
186
|
+
assertFalse(nonExact.contains("isConnected("))
|
|
187
|
+
assertFalse(exact.contains("isConnected("))
|
|
188
|
+
assertFalse(radio.contains("fun isConnected("))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@Test
|
|
192
|
+
fun disconnectStatus19PreservesTypedCccdFailureWhileDrainingBothSubscribePaths() {
|
|
193
|
+
val radio = readAndroidSource(
|
|
194
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
|
|
195
|
+
)
|
|
196
|
+
val pendingDrain = radio.substring(
|
|
197
|
+
radio.indexOf("private fun failPendingForDevice("),
|
|
198
|
+
radio.indexOf("private fun findChar(")
|
|
199
|
+
)
|
|
200
|
+
val disconnectCallback = radio.substring(
|
|
201
|
+
radio.indexOf("override fun onConnectionStateChange"),
|
|
202
|
+
radio.indexOf("override fun onDescriptorWrite")
|
|
203
|
+
)
|
|
204
|
+
val nonExactSubscribe = radio.substring(
|
|
205
|
+
radio.indexOf("private fun setNotify("),
|
|
206
|
+
radio.indexOf("/** Enables or disables an exact duplicate-safe")
|
|
207
|
+
)
|
|
208
|
+
val exactSubscribe = radio.substring(
|
|
209
|
+
radio.indexOf("private fun setNotifyTarget("),
|
|
210
|
+
radio.indexOf("private fun rollbackNotifyRegistration")
|
|
211
|
+
)
|
|
212
|
+
val descriptorCallback = radio.substring(
|
|
213
|
+
radio.indexOf("override fun onDescriptorWrite"),
|
|
214
|
+
radio.indexOf("@Deprecated(\"Deprecated in Java\")")
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
assertTrue(pendingDrain.contains("gattStatus: Int?"))
|
|
218
|
+
assertTrue(pendingDrain.contains("classifyAndroidGattOperationFailure(\"cccd-write\", gattStatus)"))
|
|
219
|
+
assertTrue(pendingDrain.contains("if (key.startsWith(\"cccd:\")) failCccd else failUnit"))
|
|
220
|
+
assertTrue(pendingDrain.contains("if (entry.value.subscriptionEnabled != null) failCccd else failUnit"))
|
|
221
|
+
assertTrue(nonExactSubscribe.contains("pendingDesc[key]"))
|
|
222
|
+
assertTrue(exactSubscribe.contains("exactCccdPending.putIfAbsent(cccd, pending)"))
|
|
223
|
+
assertTrue(descriptorCallback.contains("classifyAndroidGattOperationFailure"))
|
|
224
|
+
assertTrue(
|
|
225
|
+
disconnectCallback.contains(
|
|
226
|
+
"failPendingForDevice(key, \"disconnected status=\$status\", status)"
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@Test
|
|
232
|
+
fun cccdRollbackFailureDoesNotReplaceThePrimaryTypedGattFailure() {
|
|
233
|
+
val radio = readAndroidSource(
|
|
234
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
|
|
235
|
+
)
|
|
236
|
+
val completion = radio.substring(
|
|
237
|
+
radio.indexOf("private fun completeExactUnit("),
|
|
238
|
+
radio.indexOf("private fun rollbackNotifyRegistration")
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
assertTrue(completion.contains("androidGattTerminalResult(result, rollbackFailure)"))
|
|
242
|
+
assertTrue(completion.contains("registerRetryableCleanup(rollbackFailure.operation)"))
|
|
243
|
+
assertTrue(completion.contains("reportCleanupFailure(rollbackFailure)"))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@Test
|
|
247
|
+
fun cccdRollbackFailurePreservesTypedPrimaryAndKeepsSuccessfulPrimaryCleanupFailureBehavior() {
|
|
248
|
+
val primary = classifyAndroidGattOperationFailure("cccd-write", 19)
|
|
249
|
+
val rollbackFailure = OwnedRadioTeardownFailure(
|
|
250
|
+
"cccdRollback:AA:BB",
|
|
251
|
+
IllegalStateException("link is gone")
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
val typedFailure = androidGattTerminalResult(Result.failure<Unit>(primary), rollbackFailure)
|
|
255
|
+
val typedError = typedFailure.exceptionOrNull()
|
|
256
|
+
assertTrue(typedError === primary)
|
|
257
|
+
assertTrue(typedError is AndroidGattOperationFailure)
|
|
258
|
+
if (typedError !is AndroidGattOperationFailure) throw AssertionError("typed primary was flattened")
|
|
259
|
+
assertTrue(typedError.isLinkLoss)
|
|
260
|
+
|
|
261
|
+
val callbackFailure = classifyAndroidGattOperationFailure("cccd-write", 133)
|
|
262
|
+
val callbackFailureWithGenericCleanupError = androidGattTerminalResult(
|
|
263
|
+
Result.failure<Unit>(callbackFailure),
|
|
264
|
+
rollbackFailure
|
|
265
|
+
)
|
|
266
|
+
assertTrue(callbackFailureWithGenericCleanupError.exceptionOrNull() === callbackFailure)
|
|
267
|
+
|
|
268
|
+
val rejectedRegistrationRollback = OwnedRadioTeardownFailure(
|
|
269
|
+
"cccdRollback:AA:BB",
|
|
270
|
+
AndroidNotificationRollbackRejected()
|
|
271
|
+
)
|
|
272
|
+
val linkLossAfterCallbackFailure = androidGattTerminalResult(
|
|
273
|
+
Result.failure<Unit>(callbackFailure),
|
|
274
|
+
rejectedRegistrationRollback
|
|
275
|
+
)
|
|
276
|
+
val callbackError = linkLossAfterCallbackFailure.exceptionOrNull()
|
|
277
|
+
assertTrue(callbackError is AndroidGattOperationFailure)
|
|
278
|
+
if (callbackError !is AndroidGattOperationFailure) {
|
|
279
|
+
throw AssertionError("CCCD callback failure plus rejected rollback was not classified")
|
|
280
|
+
}
|
|
281
|
+
assertTrue(callbackError.isLinkLoss)
|
|
282
|
+
assertEquals(133, callbackError.gattStatus)
|
|
283
|
+
|
|
284
|
+
val descriptorFailure = classifyAndroidGattOperationFailure("descriptor-write", 133)
|
|
285
|
+
assertTrue(
|
|
286
|
+
androidGattTerminalResult(
|
|
287
|
+
Result.failure<Unit>(descriptorFailure),
|
|
288
|
+
rejectedRegistrationRollback
|
|
289
|
+
).exceptionOrNull() === descriptorFailure
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
val successfulPrimary = androidGattTerminalResult(Result.success(Unit), rollbackFailure)
|
|
293
|
+
assertTrue(successfulPrimary.isFailure)
|
|
294
|
+
assertTrue(successfulPrimary.exceptionOrNull() is IllegalStateException)
|
|
295
|
+
assertTrue(successfulPrimary.exceptionOrNull()?.message?.contains("CCCD rollback failed") == true)
|
|
296
|
+
|
|
297
|
+
val ordinaryPrimary = IllegalStateException("ordinary CCCD failure")
|
|
298
|
+
val ordinaryFailure = androidGattTerminalResult(Result.failure<Unit>(ordinaryPrimary), rollbackFailure)
|
|
299
|
+
assertTrue(ordinaryFailure.exceptionOrNull() === ordinaryPrimary)
|
|
300
|
+
|
|
301
|
+
val synchronousSubmission = AndroidCccdSubmissionFailure(133)
|
|
302
|
+
assertEquals(133, synchronousSubmission.platformStatus)
|
|
303
|
+
val linkLossAfterSubmission = androidGattTerminalResult(
|
|
304
|
+
Result.failure<Unit>(synchronousSubmission),
|
|
305
|
+
rejectedRegistrationRollback
|
|
306
|
+
)
|
|
307
|
+
val linkLossError = linkLossAfterSubmission.exceptionOrNull()
|
|
308
|
+
assertTrue(linkLossError is AndroidGattOperationFailure)
|
|
309
|
+
if (linkLossError !is AndroidGattOperationFailure) throw AssertionError("submission failure was not classified")
|
|
310
|
+
assertTrue(linkLossError.isLinkLoss)
|
|
311
|
+
assertTrue(
|
|
312
|
+
androidGattTerminalResult(Result.failure(synchronousSubmission), null).exceptionOrNull() ===
|
|
313
|
+
synchronousSubmission
|
|
314
|
+
)
|
|
315
|
+
val legacySubmission = AndroidCccdSubmissionFailure(null)
|
|
316
|
+
assertNull(legacySubmission.platformStatus)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@Test
|
|
320
|
+
fun exactCccdSubmissionRejectionIsMarkedBeforeRollbackClassification() {
|
|
321
|
+
val radio = readAndroidSource(
|
|
322
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
|
|
323
|
+
)
|
|
324
|
+
val exactSubscribe = radio.substring(
|
|
325
|
+
radio.indexOf("private fun setNotifyTarget("),
|
|
326
|
+
radio.indexOf("private fun rollbackNotifyRegistration")
|
|
327
|
+
)
|
|
328
|
+
val completion = radio.substring(
|
|
329
|
+
radio.indexOf("private fun completeExactUnit("),
|
|
330
|
+
radio.indexOf("private fun rollbackNotifyRegistration")
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
assertTrue(exactSubscribe.contains("AndroidCccdSubmissionFailure"))
|
|
334
|
+
assertTrue(exactSubscribe.contains("AndroidCccdSubmissionFailure(status)"))
|
|
335
|
+
assertTrue(exactSubscribe.contains("AndroidCccdSubmissionFailure(null)"))
|
|
336
|
+
assertTrue(completion.contains("androidGattTerminalResult(result, rollbackFailure)"))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@Test
|
|
340
|
+
fun publicSubscribeUsesExactNotificationPathWhileLegacyPrivatePathStaysOrdinary() {
|
|
341
|
+
val dispatcher = readAndroidSource(
|
|
342
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt"
|
|
343
|
+
)
|
|
344
|
+
val subscribe = dispatcher.substring(
|
|
345
|
+
dispatcher.indexOf("private fun subscribe(command: ProtocolWireRecord, enable: Boolean)"),
|
|
346
|
+
dispatcher.indexOf("private fun destroy(command: ProtocolWireRecord)")
|
|
71
347
|
)
|
|
72
|
-
|
|
73
|
-
|
|
348
|
+
assertTrue(subscribe.contains("radio.setNotifyExact("))
|
|
349
|
+
assertFalse(subscribe.contains("radio.setNotify("))
|
|
74
350
|
|
|
75
|
-
val
|
|
76
|
-
|
|
77
|
-
|
|
351
|
+
val radio = readAndroidSource(
|
|
352
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
|
|
353
|
+
)
|
|
354
|
+
val legacy = radio.substring(
|
|
355
|
+
radio.indexOf("private fun setNotify("),
|
|
356
|
+
radio.indexOf("/** Enables or disables an exact duplicate-safe")
|
|
78
357
|
)
|
|
79
|
-
|
|
80
|
-
|
|
358
|
+
assertTrue(legacy.contains("IllegalStateException(\"writeDescriptor failed to start status=\$status\")"))
|
|
359
|
+
assertTrue(legacy.contains("IllegalStateException(\"writeDescriptor failed to start\")"))
|
|
360
|
+
assertFalse(legacy.contains("AndroidCccdSubmissionFailure"))
|
|
81
361
|
}
|
|
82
362
|
|
|
83
363
|
@Test
|
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.22`. 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.22` 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.22`; 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.22`; 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.22`, 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.22
|
|
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.22`. 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.22`. 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.22`
|
|
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.22';
|
|
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.22';
|
|
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.22";
|
|
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.22";
|
|
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.22";
|
|
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.22";
|
|
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.22";
|
|
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.22";
|
|
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.22";
|
|
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.22";
|
|
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.22";
|
|
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.22";
|
|
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.22";
|
|
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.22";
|
|
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.22",
|
|
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": "32005cc887bee75665ad0dd1ebf8ea39affaab593d41d53b65c7e05810185abd"
|
|
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": "b57f1f75ea39424b7525560408f84f0fac67467073fd05e0606c44383a94bcf5"
|
|
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": "3dbd53397b3386dff78d77942ba0f83329c4126cec1fa4d8ff6c910daddef9ce"
|
|
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": "0b37b8bdbd55ecce5edbd7b30615181aa8891951799a86c33557a1f7011563cb"
|
|
41
41
|
}
|
|
42
42
|
]
|
|
43
43
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED