unified-ble-manager 4.0.13 → 4.0.15
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 +29 -0
- package/MIGRATION_4.0.md +1 -1
- package/README.md +1 -1
- package/RELEASE.md +21 -8
- package/SBOM.cdx.json +5 -5
- package/THIRD_PARTY_LICENSES.json +2 -2
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt +40 -2
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt +36 -0
- package/android/src/test/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcherLifecycleTest.kt +139 -1
- 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/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.js +3 -0
- package/lib/commonjs/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.js.map +1 -1
- package/lib/commonjs/backends/corebluetooth/corebluetooth-backend.js +27 -2
- package/lib/commonjs/backends/corebluetooth/corebluetooth-backend.js.map +1 -1
- package/lib/commonjs/backends/winrt/winrt-backend.js +52 -10
- package/lib/commonjs/backends/winrt/winrt-backend.js.map +1 -1
- package/lib/commonjs/backends/winrt/winrt-handles.js +1 -1
- package/lib/commonjs/backends/winrt/winrt-handles.js.map +1 -1
- package/lib/commonjs/implementation-version.js +1 -1
- package/lib/commonjs/native-protocol/rn-android-boundary.js +44 -0
- package/lib/commonjs/native-protocol/rn-android-boundary.js.map +1 -1
- package/lib/module/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.js +3 -0
- package/lib/module/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.js.map +1 -1
- package/lib/module/backends/corebluetooth/corebluetooth-backend.js +27 -2
- package/lib/module/backends/corebluetooth/corebluetooth-backend.js.map +1 -1
- package/lib/module/backends/winrt/winrt-backend.js +52 -10
- package/lib/module/backends/winrt/winrt-backend.js.map +1 -1
- package/lib/module/backends/winrt/winrt-handles.js +1 -1
- package/lib/module/backends/winrt/winrt-handles.js.map +1 -1
- package/lib/module/implementation-version.js +1 -1
- package/lib/module/native-protocol/rn-android-boundary.js +44 -0
- package/lib/module/native-protocol/rn-android-boundary.js.map +1 -1
- package/lib/typescript/commonjs/src/backends/bluez/bluez-backend-provider.d.ts +1 -1
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.d.ts +1 -0
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-backend.d.ts +2 -0
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-backend.d.ts.map +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-backend.d.ts +3 -0
- package/lib/typescript/commonjs/src/backends/winrt/winrt-backend.d.ts.map +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/commonjs/src/native-protocol/rn-android-boundary.d.ts +8 -0
- package/lib/typescript/commonjs/src/native-protocol/rn-android-boundary.d.ts.map +1 -1
- package/lib/typescript/module/src/backends/bluez/bluez-backend-provider.d.ts +1 -1
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.d.ts +1 -0
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.d.ts.map +1 -1
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-backend.d.ts +2 -0
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-backend.d.ts.map +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-backend.d.ts +3 -0
- package/lib/typescript/module/src/backends/winrt/winrt-backend.d.ts.map +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/lib/typescript/module/src/native-protocol/rn-android-boundary.d.ts +8 -0
- package/lib/typescript/module/src/native-protocol/rn-android-boundary.d.ts.map +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/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.ts +4 -0
- package/src/backends/corebluetooth/corebluetooth-backend.ts +27 -2
- package/src/backends/winrt/winrt-backend.ts +57 -10
- package/src/backends/winrt/winrt-handles.ts +1 -1
- package/src/implementation-version.ts +1 -1
- package/src/native-protocol/rn-android-boundary.ts +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,35 @@ All notable changes to `unified-ble-manager` are documented here.
|
|
|
6
6
|
|
|
7
7
|
No changes yet.
|
|
8
8
|
|
|
9
|
+
## [4.0.15] - 2026-09-02
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
- Silence exactly one expected late React Native Android terminal when a peer
|
|
14
|
+
is lost while an operation such as GATT discovery is still settling. The
|
|
15
|
+
boundary records only operations it explicitly rejected for that peer,
|
|
16
|
+
bounds and lifecycle-scopes those records, and continues to report unknown
|
|
17
|
+
or duplicate terminals as protocol diagnostics.
|
|
18
|
+
|
|
19
|
+
## [4.0.14] - 2026-09-02
|
|
20
|
+
|
|
21
|
+
### Fixes
|
|
22
|
+
|
|
23
|
+
- Release Android GATT ownership immediately when Bluetooth powers off or
|
|
24
|
+
resets instead of waiting for a `STATE_DISCONNECTED` callback that Android
|
|
25
|
+
may never deliver. Failed native closes remain retained and retry when the
|
|
26
|
+
adapter returns, protocol routes are invalidated before adapter-state
|
|
27
|
+
publication, and an ordinary disconnect timeout now also publishes the
|
|
28
|
+
missing terminal state before cleanup. A cleanup unsubscribe after adapter
|
|
29
|
+
loss is idempotent when native ownership is already gone.
|
|
30
|
+
- Terminalize adapter-loss connections exactly once with the typed `adapter`
|
|
31
|
+
reason when a native CoreBluetooth-style or WinRT disconnect completes
|
|
32
|
+
without a platform loss callback. Delayed native callbacks are ignored, and
|
|
33
|
+
WinRT connection paths retain the attachment that created them.
|
|
34
|
+
- Treat an operation that is still settling after adapter loss as expected
|
|
35
|
+
quarantine rather than a cleanup error. Cleanup still waits and retries, but
|
|
36
|
+
only an actual native release failure produces an error diagnostic.
|
|
37
|
+
|
|
9
38
|
## [4.0.13] - 2026-09-01
|
|
10
39
|
|
|
11
40
|
### 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.15`. 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.15`. 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,7 @@ 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.14` are immutable. `4.0.15` is the current train head.
|
|
85
85
|
|
|
86
86
|
```sh
|
|
87
87
|
release_candidate=4.0.0-rc.N
|
|
@@ -133,9 +133,9 @@ The `v4.0.3` tag is immutable published history. Do not recreate or move it.
|
|
|
133
133
|
git tag -a v4.0.3 -m "v4.0.3"
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
## Releasing 4.0.
|
|
136
|
+
## Releasing 4.0.15
|
|
137
137
|
|
|
138
|
-
The `v4.0.
|
|
138
|
+
The `v4.0.15` tag must identify the exact current `main` commit after canonical
|
|
139
139
|
CI passes. Do not tag this feature branch directly.
|
|
140
140
|
|
|
141
141
|
```sh
|
|
@@ -144,15 +144,28 @@ git checkout main
|
|
|
144
144
|
git pull --ff-only origin main
|
|
145
145
|
|
|
146
146
|
test "$(git branch --show-current)" = "main"
|
|
147
|
-
test "$(node -p "require('./package.json').version")" = "4.0.
|
|
147
|
+
test "$(node -p "require('./package.json').version")" = "4.0.15"
|
|
148
148
|
git diff --exit-code
|
|
149
149
|
git diff --cached --exit-code
|
|
150
150
|
|
|
151
|
-
git tag -a v4.0.
|
|
152
|
-
git push origin v4.0.
|
|
151
|
+
git tag -a v4.0.15 -m "v4.0.15"
|
|
152
|
+
git push origin v4.0.15
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
Before tagging, confirm release-note extraction finds `## [4.0.
|
|
155
|
+
Before tagging, confirm release-note extraction finds `## [4.0.15]`.
|
|
156
|
+
|
|
157
|
+
## Releasing 4.0.14
|
|
158
|
+
|
|
159
|
+
The `v4.0.14` tag is immutable history, but its publish workflow was cancelled
|
|
160
|
+
before the npm version check and publish steps. Do not recreate or move it.
|
|
161
|
+
|
|
162
|
+
## Releasing 4.0.13
|
|
163
|
+
|
|
164
|
+
The `v4.0.13` tag is immutable published history. Do not recreate or move it.
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
git tag -a v4.0.13 -m "v4.0.13"
|
|
168
|
+
```
|
|
156
169
|
|
|
157
170
|
## Releasing 4.0.12
|
|
158
171
|
|
|
@@ -327,7 +340,7 @@ a green publish job and a package a consumer can actually install are not the
|
|
|
327
340
|
same claim.
|
|
328
341
|
|
|
329
342
|
```sh
|
|
330
|
-
version=4.0.
|
|
343
|
+
version=4.0.15
|
|
331
344
|
|
|
332
345
|
npm view "unified-ble-manager@$version" version
|
|
333
346
|
npm view unified-ble-manager dist-tags --json
|
package/SBOM.cdx.json
CHANGED
|
@@ -6,20 +6,20 @@
|
|
|
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.15",
|
|
10
10
|
"name": "unified-ble-manager",
|
|
11
|
-
"version": "4.0.
|
|
11
|
+
"version": "4.0.15",
|
|
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.15"
|
|
18
18
|
},
|
|
19
19
|
"properties": [
|
|
20
20
|
{
|
|
21
21
|
"name": "unified-ble-manager:pnpm-lock-sha256",
|
|
22
|
-
"value": "
|
|
22
|
+
"value": "fea8b9fea3dca31d966c33e5942c5fb0f179c492a8462f90482d0c5dc1bd86d2"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"name": "unified-ble-manager:dependency-scope",
|
|
@@ -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.15",
|
|
541
541
|
"dependsOn": [
|
|
542
542
|
"pkg:npm/%40babel/runtime@7.29.7",
|
|
543
543
|
"pkg:npm/node-addon-api@8.9.0",
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
"schemaVersion": "1.0.0",
|
|
5
5
|
"package": {
|
|
6
6
|
"name": "unified-ble-manager",
|
|
7
|
-
"version": "4.0.
|
|
7
|
+
"version": "4.0.15"
|
|
8
8
|
},
|
|
9
9
|
"source": {
|
|
10
10
|
"method": "pnpm-lock production graph with installed-manifest license audit",
|
|
11
11
|
"lockfile": "pnpm-lock.yaml",
|
|
12
|
-
"lockfileSha256": "
|
|
12
|
+
"lockfileSha256": "fea8b9fea3dca31d966c33e5942c5fb0f179c492a8462f90482d0c5dc1bd86d2"
|
|
13
13
|
},
|
|
14
14
|
"reviewedOverrides": [
|
|
15
15
|
{
|
|
@@ -38,7 +38,8 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
38
38
|
private var attachmentRecord: ProtocolWireRecord? = null
|
|
39
39
|
|
|
40
40
|
init {
|
|
41
|
-
radio.onAdapterState = {
|
|
41
|
+
radio.onAdapterState = { adapterState ->
|
|
42
|
+
clearGattProtocolOwnershipForAdapterLoss(adapterState)
|
|
42
43
|
emitCurrentAdapterState()
|
|
43
44
|
}
|
|
44
45
|
radio.onCleanupFailure = { failure ->
|
|
@@ -140,6 +141,33 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
private fun clearGattProtocolOwnershipForAdapterLoss(adapterState: String) {
|
|
145
|
+
val state = radio.currentProtocolAdapterState()
|
|
146
|
+
if (state.availability == "available" && state.authorization == "granted" && state.power == "on") return
|
|
147
|
+
val failure = when (adapterState) {
|
|
148
|
+
"PoweredOff" -> AdapterLossConnectFailure(
|
|
149
|
+
"adapterPoweredOff",
|
|
150
|
+
"Android Bluetooth was powered off while the connection was pending"
|
|
151
|
+
)
|
|
152
|
+
"Resetting" -> AdapterLossConnectFailure(
|
|
153
|
+
"adapterResetting",
|
|
154
|
+
"Android Bluetooth reset while the connection was pending"
|
|
155
|
+
)
|
|
156
|
+
else -> AdapterLossConnectFailure(
|
|
157
|
+
"adapterUnavailable",
|
|
158
|
+
"Android Bluetooth became unavailable while the connection was pending"
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
pendingConnects.entries.toList().forEach { entry ->
|
|
162
|
+
if (pendingConnects.remove(entry.key, entry.value)) {
|
|
163
|
+
emitFailure(entry.value, failure.code, failure.message)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
establishedConnections.clear()
|
|
167
|
+
activeDatabases.clear()
|
|
168
|
+
activeSubscriptions.clear()
|
|
169
|
+
}
|
|
170
|
+
|
|
143
171
|
fun emitCurrentAdapterState() {
|
|
144
172
|
val state = radio.currentProtocolAdapterState()
|
|
145
173
|
val fields = mutableMapOf<Int, ProtocolWireValue>(
|
|
@@ -564,6 +592,11 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
564
592
|
}
|
|
565
593
|
|
|
566
594
|
private fun subscribe(command: ProtocolWireRecord, enable: Boolean) {
|
|
595
|
+
val subscriptionId = command.requiredString(7)
|
|
596
|
+
if (!enable && !activeSubscriptions.containsKey(subscriptionId)) {
|
|
597
|
+
emitSuccess(command, "unsubscribed")
|
|
598
|
+
return
|
|
599
|
+
}
|
|
567
600
|
val endpoint = characteristicEndpoint(command.requiredRecord(4))
|
|
568
601
|
val radioOperationId = radio.setNotifyExact(
|
|
569
602
|
endpoint.deviceId,
|
|
@@ -597,7 +630,6 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
597
630
|
}
|
|
598
631
|
return@fold
|
|
599
632
|
}
|
|
600
|
-
val subscriptionId = command.requiredString(7)
|
|
601
633
|
if (enable) {
|
|
602
634
|
activeSubscriptions[subscriptionId] = SubscriptionRoute(
|
|
603
635
|
subscriptionId,
|
|
@@ -1132,6 +1164,11 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
1132
1164
|
}
|
|
1133
1165
|
}
|
|
1134
1166
|
|
|
1167
|
+
private data class AdapterLossConnectFailure(
|
|
1168
|
+
val code: String,
|
|
1169
|
+
val message: String
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1135
1172
|
private fun requirePhyAvailable() {
|
|
1136
1173
|
check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1137
1174
|
"Android PHY requires API 26"
|
|
@@ -1158,6 +1195,7 @@ internal fun connectionLostEvent(
|
|
|
1158
1195
|
8 to ProtocolWireValue.SignedIntegerValue(status.toLong())
|
|
1159
1196
|
)
|
|
1160
1197
|
)
|
|
1198
|
+
|
|
1161
1199
|
return ProtocolWireRecord(
|
|
1162
1200
|
RecordKind.EVENT,
|
|
1163
1201
|
mapOf(
|
package/android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt
CHANGED
|
@@ -150,6 +150,11 @@ internal fun bondedPeerAdapterReadiness(
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
internal fun requiresImmediateGattTeardownOnAdapterState(state: Int?): Boolean =
|
|
154
|
+
state == BluetoothAdapter.STATE_OFF ||
|
|
155
|
+
state == BluetoothAdapter.STATE_TURNING_OFF ||
|
|
156
|
+
state == BluetoothAdapter.STATE_TURNING_ON
|
|
157
|
+
|
|
153
158
|
/** Normalize the platform snapshot before it crosses the Android protocol boundary. */
|
|
154
159
|
internal fun normalizeBondedPeerSnapshots(
|
|
155
160
|
peers: Iterable<BondedPeerSnapshot>
|
|
@@ -416,6 +421,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
416
421
|
override fun onReceive(ctx: Context?, intent: Intent?) {
|
|
417
422
|
if (intent?.action != BluetoothAdapter.ACTION_STATE_CHANGED) return
|
|
418
423
|
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
|
|
424
|
+
handleAdapterStateTransition(state)
|
|
419
425
|
onAdapterState?.invoke(mapAdapterState(state))
|
|
420
426
|
}
|
|
421
427
|
}
|
|
@@ -430,6 +436,23 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
430
436
|
adapterStateReceiver = receiver
|
|
431
437
|
}
|
|
432
438
|
|
|
439
|
+
private fun handleAdapterStateTransition(state: Int) {
|
|
440
|
+
if (state == BluetoothAdapter.STATE_ON) {
|
|
441
|
+
pendingGattTeardowns.keys.toList().forEach { key -> retryGattTeardown(key) }
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
if (!requiresImmediateGattTeardownOnAdapterState(state)) return
|
|
445
|
+
|
|
446
|
+
pendingReconnect.clear()
|
|
447
|
+
(gatts.keys + pendingGattTeardowns.keys).toSet().forEach { key ->
|
|
448
|
+
failPendingForDevice(key, "adapter unavailable")
|
|
449
|
+
val owner = pendingGattTeardowns[key]
|
|
450
|
+
val gatt = gatts[key] ?: owner?.gatt ?: return@forEach
|
|
451
|
+
val teardownFailure = completeGattTeardown(key, gatt)
|
|
452
|
+
pendingDisconnectCallbacks.remove(key)?.invoke(teardownFailure)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
433
456
|
internal fun unregisterAdapterStateReceiver(): OwnedRadioTeardownFailure? {
|
|
434
457
|
val receiver = adapterStateReceiver ?: return null
|
|
435
458
|
try {
|
|
@@ -838,6 +861,18 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
838
861
|
onComplete(failure)
|
|
839
862
|
return failure
|
|
840
863
|
}
|
|
864
|
+
if (pendingGattTeardowns[key] != null) {
|
|
865
|
+
val teardownFailure = retryGattTeardown(key)
|
|
866
|
+
val failure = cleanupFailure ?: teardownFailure
|
|
867
|
+
onComplete(failure)
|
|
868
|
+
return failure
|
|
869
|
+
}
|
|
870
|
+
if (requiresImmediateGattTeardownOnAdapterState(adapter?.state)) {
|
|
871
|
+
val teardownFailure = completeGattTeardown(key, g)
|
|
872
|
+
val failure = cleanupFailure ?: teardownFailure
|
|
873
|
+
onComplete(failure)
|
|
874
|
+
return failure
|
|
875
|
+
}
|
|
841
876
|
// R3-F003: only disconnect(); close() waits for STATE_DISCONNECTED (or safety timeout).
|
|
842
877
|
pendingDisconnectCallbacks[key] = onComplete
|
|
843
878
|
scheduleSafeClose(key, g)
|
|
@@ -923,6 +958,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
923
958
|
Runnable {
|
|
924
959
|
if (gatts[key] === gatt) {
|
|
925
960
|
OwnedAndroidLog.e("GATT close safety timeout for $key (DISCONNECTED never arrived)")
|
|
961
|
+
dispatchConnectionState(key, false, BluetoothGatt.GATT_FAILURE)
|
|
926
962
|
failPendingForDevice(key, "disconnected timeout")
|
|
927
963
|
val teardownFailure = completeGattTeardown(key, gatt)
|
|
928
964
|
pendingDisconnectCallbacks.remove(key)?.invoke(teardownFailure)
|
|
@@ -11,6 +11,7 @@ import com.sfourdrinier.unifiedblemanager.radio.OwnedAndroidSubscriptionOwnershi
|
|
|
11
11
|
import com.sfourdrinier.unifiedblemanager.radio.BondedPeerSnapshot
|
|
12
12
|
import com.sfourdrinier.unifiedblemanager.radio.normalizeBondedPeerSnapshots
|
|
13
13
|
import com.sfourdrinier.unifiedblemanager.radio.bondedPeerAdapterReadiness
|
|
14
|
+
import com.sfourdrinier.unifiedblemanager.radio.requiresImmediateGattTeardownOnAdapterState
|
|
14
15
|
import org.junit.Assert.assertEquals
|
|
15
16
|
import org.junit.Assert.assertArrayEquals
|
|
16
17
|
import org.junit.Assert.assertFalse
|
|
@@ -26,6 +27,7 @@ import java.util.concurrent.CountDownLatch
|
|
|
26
27
|
import java.util.concurrent.atomic.AtomicInteger
|
|
27
28
|
import kotlin.concurrent.thread
|
|
28
29
|
import android.bluetooth.BluetoothDevice
|
|
30
|
+
import android.bluetooth.BluetoothAdapter
|
|
29
31
|
import android.bluetooth.le.ScanSettings
|
|
30
32
|
|
|
31
33
|
class UnifiedBleProtocolAndroidDispatcherLifecycleTest {
|
|
@@ -477,7 +479,7 @@ class UnifiedBleProtocolAndroidDispatcherLifecycleTest {
|
|
|
477
479
|
val consumerAndroidDirectory = File(consumerRoot, "android")
|
|
478
480
|
val packagedSource = File(consumerRoot, "node_modules/unified-ble-manager/$relativePath")
|
|
479
481
|
consumerAndroidDirectory.mkdirs()
|
|
480
|
-
packagedSource.parentFile.mkdirs()
|
|
482
|
+
requireNotNull(packagedSource.parentFile).mkdirs()
|
|
481
483
|
packagedSource.writeText("installed-package-source")
|
|
482
484
|
|
|
483
485
|
try {
|
|
@@ -788,6 +790,142 @@ class UnifiedBleProtocolAndroidDispatcherLifecycleTest {
|
|
|
788
790
|
assertEquals(1, secondStarted)
|
|
789
791
|
}
|
|
790
792
|
|
|
793
|
+
@Test
|
|
794
|
+
fun gattSafetyClosePublishesConnectionLossBeforeDroppingNativeOwnership() {
|
|
795
|
+
val radio = readAndroidSource(
|
|
796
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
|
|
797
|
+
)
|
|
798
|
+
val timeout = radio.substring(
|
|
799
|
+
radio.indexOf("private fun scheduleSafeClose"),
|
|
800
|
+
radio.indexOf("private fun cancelSafeClose")
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
val connectionLossIndex = timeout.indexOf("dispatchConnectionState(key, false, BluetoothGatt.GATT_FAILURE)")
|
|
804
|
+
val failPendingIndex = timeout.indexOf("failPendingForDevice(key, \"disconnected timeout\")")
|
|
805
|
+
val teardownIndex = timeout.indexOf("val teardownFailure = completeGattTeardown(key, gatt)")
|
|
806
|
+
assertTrue(connectionLossIndex >= 0)
|
|
807
|
+
assertTrue(connectionLossIndex < failPendingIndex)
|
|
808
|
+
assertTrue(connectionLossIndex < teardownIndex)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
@Test
|
|
812
|
+
fun adapterLossClosesGattOwnersBeforeForwardingTheState() {
|
|
813
|
+
val radio = readAndroidSource(
|
|
814
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
|
|
815
|
+
)
|
|
816
|
+
val receiver = radio.substring(
|
|
817
|
+
radio.indexOf("fun registerAdapterStateReceiver()"),
|
|
818
|
+
radio.indexOf("internal fun unregisterAdapterStateReceiver")
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
val teardownIndex = receiver.indexOf("handleAdapterStateTransition(state)")
|
|
822
|
+
val forwardIndex = receiver.indexOf("onAdapterState?.invoke(mapAdapterState(state))")
|
|
823
|
+
assertTrue(teardownIndex >= 0)
|
|
824
|
+
assertTrue(teardownIndex < forwardIndex)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
@Test
|
|
828
|
+
fun onlyUnavailableAdapterTransitionsRequireImmediateGattTeardown() {
|
|
829
|
+
assertFalse(requiresImmediateGattTeardownOnAdapterState(BluetoothAdapter.STATE_ON))
|
|
830
|
+
assertTrue(requiresImmediateGattTeardownOnAdapterState(BluetoothAdapter.STATE_OFF))
|
|
831
|
+
assertTrue(requiresImmediateGattTeardownOnAdapterState(BluetoothAdapter.STATE_TURNING_OFF))
|
|
832
|
+
assertTrue(requiresImmediateGattTeardownOnAdapterState(BluetoothAdapter.STATE_TURNING_ON))
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
@Test
|
|
836
|
+
fun adapterLossTeardownDoesNotWaitForGattDisconnectedCallback() {
|
|
837
|
+
val radio = readAndroidSource(
|
|
838
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
|
|
839
|
+
)
|
|
840
|
+
val transition = radio.substring(
|
|
841
|
+
radio.indexOf("private fun handleAdapterStateTransition("),
|
|
842
|
+
radio.indexOf("internal fun unregisterAdapterStateReceiver")
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
assertTrue(transition.contains("pendingReconnect.clear()"))
|
|
846
|
+
assertTrue(transition.contains("failPendingForDevice(key, \"adapter unavailable\")"))
|
|
847
|
+
assertTrue(transition.contains("completeGattTeardown(key, gatt)"))
|
|
848
|
+
assertTrue(transition.contains("pendingDisconnectCallbacks.remove(key)?.invoke(teardownFailure)"))
|
|
849
|
+
assertFalse(transition.contains("scheduleSafeClose"))
|
|
850
|
+
assertFalse(transition.contains("dispatchConnectionState"))
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
@Test
|
|
854
|
+
fun adapterRecoveryRetriesRetainedGattTeardownOwnership() {
|
|
855
|
+
val radio = readAndroidSource(
|
|
856
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
|
|
857
|
+
)
|
|
858
|
+
val transition = radio.substring(
|
|
859
|
+
radio.indexOf("private fun handleAdapterStateTransition("),
|
|
860
|
+
radio.indexOf("internal fun unregisterAdapterStateReceiver")
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
assertTrue(transition.contains("BluetoothAdapter.STATE_ON"))
|
|
864
|
+
assertTrue(transition.contains("pendingGattTeardowns.keys.toList()"))
|
|
865
|
+
assertTrue(transition.contains("retryGattTeardown(key)"))
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
@Test
|
|
869
|
+
fun disconnectRetriesRetainedOrUnavailableGattWithoutSchedulingSafetyClose() {
|
|
870
|
+
val radio = readAndroidSource(
|
|
871
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
|
|
872
|
+
)
|
|
873
|
+
val disconnect = radio.substring(
|
|
874
|
+
radio.indexOf("internal fun disconnect("),
|
|
875
|
+
radio.indexOf("private fun openGatt")
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
val retainedIndex = disconnect.indexOf("pendingGattTeardowns[key]")
|
|
879
|
+
val unavailableIndex = disconnect.indexOf("requiresImmediateGattTeardownOnAdapterState(adapter?.state)")
|
|
880
|
+
val safetyCloseIndex = disconnect.indexOf("scheduleSafeClose(key, g)")
|
|
881
|
+
assertTrue(retainedIndex >= 0)
|
|
882
|
+
assertTrue(unavailableIndex > retainedIndex)
|
|
883
|
+
assertTrue(safetyCloseIndex > unavailableIndex)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
@Test
|
|
887
|
+
fun adapterLossClearsProtocolGattRoutesBeforePublishingAdapterState() {
|
|
888
|
+
val dispatcher = readAndroidSource(
|
|
889
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt"
|
|
890
|
+
)
|
|
891
|
+
val callback = dispatcher.substring(
|
|
892
|
+
dispatcher.indexOf("radio.onAdapterState = {"),
|
|
893
|
+
dispatcher.indexOf("radio.onCleanupFailure")
|
|
894
|
+
)
|
|
895
|
+
val clearIndex = callback.indexOf("clearGattProtocolOwnershipForAdapterLoss(adapterState)")
|
|
896
|
+
val emitIndex = callback.indexOf("emitCurrentAdapterState()")
|
|
897
|
+
assertTrue(clearIndex >= 0)
|
|
898
|
+
assertTrue(clearIndex < emitIndex)
|
|
899
|
+
|
|
900
|
+
val cleanup = dispatcher.substring(
|
|
901
|
+
dispatcher.indexOf("private fun clearGattProtocolOwnershipForAdapterLoss(adapterState: String)"),
|
|
902
|
+
dispatcher.indexOf("fun emitCurrentAdapterState()")
|
|
903
|
+
)
|
|
904
|
+
assertTrue(cleanup.contains("establishedConnections.clear()"))
|
|
905
|
+
assertTrue(cleanup.contains("activeDatabases.clear()"))
|
|
906
|
+
assertTrue(cleanup.contains("activeSubscriptions.clear()"))
|
|
907
|
+
assertTrue(cleanup.contains("pendingConnects.entries.toList()"))
|
|
908
|
+
assertTrue(cleanup.contains("pendingConnects.remove(entry.key, entry.value)"))
|
|
909
|
+
assertTrue(cleanup.contains("emitFailure(entry.value, failure.code, failure.message)"))
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
@Test
|
|
913
|
+
fun unsubscribeIsIdempotentAfterAdapterLossClearsNativeOwnership() {
|
|
914
|
+
val dispatcher = readAndroidSource(
|
|
915
|
+
"android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt"
|
|
916
|
+
)
|
|
917
|
+
val subscribe = dispatcher.substring(
|
|
918
|
+
dispatcher.indexOf("private fun subscribe(command: ProtocolWireRecord, enable: Boolean)"),
|
|
919
|
+
dispatcher.indexOf("private fun destroy(command: ProtocolWireRecord)")
|
|
920
|
+
)
|
|
921
|
+
val absentRouteIndex = subscribe.indexOf("!enable && !activeSubscriptions.containsKey(subscriptionId)")
|
|
922
|
+
val successIndex = subscribe.indexOf("emitSuccess(command, \"unsubscribed\")")
|
|
923
|
+
val nativeIndex = subscribe.indexOf("radio.setNotifyExact(")
|
|
924
|
+
assertTrue(absentRouteIndex >= 0)
|
|
925
|
+
assertTrue(successIndex > absentRouteIndex)
|
|
926
|
+
assertTrue(nativeIndex > successIndex)
|
|
927
|
+
}
|
|
928
|
+
|
|
791
929
|
@Test
|
|
792
930
|
fun destroyBeforePumpCallsEveryQueuedCancellationAndNeverStartsThem() {
|
|
793
931
|
val scheduled = ArrayDeque<() -> Unit>()
|
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.15`. 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.15` 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.15`; 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.15`; 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.15`, 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.15
|
|
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.15`. 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.15`. 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.15`
|
|
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
|
|
|
@@ -39,12 +39,15 @@ async function releaseCoreBluetoothAdapterLossResources(state) {
|
|
|
39
39
|
failures.push(...cleanup.failures);
|
|
40
40
|
}
|
|
41
41
|
for (const record of [...state.connections.values()]) {
|
|
42
|
+
const previous = record.state === 'disconnecting' ? 'disconnecting' : 'connected';
|
|
42
43
|
record.state = 'disconnecting';
|
|
43
44
|
try {
|
|
44
45
|
const cleanup = await state.disconnectNative(record, 'direct-gatt.adapter-loss.disconnect', true);
|
|
45
46
|
failures.push(...cleanup.failures);
|
|
46
47
|
if (cleanup.state === 'release-failed') {
|
|
47
48
|
record.state = 'connected';
|
|
49
|
+
} else {
|
|
50
|
+
state.terminalizeAdapterLossConnection(record, previous);
|
|
48
51
|
}
|
|
49
52
|
} catch (error) {
|
|
50
53
|
record.state = 'connected';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_corebluetoothHandles","require","releaseCoreBluetoothAdapterLossResources","state","failures","group","scanGroup","consumer","consumers","values","releaseScanConsumerAdmission","stream","closeWithReason","cleanup","stopNativeScan","push","clear","clearScanGroup","error","cleanupFailureDetail","physical","subscriptions","subscription","removed","gattOperations","stopPhysicalSubscription","record","connections","disconnectNative","length","releasedCleanup","Object","freeze"],"sourceRoot":"../../../../src","sources":["backends/corebluetooth/corebluetooth-adapter-loss-cleanup.ts"],"mappings":";;;;;;AAGA,IAAAA,qBAAA,GAAAC,OAAA;AAHA;;
|
|
1
|
+
{"version":3,"names":["_corebluetoothHandles","require","releaseCoreBluetoothAdapterLossResources","state","failures","group","scanGroup","consumer","consumers","values","releaseScanConsumerAdmission","stream","closeWithReason","cleanup","stopNativeScan","push","clear","clearScanGroup","error","cleanupFailureDetail","physical","subscriptions","subscription","removed","gattOperations","stopPhysicalSubscription","record","connections","previous","disconnectNative","terminalizeAdapterLossConnection","length","releasedCleanup","Object","freeze"],"sourceRoot":"../../../../src","sources":["backends/corebluetooth/corebluetooth-adapter-loss-cleanup.ts"],"mappings":"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","ignoreList":[]}
|