Draft
Conversation
…Codex Android review)
Fixes from Codex review: - Use Codegen typed events, not RCTEventEmitter (legacy) - iOS: @objc adapter over Swift actors (actors not TurboModule surface) - Background BLE with state restoration is supported (not dropped) - Rich error model with platform diagnostics (gattStatus, attErrorCode, isRetryable) - Event batching: per-stream rules, bypass for timing-sensitive, ordering contract - Android mock testing: custom MockBleManager (Nordic mock utils not in v2.x) - Support floor: RN 0.82+ (legacy arch removed) - Additional test cases: permission revoke, BT off, bond failure, restoration
…ding/L2CAP/PHY error codes
…earch
Research-driven fixes:
- Codegen spec rewritten: NativeBlePlx.ts with inline Readonly<{}> types,
CodegenTypes.EventEmitter<T> for all streaming events, no Object escape hatches
- codegenConfig added for package.json
- TurboModuleRegistry.get (nullable) instead of getEnforcing
- iOS: custom actor executor on CB DispatchQueue (Swift 5.9+) for non-Sendable CB objects
- iOS: ObjC++ .mm entry point (pure Swift TurboModules not possible)
- iOS: @preconcurrency import CoreBluetooth for Swift 6
- iOS: GATT operation queue (CB does NOT serialize operations)
- requestConnectionParameters → requestConnectionPriority (coarse, Android-only)
- Scanner Compat does NOT handle throttling — built-in debouncing required
- Auto-MTU 517 on Android connect (fixes dotintent#1 user-reported issue)
- L2CAP expanded: open/write/close lifecycle, dynamic PSMs only
- Device identifier incompatibility documented (MAC vs opaque UUID)
- onStateChange as EventEmitter (not callback — callbacks are single-fire)
- Background scan on iOS: must specify service UUID filters
…latforms, bonding contract, restore ordering
- Fix filename inconsistency: NativeBlePlx.ts throughout (was NativeBleModule.ts in header)
- Replace optionsJson string with typed Readonly<{}> for scan and connect options
- Android 14 auto-MTU caveat: skip requestMtu if system already negotiated >= 517
- Add neverForLocation manifest flag requirement
- Note for impl: transactionId→Nordic Request mapping, bonding vs encryption race,
L2CAP StreamDelegate bridge, concurrent write+indicate stress test needed
… (Codex final validation)
…ignature, L2CAP wiring, connection events, task ordering
…en spec Add subscriptionType parameter to monitorCharacteristic to allow explicit indicate vs notify selection (null = auto-detect, preferring notify). Add getMtu(deviceId) method to read current negotiated MTU. Add onBondStateChange event emitter for bond state transitions (none/bonding/bonded).
Add src/specs/NativeBlePlx.ts — the Codegen contract for the v4 TurboModule rewrite. Defines all typed interfaces (DeviceInfo, CharacteristicInfo, ScanResult, etc.), method signatures, and ten typed CodegenTypes.EventEmitter declarations. Uses TurboModuleRegistry.get (nullable) per spec. Add codegenConfig to package.json pointing at src/specs with Android javaPackageName com.bleplx.
add types.ts with State, LogLevel, ConnectionPriority, ConnectionState const objects and ScanOptions/ConnectOptions interfaces add BleError.ts with BleErrorCode const object and BleError class for unified cross-platform error handling add __tests__/BleError.test.ts with constructor and code value tests fix tooling for typescript support in src/ and __tests__/ directories
…cation flag - AndroidManifestNew.xml was empty (bare `<manifest></manifest>`), breaking AGP namespace mode manifest merging; add xmlns:android and xmlns:tools declarations - Default neverForLocation to true so BLUETOOTH_SCAN gets android:usesPermissionFlags="neverForLocation" out of the box, matching the library's own AndroidManifest.xml; apps that derive location from BLE can opt out by setting neverForLocation: false in their Expo config
Replace the v3 Java/RxJava implementation with a Kotlin TurboModule architecture using Nordic Android-BLE-Library 2.11.0 for GATT operations and Scanner Compat 1.6.0 for scanning. - BlePlxModule extends Codegen-generated NativeBlePlxSpec - BleManagerWrapper: per-device Nordic BleManager with coroutine suspend APIs - ScanManager: Scanner Compat with 5-starts/30s throttle protection - ErrorConverter: GATT status codes mapped to unified BleErrorCode - EventSerializer: native objects serialized to Codegen event types - PermissionHelper: runtime checks for API 31+ permission model - Fixed AndroidManifestNew.xml (was empty in v3) - Removed all v3 Java source files under android/src/main/java/
…on cleanup, missing APIs - EventBatcher.dispose() now discards buffered events instead of flushing, preventing callbacks from firing during teardown - Use Platform.OS instead of hardcoded 'ios' in onDeviceDisconnected - Track consumer subscriptions (onStateChange, onDeviceDisconnected, etc.) and clean them up in destroyClient() - Clean up existing monitor when duplicate transactionId is provided - Guard onStateChange callback with removed flag to prevent post-remove firing - Clean up previous errorSubscription in createClient() to prevent leaks - Add missing API methods: getMtu, requestPhy, readPhy, getBondedDevices, getAuthorizationStatus, L2CAP channel ops, onRestoreState, onBondStateChange, onConnectionEvent - Add BleManagerOptions with scanBatchIntervalMs constructor option - Add subscriptionType to MonitorOptions - Update tests to cover all new behavior
…t scaffolding - __tests__/protocol.test.ts: 32 tests covering base64 round-trip, DeviceInfo and CharacteristicInfo shape parsing, BleError construction from native error events, BleErrorCode distinctness, and State/ConnectionState/ConnectionPriority const object values - integration-tests/simulated/js/fullSync.test.ts: full JS-layer flow tests (mocked TurboModule) covering createClient->scan->connect->write->monitor-> disconnect, audit dotintent#3 subscription.remove() fix verification, and error scenarios (permission denied, timeout, GATT error) - integration-tests/hardware/maestro/: three Maestro flow scaffolds (scan-pair-sync, disconnect-recovery, indicate-stress) with inline comments - integration-tests/hardware/peripheral-firmware/README.md: GATT profile requirements for the BlePlxTest peripheral - integration-tests/hardware/test-app/README.md: test app setup and testID conventions - android/src/test/kotlin/com/bleplx/README.md: JUnit 5 + MockK test plan - ios/Tests/README.md: XCTest + CoreBluetoothMock test plan
…codes, Swift sendable
- Delete 13 v3 JS source files (replaced by TypeScript in v4) - Delete 9 v3 JS test files (replaced by v4 .test.ts files) - Delete .flowconfig - Remove Flow/hermes devDependencies from package.json - Remove Flow/hermes ESLint override from .eslintrc.json - Clean up .eslintignore (no more v3 JS files to ignore)
Adds Arduino sketch for Seeed XIAO nRF52840 that exposes a five-characteristic GATT test service covering read, authenticated write, notify, indicate, and MTU negotiation — the core BLE operations exercised by react-native-ble-plx v4.
…uilds - Wire react-native-ble-plx (file:..) into example app with navigation deps - Create ScanScreen, DeviceScreen, CharacteristicScreen test screens - Update Metro config to resolve library from parent directory - Fix Android: BleManagerWrapper notification callback (DataReceivedCallback via setNotificationCallback, not enableIndications.with()) - Fix iOS: migrate from bridging header to module imports for framework target - Fix iOS: replace DispatchQueue.asUnownedSerialExecutor() with DispatchSerialQueue (required for Swift 6.2 / Xcode 26) - Fix iOS: refactor BLEModuleImpl to use typed CodeGen emit methods instead of RCTEventEmitter.sendEvent pattern - Bump iOS deployment target to 17.0 (required for DispatchSerialQueue) - Downgrade Gradle to 8.13 for JDK 21 compatibility - Both Android assembleDebug and iOS xcodebuild succeed
Replace placeholder element IDs in all three existing Maestro flow files with the actual testID values from the example app screens (ScanScreen, DeviceScreen, CharacteristicScreen). Add the missing write-read-roundtrip flow and a README documenting prerequisites, permission pre-grant commands, and the testID reference table. Key changes: - scan-start-btn / scan-stop-btn replace start-scan-button - disconnect-btn replaces disconnect-button - discover-btn replaces implicit service-discovery step - monitor-toggle (Switch) replaces stop-monitor-button - value-display replaces char-value / indicate-count-label - char-fff1/fff2/fff3 used for known firmware characteristic UUIDs - disconnect-recovery updated to match app navigation (goBack to ScanScreen) - indicate-stress updated to use CharacteristicScreen monitor-toggle pattern
Add JUnit 5 + MockK tests for ScanManager throttle logic, ErrorConverter GATT mapping, PermissionHelper SDK gating, and EventSerializer serialization on Android; add XCTest suites for GATTOperationQueue serial/cancel/timeout behaviour, ErrorConverter CB/ATT error mapping, and EventSerializer snapshot dictionaries on iOS. Wire JUnit 5 platform into android/build.gradle.
Add ios.modules config to codegenConfig in package.json so the codegen
generates the NativeBlePlx -> BlePlx mapping in RCTModuleProviders.mm.
Without this, TurboModuleRegistry.get('NativeBlePlx') returns null at
runtime even though the native code compiles fine.
Also exclude ios/Tests/ and ios/BlePlx.xcodeproj/ from the podspec
source_files to prevent XCTest import failures in app builds.
… flag - ios/BlePlx.mm: change startDeviceScan/connectToDevice option params from NSDictionary* to Codegen C++ struct types, fix nullability annotations - example/ios/Podfile: add -ObjC to OTHER_LDFLAGS to prevent dead-stripping - example/src/App.tsx: restore real test screens
…versions The BleManager constructor eagerly called TurboModuleRegistry.get() which used the library's devDependency react-native (0.77.0) instead of the example app's react-native (0.84.1). The older TurboModuleRegistry had different bridgeless checks that prevented the module from being found at runtime. Two fixes: - metro.config.js: add resolveRequest to force react/react-native to always resolve from the example app's node_modules, preventing version mismatches between JS and native runtime - BleManager.ts: lazy-initialize the native module via a getter instead of in the constructor, so the TurboModule lookup happens on first use rather than at instantiation time
The library root had react-native@0.77.0 in devDependencies/node_modules, causing Metro to resolve the wrong TurboModuleRegistry for library code. Remove library's node_modules from nodeModulesPaths — only the example app's node_modules should be in the search path.
BREAKING CHANGE: Requires React Native >= 0.82.0 (New Architecture only). Removes enable()/disable(), setLogLevel(), and the old NativeModule bridge. Library: - TurboModule (Fabric) implementation replacing the old bridge - Android: Kotlin + Nordic BLE Library - iOS: Swift actors + CoreBluetooth - New monitorL2CAPChannel() API with iOS openL2CAPChannel fix - All 25 audit issues from v3 addressed - yarn prepack builds clean (68 files), lint passes, 32/32 tests pass E2E Tests (17 flows, both platforms): - Tests 01-11: Core BLE operations (scan, connect, read, write, notify, indicate, MTU, disconnect, UUID filter, background, L2CAP) - Tests 12-17: requestMTU, monitor unsubscribe, invalid write, L2CAP echo, payload boundaries, permission denied Example Apps: - Bare RN example (React Native 0.84, React Navigation) - Expo SDK 55 example (expo-router, verified on iPhone + Pixel 6) Documentation: - README: Expo-first quickstart - API reference, migration guide, troubleshooting, testing guide
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this?
A complete rewrite of react-native-ble-plx for React Native's New Architecture (TurboModules + Fabric). This isn't a patch — it's a ground-up reimplementation of the native modules, JS API layer, example apps, documentation, and test infrastructure.
I started this because v3 had 25 documented issues (thread safety, memory leaks, broken APIs) that couldn't be fixed incrementally, and the library had no path to New Architecture support. Rather than file 25 separate issues, I built the fix. Oh, and the bigger "why"? I have a project I'm starting that needs BLE, and Claude is giving away extra usage on weekends.
I know, I know... v4 drops support for everything but the latest Expo SDK and newer RN versions. I felt that v3 could continue to support old versions, but if someone really wants the latest & greatest, they need to bring their app up to the latest & greatest also.
How this was built
Full transparency: this rewrite was primarily built using Claude Code (Opus 4.6), with peer review assistance from OpenAI Codex (GPT-5.4) and Google Gemini. I directed the architecture, made design decisions, tested on physical hardware, and validated all changes — but the AI agents wrote the vast majority of the code, tests, documentation, and even most of this PR description on my behalf. I believe in full disclosure for OSS contributions.
What changed
Native modules — completely rewritten:
JS API — mostly compatible, some breaking changes:
enable()/disable()removed (broken on Android 12+, never worked reliably)setLogLevel()removedas constobjectsmonitorL2CAPChannel(),requestPhy()/readPhy(),onBondStateChange(),onConnectionEvent()Testing — new hardware E2E suite:
run-e2e.sh) that manages both devicesExample apps:
Documentation — complete rewrite:
Breaking changes
enable()/disable()removedsetLogLevel()removedas constobjectsBleManageris now a true singleton (was already trending this way in v3.4.0)createClient()must be called explicitly before using any APIWhat I'd like from maintainers
This is a large PR and I don't expect an immediate merge. I'm opening it as a draft so you can:
I'm happy to split this into smaller PRs if that's preferred, though the native modules are interdependent so it may not decompose cleanly.
How to test
CI note
The existing CI matrix tests against RN 0.77.0 and 0.76.6, which are below v4's minimum of 0.82.0. Those matrix jobs will fail by design. The CI config will need updating for v4.