mobile-hardware-ble-nfc
BLE scanning/connecting/GATT operations with react-native-ble-plx, NFC tag reading/writing with react-native-nfc-manager, permissions, background mode, battery-efficient patterns
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-hardware-ble-nfc/skills/mobile-hardware-ble-nfc
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
BLE & NFC Patterns
Quick Guide: Use
react-native-ble-plxfor BLE (scanning, connecting, GATT read/write/monitor). Usereact-native-nfc-managerfor NFC (NDEF read/write, tag technology access). BLE values are Base64-encoded -- decode before use. NFC operations follow request-technology/operate/cancel-technology lifecycle. Always clean up: remove BLE subscriptions, callcancelTechnologyRequest()for NFC, anddestroy()the BleManager. MTU defaults to 23 bytes (20 usable) -- negotiate higher on Android. iOS auto-negotiates up to 187 bytes.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST call destroy() on BleManager when deallocating resources -- leaking the manager causes native memory leaks and zombie listeners)
(You MUST call discoverAllServicesAndCharacteristics() after connecting before any read/write/monitor operations -- GATT structure is not available until discovered)
(You MUST call cancelTechnologyRequest() in a finally block after every NFC operation -- failing to release the NFC session blocks subsequent scans)
(You MUST check BLE adapter state (PoweredOn) before scanning -- scanning while powered off or unauthorized throws errors silently on some devices)
(You MUST remove all BLE subscriptions (scan listeners, characteristic monitors, disconnect listeners) on cleanup -- leaked subscriptions cause crashes after component unmount)
</critical_requirements>
Auto-detection: react-native-ble-plx, BleManager, startDeviceScan, connectToDevice, monitorCharacteristicForDevice, writeCharacteristicWithResponseForDevice, readCharacteristicForDevice, requestMTUForDevice, react-native-nfc-manager, NfcManager, NfcTech, Ndef, requestTechnology, cancelTechnologyRequest, writeNdefMessage, ndefHandler, useCodeScanner BLE, BLE scanning, NFC tag, NDEF record, characteristic notification, GATT
When to use:
- Scanning for and connecting to BLE peripherals (IoT sensors, wearables, medical devices)
- Reading/writing BLE GATT characteristics and monitoring notifications
- Reading NDEF tags or writing NDEF records to NFC tags
- Implementing background BLE reconnection with state restoration
- MTU negotiation for large data transfers over BLE
- Accessing low-level NFC technologies (NfcA, IsoDep, MifareUltralight)
When NOT to use:
- Classic Bluetooth audio/file transfer (different protocol, different libraries)
- BLE peripheral/server mode (react-native-ble-plx is central-only)
- Web-based Bluetooth (use Web Bluetooth API)
- Wi-Fi Direct or peer-to-peer networking
Key patterns covered:
- BLE lifecycle: scan, connect, discover services, read/write/monitor, disconnect
- Battery-efficient scanning with UUID filters and scan modes
- MTU negotiation (Android explicit, iOS automatic)
- Characteristic subscriptions (notifications/indications) with cleanup
- BLE reconnection and disconnect monitoring
- NFC NDEF read/write lifecycle with technology request/cancel
- NFC technology types and platform availability (iOS vs Android)
- Permission handling for both BLE and NFC
Detailed Resources:
- examples/core.md - BLE scanning, connecting, GATT operations, disconnect handling
- examples/nfc.md - NFC NDEF reading/writing, technology types, platform differences
- reference.md - API quick reference, permission matrix, decision frameworks
<decision_framework>
Decision Framework
BLE vs NFC
What kind of hardware interaction?
|
+-> Continuous connection with a peripheral (sensor, wearable)?
| +-> BLE -- long-lived connection with GATT operations
|
+-> One-tap read/write (tag, card)?
| +-> NFC -- session-based, tap and go
|
+-> Background monitoring of nearby devices?
| +-> BLE -- background scanning with state restoration
|
+-> Quick device provisioning (write config to tag)?
+-> NFC -- write NDEF record, tap target device
BLE Write Method
Which write method?
|
+-> Data MUST arrive reliably?
| +-> writeCharacteristicWithResponseForService (acknowledged, slower)
|
+-> Speed matters more than reliability?
| +-> writeCharacteristicWithoutResponseForService (fire-and-forget, faster)
|
+-> Large payload (> MTU)?
+-> Negotiate higher MTU first, then use write-with-response
NFC Technology Selection
What kind of NFC tag?
|
+-> Standard NDEF content (URL, text, MIME)?
| +-> NfcTech.Ndef (iOS + Android)
|
+-> ISO 14443-3A tag (raw commands)?
| +-> NfcTech.NfcA (iOS + Android)
|
+-> Smart card / ISO 7816 (APDU commands)?
| +-> NfcTech.IsoDep (iOS + Android)
|
+-> Mifare Classic (Android only)?
| +-> NfcTech.MifareClassic
|
+-> Mifare Ultralight (Android only)?
+-> NfcTech.MifareUltralight
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Skipping
discoverAllServicesAndCharacteristics()after connecting -- read/write/monitor calls fail silently or throw errors because GATT structure is not cached - Not removing BLE subscriptions on cleanup -- leaked subscriptions fire callbacks after component unmount, causing "setState on unmounted component" crashes
- Forgetting
cancelTechnologyRequest()in NFC finally block -- NFC session stays locked, all subsequent NFC operations fail until app restart - Creating multiple BleManager instances -- each instance allocates native resources. Create ONE and share it. Call
destroy()only once on app teardown. - Scanning without UUID filter and without timeout -- scans all devices indefinitely, drains battery rapidly
Medium Priority Issues:
- Not checking
State.PoweredOnbefore scanning -- scanning while Bluetooth is off throws errors on some devices and does nothing on others - Not handling Base64 encoding/decoding for BLE characteristic values -- raw Base64 strings are not human-readable and cannot be compared directly
- Using
writeCharacteristicWithoutResponseForServicefor critical data -- fire-and-forget may lose data; use write-with-response for reliability - Not requesting MTU on Android -- default 23-byte MTU means 20 usable bytes per packet, making large transfers extremely slow
- Hardcoding service/characteristic UUIDs inline instead of using named constants
Gotchas & Edge Cases:
- iOS BLE device IDs are random UUIDs, not MAC addresses -- they can change after Bluetooth is toggled or the device restarts. Do not persist iOS device IDs for reconnection; use service UUID scanning instead.
- Android 12+ changed BLE permissions --
BLUETOOTH_SCANandBLUETOOTH_CONNECTreplacedACCESS_FINE_LOCATIONfor BLE scanning (setneverForLocation: truein Expo config if scanning does not need location) - Android 14+ defaults MTU to 517 bytes --
requestMTUmay be unnecessary on newer Android devices. Checkdevice.mtuafter connection. - iOS auto-negotiates MTU up to 187 bytes -- calling
requestMTUon iOS has no effect onDeviceDisconnectedfires once per registration -- you must re-register the listener after each reconnection if you want continued disconnect monitoring- NfcTech.NfcB, NfcF, NfcV are Android-only -- iOS has different equivalents (Iso15693IOS, FelicaIOS). Always check platform before requesting a technology.
- NFC on iOS shows a system scan dialog -- you cannot customize it beyond the
alertMessage. On Android, scanning is silent. - BLE
allowDuplicatesis iOS-only -- Android always emits duplicates. Deduplicate in your scan callback using a Set of device IDs. - HCE (Host Card Emulation) is NOT supported by react-native-nfc-manager -- use a dedicated library like
react-native-hcefor card emulation - NFC
getTag()returns the last discovered tag -- if no tag was tapped during the session, it returns the previous tag. Always request technology first.
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST call destroy() on BleManager when deallocating resources -- leaking the manager causes native memory leaks and zombie listeners)
(You MUST call discoverAllServicesAndCharacteristics() after connecting before any read/write/monitor operations -- GATT structure is not available until discovered)
(You MUST call cancelTechnologyRequest() in a finally block after every NFC operation -- failing to release the NFC session blocks subsequent scans)
(You MUST check BLE adapter state (PoweredOn) before scanning -- scanning while powered off or unauthorized throws errors silently on some devices)
(You MUST remove all BLE subscriptions (scan listeners, characteristic monitors, disconnect listeners) on cleanup -- leaked subscriptions cause crashes after component unmount)
Failure to follow these rules will cause native crashes, memory leaks, blocked NFC sessions, and battery drain.
</critical_reminders>
Files (skills)
-
examples
-
core.md 14.4 KB
# BLE & NFC - Core BLE Patterns > BLE scanning, connecting, GATT operations, characteristic monitoring, disconnect handling, and reconnection. See [nfc.md](nfc.md) for NFC patterns. See [SKILL.md](../SKILL.md) for red flags and decision guidance. **Prerequisites:** react-native-ble-plx v3.2+, base-64 (for encoding/decoding characteristic values) --- ## Pattern 1: BleManager Initialization and State Monitoring Create a single BleManager instance for the entire app. Monitor adapter state to know when BLE is ready. ```typescript import { BleManager, State, type Subscription } from "react-native-ble-plx"; // Singleton -- create once, share everywhere const manager = new BleManager(); function waitForPoweredOn(): Promise<void> { return new Promise((resolve, reject) => { const subscription = manager.onStateChange((state) => { if (state === State.PoweredOn) { subscription.remove(); resolve(); } else if (state === State.Unsupported) { subscription.remove(); reject(new Error("BLE is not supported on this device")); } else if (state === State.Unauthorized) { subscription.remove(); reject(new Error("Bluetooth permission not granted")); } }, true); // true = emit current state immediately }); } // Usage async function initBle() { await waitForPoweredOn(); // Now safe to scan and connect } // App teardown -- call once when app is being destroyed function teardownBle() { manager.destroy(); } ``` **Why good:** single manager instance avoids native resource leaks, state checked before operations, subscription removed after state resolved, `true` flag avoids missing initial state ```typescript // Bad: creating manager per component function MyComponent() { const manager = new BleManager(); // Leaks on every mount // ... } ``` **Why bad:** each BleManager allocates native resources, unmounting without destroy() causes memory leaks --- ### Background Mode and State Restoration For apps that must maintain BLE connections when backgrounded (iOS): ```typescript const RESTORE_ID = "my-app-ble-restore"; const manager = new BleManager({ restoreStateIdentifier: RESTORE_ID, restoreStateFunction: (restoredState) => { if (restoredState?.connectedPeripherals) { // Re-establish monitoring on peripherals that were connected // when the app was terminated by iOS for (const device of restoredState.connectedPeripherals) { // Re-subscribe to characteristics } } }, }); ``` **Platform setup:** - **iOS:** Enable "Uses Bluetooth LE Accessories" in Background Modes capability - **Android:** Set `isBackgroundEnabled: true` in Expo plugin config - **Expo:** Add `react-native-ble-plx` plugin with `{ isBackgroundEnabled: true, modes: ["central"] }` in app.json --- ## Pattern 2: BLE Scanning with Permissions Always request permissions before scanning. Filter by service UUID for battery efficiency. ```typescript import { Platform, PermissionsAndroid } from "react-native"; import { BleManager, type Device, type Subscription, ScanMode, } from "react-native-ble-plx"; const HEART_RATE_SERVICE = "0000180d-0000-1000-8000-00805f9b34fb"; const SCAN_TIMEOUT_MS = 15000; const ANDROID_12_API_LEVEL = 31; async function requestBlePermissions(): Promise<boolean> { if (Platform.OS === "android") { if (Platform.Version >= ANDROID_12_API_LEVEL) { const results = await PermissionsAndroid.requestMultiple([ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, ]); return Object.values(results).every( (status) => status === PermissionsAndroid.RESULTS.GRANTED, ); } // Android < 12: location permission required for BLE scanning const result = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, ); return result === PermissionsAndroid.RESULTS.GRANTED; } // iOS: handled via Info.plist (NSBluetoothAlwaysUsageDescription) return true; } async function scanForDevices( manager: BleManager, onDeviceFound: (device: Device) => void, ): Promise<void> { const hasPermission = await requestBlePermissions(); if (!hasPermission) { throw new Error("BLE permissions not granted"); } await waitForPoweredOn(); const seenDeviceIds = new Set<string>(); manager.startDeviceScan( [HEART_RATE_SERVICE], // Filter by service UUID -- null scans ALL devices (battery drain) { allowDuplicates: false, // iOS only -- Android always emits duplicates // Android scan modes: // ScanMode.LowLatency -- fastest, highest battery use // ScanMode.Balanced -- default // ScanMode.LowPower -- slowest, lowest battery // ScanMode.Opportunistic -- piggybacks on other apps' scans }, (error, device) => { if (error) { console.error("Scan error:", error.message); return; } if (!device) return; // Manual deduplication for Android (allowDuplicates has no effect) if (seenDeviceIds.has(device.id)) return; seenDeviceIds.add(device.id); onDeviceFound(device); }, ); // Always set a timeout to stop scanning setTimeout(() => { manager.stopDeviceScan(); }, SCAN_TIMEOUT_MS); } ``` **Why good:** permissions checked per Android API level, UUID filter reduces battery drain, scan timeout prevents indefinite scanning, manual deduplication handles Android behavior, named constants for UUIDs and timeouts ```typescript // Bad: scanning without filter, no timeout, no permissions manager.startDeviceScan(null, null, (error, device) => { setDevices((prev) => [...prev, device!]); }); ``` **Why bad:** null UUID filter scans all nearby BLE devices (battery killer), no timeout means scan runs forever, no permission check, no null guard on device, setState on every callback floods renders --- ## Pattern 3: Connection, GATT Discovery, and MTU Negotiation The full connection lifecycle: connect -> request MTU -> discover GATT -> operate. ```typescript import { Platform } from "react-native"; import { type Device, type Subscription } from "react-native-ble-plx"; const DESIRED_MTU = 512; const CONNECTION_TIMEOUT_MS = 10000; const BLE_ATT_HEADER_BYTES = 3; async function connectToDevice( manager: BleManager, deviceId: string, ): Promise<{ device: Device; mtu: number }> { // Connect with options const device = await manager.connectToDevice(deviceId, { requestMTU: Platform.OS === "android" ? DESIRED_MTU : undefined, timeout: CONNECTION_TIMEOUT_MS, // autoConnect: false -- default. true = connect when device becomes available (slower) }); // CRITICAL: Discover GATT structure before any operations await device.discoverAllServicesAndCharacteristics(); // Calculate usable payload size const usableBytes = device.mtu - BLE_ATT_HEADER_BYTES; return { device, mtu: usableBytes }; } ``` **Why good:** MTU requested only on Android (iOS auto-negotiates), connection timeout prevents hanging, GATT discovered before operations, usable bytes calculated with ATT header subtracted ```typescript // Bad: reading immediately after connect const device = await manager.connectToDevice(deviceId); const char = await device.readCharacteristicForService(svc, chr); // Throws: services not discovered ``` **Why bad:** GATT structure not discovered, read/write/monitor operations fail because the service/characteristic tree is not cached locally --- ## Pattern 4: Reading and Writing Characteristics All BLE values are Base64-encoded. Use a library like `base-64` for encoding/decoding. ```typescript import { decode as atob, encode as btoa } from "base-64"; import type { Characteristic } from "react-native-ble-plx"; const DEVICE_INFO_SERVICE = "0000180a-0000-1000-8000-00805f9b34fb"; const FIRMWARE_REV_CHAR = "00002a26-0000-1000-8000-00805f9b34fb"; const CONTROL_POINT_CHAR = "00002a55-0000-1000-8000-00805f9b34fb"; // Read a characteristic async function readFirmwareVersion(device: Device): Promise<string> { const characteristic = await device.readCharacteristicForService( DEVICE_INFO_SERVICE, FIRMWARE_REV_CHAR, ); return atob(characteristic.value ?? ""); } // Write with response (acknowledged -- reliable) async function sendCommand(device: Device, command: string): Promise<void> { const base64Value = btoa(command); await device.writeCharacteristicWithResponseForService( DEVICE_INFO_SERVICE, CONTROL_POINT_CHAR, base64Value, ); } // Write without response (fire-and-forget -- faster) async function sendFastCommand(device: Device, command: string): Promise<void> { const base64Value = btoa(command); await device.writeCharacteristicWithoutResponseForService( DEVICE_INFO_SERVICE, CONTROL_POINT_CHAR, base64Value, ); } ``` **Why good:** Base64 encode/decode explicit, named constants for all UUIDs, write-with-response for reliable delivery, write-without-response option for speed, value null-coalesced before decode --- ## Pattern 5: Characteristic Monitoring (Notifications/Indications) Subscribe to real-time value changes from a characteristic. Returns a Subscription that MUST be removed. ```typescript import { useEffect, useRef, useState, useCallback } from "react"; import { decode as atob } from "base-64"; import type { Device, Subscription } from "react-native-ble-plx"; const HEART_RATE_SERVICE = "0000180d-0000-1000-8000-00805f9b34fb"; const HEART_RATE_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb"; export function useHeartRateMonitor(device: Device | null) { const [heartRate, setHeartRate] = useState<number | null>(null); const subscriptionRef = useRef<Subscription | null>(null); useEffect(() => { if (!device) return; subscriptionRef.current = device.monitorCharacteristicForService( HEART_RATE_SERVICE, HEART_RATE_MEASUREMENT, (error, characteristic) => { if (error) { console.error("Monitor error:", error.message); return; } if (!characteristic?.value) return; // Heart rate measurement format: first byte is flags, second byte is HR value const raw = atob(characteristic.value); const heartRateValue = raw.charCodeAt(1); setHeartRate(heartRateValue); }, ); // CRITICAL: Remove subscription on cleanup return () => { subscriptionRef.current?.remove(); subscriptionRef.current = null; }; }, [device]); return heartRate; } ``` **Why good:** subscription stored in ref and removed in cleanup, null checks on device and characteristic, error handled in callback, useEffect cleanup prevents leaked listeners ```typescript // Bad: no cleanup device.monitorCharacteristicForService(svc, chr, (err, char) => { setHeartRate(parseHR(char)); }); // Subscription leaked -- fires after unmount, causes crash ``` **Why bad:** subscription not stored or removed, callback fires after component unmount causing "setState on unmounted component" crash --- ## Pattern 6: Disconnect Monitoring and Reconnection Monitor for unexpected disconnects and implement reconnection with backoff. ```typescript import type { BleManager, Device, Subscription } from "react-native-ble-plx"; const MAX_RECONNECT_ATTEMPTS = 5; const BASE_DELAY_MS = 1000; const MAX_DELAY_MS = 30000; const BACKOFF_MULTIPLIER = 2; async function connectWithReconnection( manager: BleManager, deviceId: string, onConnected: (device: Device) => void, onDisconnected: () => void, ): Promise<() => void> { let disconnectSubscription: Subscription | null = null; let isCleanedUp = false; let reconnectAttempts = 0; async function connect() { try { const device = await manager.connectToDevice(deviceId); await device.discoverAllServicesAndCharacteristics(); reconnectAttempts = 0; // Reset on successful connection onConnected(device); // Monitor for disconnects -- fires once per registration disconnectSubscription = device.onDisconnected((error) => { if (isCleanedUp) return; onDisconnected(); attemptReconnect(); }); } catch (error) { attemptReconnect(); } } async function attemptReconnect() { if (isCleanedUp || reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return; reconnectAttempts++; const delay = Math.min( BASE_DELAY_MS * Math.pow(BACKOFF_MULTIPLIER, reconnectAttempts - 1), MAX_DELAY_MS, ); await new Promise((resolve) => setTimeout(resolve, delay)); if (!isCleanedUp) { await connect(); } } await connect(); // Return cleanup function return () => { isCleanedUp = true; disconnectSubscription?.remove(); manager.cancelDeviceConnection(deviceId).catch(() => { // Ignore disconnect errors during cleanup }); }; } ``` **Why good:** exponential backoff prevents aggressive reconnection, max attempts prevents infinite loops, cleanup function cancels connection and removes listeners, `onDisconnected` fires once per registration (re-registered after each reconnect), named constants for all timing values --- ## Pattern 7: Listing Services and Characteristics After GATT discovery, enumerate available services and their characteristics. ```typescript import type { Device, Service, Characteristic } from "react-native-ble-plx"; interface GattMap { services: Array<{ uuid: string; characteristics: Array<{ uuid: string; isReadable: boolean; isWritableWithResponse: boolean; isWritableWithoutResponse: boolean; isNotifiable: boolean; isIndicatable: boolean; }>; }>; } async function discoverGattStructure(device: Device): Promise<GattMap> { const services = await device.services(); const serviceEntries = await Promise.all( services.map(async (service) => { const characteristics = await service.characteristics(); return { uuid: service.uuid, characteristics: characteristics.map((char) => ({ uuid: char.uuid, isReadable: char.isReadable, isWritableWithResponse: char.isWritableWithResponse, isWritableWithoutResponse: char.isWritableWithoutResponse, isNotifiable: char.isNotifiable, isIndicatable: char.isIndicatable, })), }; }), ); return { services: serviceEntries }; } ``` **Why good:** enumerates all services and characteristics with capability flags, useful for debugging unknown peripherals, Promise.all parallelizes service enumeration **Key:** Always check `isNotifiable` / `isIndicatable` before calling `monitorCharacteristicForService` -- not all characteristics support notifications. -
nfc.md 11.4 KB
# BLE & NFC - NFC Patterns > NFC NDEF reading/writing, technology types, platform differences, and low-level tag access. See [core.md](core.md) for BLE patterns. See [SKILL.md](../SKILL.md) for red flags and decision guidance. **Prerequisites:** react-native-nfc-manager v3.14+ --- ## Pattern 1: NFC Initialization and Capability Check Initialize NfcManager once at app start. Check if NFC is supported and enabled. ```typescript import NfcManager from "react-native-nfc-manager"; async function initNfc(): Promise<boolean> { const isSupported = await NfcManager.isSupported(); if (!isSupported) { return false; } await NfcManager.start(); return true; } // Check if NFC is currently enabled (user may have disabled it in settings) async function isNfcEnabled(): Promise<boolean> { try { return await NfcManager.isEnabled(); } catch { return false; } } ``` **Why good:** support checked before start, start() called once, isEnabled() checks runtime state (user may toggle NFC in settings) **Platform setup:** - **iOS:** Add `NFCReaderUsageDescription` to Info.plist, enable "Near Field Communication Tag Reading" capability in Xcode - **Android:** Add `<uses-permission android:name="android.permission.NFC" />` to AndroidManifest.xml - **Android 12+:** Set `compileSdkVersion` to 31+ (PendingIntent mutability requirement) --- ## Pattern 2: Reading NDEF Tags Request NDEF technology, read the tag, parse records. Always cancel in finally. ```typescript import NfcManager, { NfcTech, Ndef } from "react-native-nfc-manager"; interface NdefRecord { tnf: number; // Type Name Format type: number[]; id: number[]; payload: number[]; } interface ParsedTag { id: string | null; records: Array<{ type: string; value: string; }>; } async function readNdefTag(): Promise<ParsedTag | null> { try { await NfcManager.requestTechnology(NfcTech.Ndef); const tag = await NfcManager.getTag(); if (!tag?.ndefMessage || tag.ndefMessage.length === 0) { return { id: tag?.id ?? null, records: [] }; } const records = tag.ndefMessage.map((record: NdefRecord) => { const type = String.fromCharCode(...record.type); // Decode based on record type if (type === "U") { // URI record -- first payload byte is URI prefix code return { type: "uri", value: Ndef.uri.decodePayload( record.payload as unknown as Uint8Array, ), }; } if (type === "T") { // Text record -- first bytes encode language return { type: "text", value: Ndef.text.decodePayload( record.payload as unknown as Uint8Array, ), }; } // Raw payload fallback return { type: "unknown", value: String.fromCharCode(...record.payload), }; }); return { id: tag.id ?? null, records }; } catch (error) { // NfcError: user cancelled, tag removed too soon, or technology unavailable return null; } finally { // CRITICAL: Always release the NFC session await NfcManager.cancelTechnologyRequest(); } } ``` **Why good:** try/finally ensures cancelTechnologyRequest is always called, record type checked for proper decoding (URI vs text vs raw), tag ID extracted, null checks on tag and ndefMessage ```typescript // Bad: no finally, no error handling async function readTag() { await NfcManager.requestTechnology(NfcTech.Ndef); const tag = await NfcManager.getTag(); return tag; // Technology never released if error thrown above } ``` **Why bad:** if getTag() throws (tag removed too soon), technology is never released, blocking all subsequent NFC operations until app restart --- ## Pattern 3: Writing NDEF Tags Write URL records, text records, or multi-record messages. ```typescript import NfcManager, { NfcTech, Ndef } from "react-native-nfc-manager"; // Write a single URI record async function writeUrlTag(url: string): Promise<boolean> { try { await NfcManager.requestTechnology(NfcTech.Ndef); const bytes = Ndef.encodeMessage([Ndef.uriRecord(url)]); await NfcManager.ndefHandler.writeNdefMessage(bytes); return true; } catch { return false; } finally { await NfcManager.cancelTechnologyRequest(); } } // Write a single text record async function writeTextTag(text: string): Promise<boolean> { try { await NfcManager.requestTechnology(NfcTech.Ndef); const bytes = Ndef.encodeMessage([Ndef.textRecord(text)]); await NfcManager.ndefHandler.writeNdefMessage(bytes); return true; } catch { return false; } finally { await NfcManager.cancelTechnologyRequest(); } } // Write multiple records in one NDEF message async function writeMultiRecordTag( url: string, description: string, ): Promise<boolean> { try { await NfcManager.requestTechnology(NfcTech.Ndef); const bytes = Ndef.encodeMessage([ Ndef.uriRecord(url), Ndef.textRecord(description), ]); await NfcManager.ndefHandler.writeNdefMessage(bytes); return true; } catch { return false; } finally { await NfcManager.cancelTechnologyRequest(); } } ``` **Why good:** Ndef utilities used for proper record encoding, try/finally in every function, multi-record message shows composability, boolean return signals success/failure --- ## Pattern 4: NFC Technology Types and Platform Availability Different NFC technologies have different platform support. Always check platform before requesting a technology. ```typescript import { Platform } from "react-native"; import NfcManager, { NfcTech } from "react-native-nfc-manager"; // Technology availability matrix // | Technology | Android | iOS | // |---------------------|---------|-----| // | NfcTech.Ndef | Y | Y | // | NfcTech.NfcA | Y | Y | // | NfcTech.IsoDep | Y | Y | // | NfcTech.NfcB | Y | N | // | NfcTech.NfcF | Y | N | // | NfcTech.NfcV | Y | N | // | NfcTech.MifareClassic | Y | N | // | NfcTech.MifareUltralight | Y | N | // | NfcTech.MifareIOS | N | Y | // | NfcTech.Iso15693IOS | N | Y | // | NfcTech.FelicaIOS | N | Y | // Safe technology request with platform check async function requestTechnologySafe(tech: NfcTech): Promise<boolean> { const androidOnly: NfcTech[] = [ NfcTech.NfcB, NfcTech.NfcF, NfcTech.NfcV, NfcTech.MifareClassic, NfcTech.MifareUltralight, ]; const iosOnly: NfcTech[] = [ NfcTech.MifareIOS, NfcTech.Iso15693IOS, NfcTech.FelicaIOS, ]; if (Platform.OS === "ios" && androidOnly.includes(tech)) { return false; // Not available on iOS } if (Platform.OS === "android" && iosOnly.includes(tech)) { return false; // Not available on Android } try { await NfcManager.requestTechnology(tech); return true; } catch { return false; } } ``` **Why good:** platform-specific technologies listed explicitly, platform checked before requesting, error handled for unsupported technologies --- ## Pattern 5: Low-Level Tag Access (NfcA / IsoDep) For tags that don't use NDEF, access raw tag commands via technology-specific handlers. ```typescript import NfcManager, { NfcTech } from "react-native-nfc-manager"; // Read Mifare Ultralight pages (Android only) const PAGES_PER_READ = 4; // Mifare Ultralight reads 4 pages (16 bytes) at a time async function readMifareUltralight(): Promise<number[][] | null> { if (Platform.OS !== "android") return null; try { await NfcManager.requestTechnology(NfcTech.MifareUltralight); const pages: number[][] = []; const PAGE_COUNT = 16; // Total pages to read for (let i = 0; i < PAGE_COUNT; i += PAGES_PER_READ) { const pageData = await NfcManager.mifareUltralightHandlerAndroid.mifareUltralightReadPages( i, ); pages.push(pageData); } return pages; } catch { return null; } finally { await NfcManager.cancelTechnologyRequest(); } } // Send APDU command via IsoDep (smart card interaction) async function sendApduCommand(command: number[]): Promise<number[] | null> { try { await NfcManager.requestTechnology(NfcTech.IsoDep); const response = await NfcManager.isoDepHandler.transceive(command); return response; } catch { return null; } finally { await NfcManager.cancelTechnologyRequest(); } } ``` **Why good:** platform checked for Android-only technology, try/finally for cleanup, named constants for page count, handler accessed through the correct technology-specific property --- ## Pattern 6: iOS NFC Alert Message iOS shows a system NFC scanning dialog. Customize the alert message. ```typescript import { Platform } from "react-native"; import NfcManager, { NfcTech } from "react-native-nfc-manager"; async function readTagWithCustomAlert(): Promise<void> { try { if (Platform.OS === "ios") { // iOS: requestTechnology accepts an options object with alertMessage await NfcManager.requestTechnology(NfcTech.Ndef, { alertMessage: "Hold your device near the NFC tag", }); } else { // Android: scanning is silent (no system dialog) await NfcManager.requestTechnology(NfcTech.Ndef); } const tag = await NfcManager.getTag(); // Process tag... if (Platform.OS === "ios") { // iOS: dismiss the system dialog with a success message await NfcManager.setAlertMessageIOS("Tag read successfully!"); } } catch { // User cancelled or tag not found } finally { await NfcManager.cancelTechnologyRequest(); } } ``` **Why good:** platform check for iOS-specific alert, custom message improves UX, success message provides feedback before dialog dismisses, Android path stays simple --- ## Pattern 7: NFC with React Hooks Wrap NFC operations in a hook for component-level use. ```typescript import { useState, useCallback, useEffect } from "react"; import NfcManager, { NfcTech, Ndef } from "react-native-nfc-manager"; interface UseNfcReaderResult { isScanning: boolean; lastTag: string | null; error: string | null; startScan: () => Promise<void>; cancelScan: () => Promise<void>; } export function useNfcReader(): UseNfcReaderResult { const [isScanning, setIsScanning] = useState(false); const [lastTag, setLastTag] = useState<string | null>(null); const [error, setError] = useState<string | null>(null); // Initialize NFC on mount useEffect(() => { NfcManager.start().catch(() => { setError("NFC not supported"); }); }, []); const startScan = useCallback(async () => { setIsScanning(true); setError(null); try { await NfcManager.requestTechnology(NfcTech.Ndef); const tag = await NfcManager.getTag(); if (tag?.ndefMessage?.[0]) { const payload = tag.ndefMessage[0].payload; const text = Ndef.text.decodePayload(payload as unknown as Uint8Array); setLastTag(text); } else { setLastTag(null); } } catch { setError("Scan cancelled or failed"); } finally { await NfcManager.cancelTechnologyRequest(); setIsScanning(false); } }, []); const cancelScan = useCallback(async () => { await NfcManager.cancelTechnologyRequest(); setIsScanning(false); }, []); return { isScanning, lastTag, error, startScan, cancelScan }; } ``` **Why good:** hook encapsulates NFC lifecycle, isScanning tracks UI state, cleanup in finally, cancel exposed for user-initiated abort, error state for UI feedback, NfcManager.start() called once on mount
-
-
reference.md 11.9 KB
# BLE & NFC Quick Reference > API quick reference, permission matrix, and decision frameworks. See [SKILL.md](SKILL.md) for red flags and anti-patterns. --- ## BLE Connection Flow ``` 1. new BleManager() -- create once, share globally 2. onStateChange(PoweredOn) -- wait for adapter ready 3. startDeviceScan(UUIDs, opts) -- scan with UUID filter 4. stopDeviceScan() -- stop when target found or timeout 5. connectToDevice(id, opts) -- establish connection (requestMTU on Android) 6. discoverAllServicesAndCharacteristics() -- MUST call before read/write/monitor 7. read / write / monitor -- GATT operations 8. cancelConnection() -- disconnect 9. destroy() -- cleanup manager on app teardown ``` --- ## BLE Permission Matrix | Platform | API Level | Required Permissions | Notes | | ------------------------ | --------- | -------------------------------------------------- | --------------------------------------------- | | **iOS** | All | Info.plist: `NSBluetoothAlwaysUsageDescription` | Handled at build time, no runtime request | | **Android 12+** | 31+ | `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT` | Runtime request via PermissionsAndroid | | **Android 10-11** | 29-30 | `ACCESS_FINE_LOCATION` | Location required for BLE scanning | | **Android <10** | 23-28 | `ACCESS_COARSE_LOCATION` or `ACCESS_FINE_LOCATION` | Either works | | **Android (background)** | 29+ | `ACCESS_BACKGROUND_LOCATION` | Additional permission for background scanning | **Expo config plugin permissions:** ```json { "plugins": [ [ "react-native-ble-plx", { "isBackgroundEnabled": true, "modes": ["central"], "neverForLocation": true, "bluetoothAlwaysPermission": "This app uses Bluetooth to communicate with your device" } ] ] } ``` - `neverForLocation: true` -- declares scanning does not use location (Android 12+, avoids location permission) - `modes: ["peripheral", "central"]` -- iOS background modes --- ## NFC Permission Matrix | Platform | Required Setup | Notes | | ----------------- | ------------------------------------------------------------------------------ | ------------------------------------ | | **iOS** | Info.plist: `NFCReaderUsageDescription` | Required | | **iOS** | Xcode: "Near Field Communication Tag Reading" capability | Required | | **iOS (ISO7816)** | Info.plist: `com.apple.developer.nfc.readersession.iso7816.select-identifiers` | For smart card AIDs | | **Android** | Manifest: `<uses-permission android:name="android.permission.NFC" />` | Required | | **Android 12+** | `compileSdkVersion >= 31` | PendingIntent mutability requirement | --- ## BLE Adapter States | State | Meaning | Action | | -------------- | ------------------ | -------------------------------------- | | `Unknown` | Transitioning | Wait for next state | | `Resetting` | Resetting | Wait for PoweredOn | | `Unsupported` | No BLE hardware | Show error, disable BLE features | | `Unauthorized` | Permission denied | Request permission or link to Settings | | `PoweredOff` | Bluetooth disabled | Prompt user to enable Bluetooth | | `PoweredOn` | Ready | Safe to scan and connect | --- ## BLE Scan Modes (Android) | Mode | Battery Impact | Scan Interval | Use Case | | ------------------------ | -------------- | ------------------------- | ------------------------------ | | `ScanMode.LowLatency` | High | Continuous | Finding a specific device fast | | `ScanMode.Balanced` | Medium | ~5s on / ~5s off | Default, general scanning | | `ScanMode.LowPower` | Low | ~0.5s on / ~4.5s off | Background monitoring | | `ScanMode.Opportunistic` | Minimal | Piggybacks on other scans | Passive discovery | --- ## BLE MTU Reference | Platform | Default MTU | Auto-Negotiated | Max MTU | | --------------- | ----------- | ------------------------- | ----------------------------------------- | | **iOS** | 23 | Yes, up to 187 | 187 (negotiated automatically on connect) | | **Android <14** | 23 | No (must call requestMTU) | 517 | | **Android 14+** | 517 | Yes | 517 | **Usable payload = MTU - 3 bytes** (ATT protocol header) | MTU | Usable Bytes | Packets for 100 bytes | | ---------------- | ------------ | --------------------- | | 23 (default) | 20 | 5 | | 187 (iOS auto) | 184 | 1 | | 512 (negotiated) | 509 | 1 | --- ## NFC Technology Handlers | Technology | Handler Property | Platform | Methods | | -------------------------- | -------------------------------- | ------------- | ----------------------------------------------------------------------------------------- | | `NfcTech.Ndef` | `ndefHandler` | iOS + Android | `writeNdefMessage()`, `makeReadOnly()`, `getNdefStatus()` | | `NfcTech.NfcA` | `nfcAHandler` | iOS + Android | `transceive()` | | `NfcTech.IsoDep` | `isoDepHandler` | iOS + Android | `transceive()` | | `NfcTech.NfcV` | `nfcVHandler` | Android | `transceive()` | | `NfcTech.MifareClassic` | `mifareClassicHandlerAndroid` | Android | `mifareClassicAuthenticateA/B()`, `mifareClassicReadBlock()`, `mifareClassicWriteBlock()` | | `NfcTech.MifareUltralight` | `mifareUltralightHandlerAndroid` | Android | `mifareUltralightReadPages()`, `mifareUltralightWritePage()` | --- ## Ndef Utility Methods | Method | Purpose | Example | | ---------------------------------- | ------------------------------ | ------------------------------------------- | | `Ndef.uriRecord(uri)` | Create URI record | `Ndef.uriRecord("https://example.com")` | | `Ndef.textRecord(text, lang?)` | Create text record | `Ndef.textRecord("Hello", "en")` | | `Ndef.encodeMessage(records)` | Encode record array to bytes | `Ndef.encodeMessage([Ndef.uriRecord(url)])` | | `Ndef.uri.decodePayload(payload)` | Decode URI from payload bytes | Returns decoded URL string | | `Ndef.text.decodePayload(payload)` | Decode text from payload bytes | Returns decoded text string | | `Ndef.uri.encodePayload(uri)` | Encode URI to payload bytes | Returns byte array | | `Ndef.text.encodePayload(text)` | Encode text to payload bytes | Returns byte array | --- ## BLE Error Codes | Category | Error Codes | Meaning | | -------------- | ---------------------------------------------------------------------- | --------------------- | | **Adapter** | `BluetoothUnsupported`, `BluetoothUnauthorized`, `BluetoothPoweredOff` | BLE not available | | **Scanning** | `BluetoothScanStartFailed` | Scan could not start | | **Connection** | `DeviceNotFound`, `DeviceNotConnected`, `DeviceConnectionFailed` | Connection issues | | **Discovery** | `ServiceNotFound`, `CharacteristicNotFound`, `DescriptorNotFound` | GATT element missing | | **Operations** | `CharacteristicReadFailed`, `CharacteristicWriteFailed` | Read/write failure | | **Control** | `OperationCancelled`, `OperationTimedOut` | Operation interrupted | --- ## BLE Characteristic Capabilities Before operating on a characteristic, check its capability flags: | Flag | Meaning | Required For | | --------------------------- | -------------------------------------------------- | ------------------------------------------------ | | `isReadable` | Value can be read | `readCharacteristicForService()` | | `isWritableWithResponse` | Write-acknowledged supported | `writeCharacteristicWithResponseForService()` | | `isWritableWithoutResponse` | Fire-and-forget write | `writeCharacteristicWithoutResponseForService()` | | `isNotifiable` | Notifications supported | `monitorCharacteristicForService()` | | `isIndicatable` | Indications supported (acknowledged notifications) | `monitorCharacteristicForService()` | --- ## Common BLE Service UUIDs | Service | UUID | Purpose | | ------------------ | -------- | ------------------------------ | | Generic Access | `0x1800` | Device name, appearance | | Generic Attribute | `0x1801` | Service changed indication | | Device Information | `0x180A` | Manufacturer, firmware, serial | | Battery Service | `0x180F` | Battery level | | Heart Rate | `0x180D` | Heart rate measurement | | Health Thermometer | `0x1809` | Temperature measurement | | Blood Pressure | `0x1810` | Blood pressure measurement | | Current Time | `0x1805` | Current time | **Note:** Full UUIDs follow the format `0000XXXX-0000-1000-8000-00805f9b34fb` where `XXXX` is the short UUID. --- ## BLE Connection Options | Option | Type | Default | Notes | | ------------- | ------------- | ------- | ------------------------------------------------------ | | `requestMTU` | number | - | Android only, request MTU during connection | | `timeout` | number | - | Connection timeout in ms | | `autoConnect` | boolean | false | Connect when device available (slower initial connect) | | `refreshGatt` | "OnConnected" | - | Android only, refresh GATT cache on connect | --- ## Battery-Efficient BLE Checklist - [ ] Scanning with UUID filter (not `null`) to reduce radio activity - [ ] Scan timeout set (never scan indefinitely) - [ ] Scan stopped as soon as target device found - [ ] Using `ScanMode.Balanced` or `ScanMode.LowPower` on Android (not `LowLatency` unless needed) - [ ] Characteristic monitoring subscriptions removed when no longer needed - [ ] Disconnect from device when communication is complete - [ ] Background scanning uses `ScanMode.LowPower` or `Opportunistic` - [ ] Connection interval appropriate for use case (not always `High` priority) -
SKILL.md 19.4 KB
--- name: mobile-hardware-ble-nfc description: BLE scanning/connecting/GATT operations with react-native-ble-plx, NFC tag reading/writing with react-native-nfc-manager, permissions, background mode, battery-efficient patterns --- # BLE & NFC Patterns > **Quick Guide:** Use `react-native-ble-plx` for BLE (scanning, connecting, GATT read/write/monitor). Use `react-native-nfc-manager` for NFC (NDEF read/write, tag technology access). BLE values are Base64-encoded -- decode before use. NFC operations follow request-technology/operate/cancel-technology lifecycle. Always clean up: remove BLE subscriptions, call `cancelTechnologyRequest()` for NFC, and `destroy()` the BleManager. MTU defaults to 23 bytes (20 usable) -- negotiate higher on Android. iOS auto-negotiates up to 187 bytes. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST call `destroy()` on BleManager when deallocating resources -- leaking the manager causes native memory leaks and zombie listeners)** **(You MUST call `discoverAllServicesAndCharacteristics()` after connecting before any read/write/monitor operations -- GATT structure is not available until discovered)** **(You MUST call `cancelTechnologyRequest()` in a finally block after every NFC operation -- failing to release the NFC session blocks subsequent scans)** **(You MUST check BLE adapter state (`PoweredOn`) before scanning -- scanning while powered off or unauthorized throws errors silently on some devices)** **(You MUST remove all BLE subscriptions (scan listeners, characteristic monitors, disconnect listeners) on cleanup -- leaked subscriptions cause crashes after component unmount)** </critical_requirements> --- **Auto-detection:** react-native-ble-plx, BleManager, startDeviceScan, connectToDevice, monitorCharacteristicForDevice, writeCharacteristicWithResponseForDevice, readCharacteristicForDevice, requestMTUForDevice, react-native-nfc-manager, NfcManager, NfcTech, Ndef, requestTechnology, cancelTechnologyRequest, writeNdefMessage, ndefHandler, useCodeScanner BLE, BLE scanning, NFC tag, NDEF record, characteristic notification, GATT **When to use:** - Scanning for and connecting to BLE peripherals (IoT sensors, wearables, medical devices) - Reading/writing BLE GATT characteristics and monitoring notifications - Reading NDEF tags or writing NDEF records to NFC tags - Implementing background BLE reconnection with state restoration - MTU negotiation for large data transfers over BLE - Accessing low-level NFC technologies (NfcA, IsoDep, MifareUltralight) **When NOT to use:** - Classic Bluetooth audio/file transfer (different protocol, different libraries) - BLE peripheral/server mode (react-native-ble-plx is central-only) - Web-based Bluetooth (use Web Bluetooth API) - Wi-Fi Direct or peer-to-peer networking **Key patterns covered:** - BLE lifecycle: scan, connect, discover services, read/write/monitor, disconnect - Battery-efficient scanning with UUID filters and scan modes - MTU negotiation (Android explicit, iOS automatic) - Characteristic subscriptions (notifications/indications) with cleanup - BLE reconnection and disconnect monitoring - NFC NDEF read/write lifecycle with technology request/cancel - NFC technology types and platform availability (iOS vs Android) - Permission handling for both BLE and NFC **Detailed Resources:** - [examples/core.md](examples/core.md) - BLE scanning, connecting, GATT operations, disconnect handling - [examples/nfc.md](examples/nfc.md) - NFC NDEF reading/writing, technology types, platform differences - [reference.md](reference.md) - API quick reference, permission matrix, decision frameworks --- <philosophy> ## Philosophy BLE and NFC are hardware communication protocols with fundamentally different interaction models: **BLE** is connection-oriented and long-lived. You scan for devices, establish a persistent connection, discover the GATT service/characteristic tree, then read/write/subscribe to characteristics over time. Connections can last minutes to hours. The main challenges are connection lifecycle management, reconnection, and battery-efficient scanning. **NFC** is session-oriented and brief. You request a technology, tap a tag, perform one operation (read or write), and release the technology. Sessions last seconds. The main challenges are platform differences (iOS vs Android technology support) and ensuring proper cleanup. **Core principles:** 1. **Lifecycle-driven** -- BLE connections have a strict flow: scan -> connect -> discover -> operate -> disconnect. Skipping steps causes silent failures. 2. **Subscription-based** -- BLE characteristic monitoring returns Subscription objects that MUST be removed on cleanup. NFC technology requests MUST be canceled in finally blocks. 3. **Base64-encoded** -- All BLE characteristic values are Base64-encoded strings. Decode before use, encode before write. 4. **Platform-aware** -- BLE permissions differ significantly between Android versions. NFC technology support varies between iOS and Android. Always check platform capabilities. **When to use BLE:** - Continuous communication with a peripheral (sensor readings, device control) - Background monitoring (health devices, beacons) - Large data transfers requiring MTU negotiation **When to use NFC:** - One-tap interactions (read a tag, write a tag, verify identity) - Quick data exchange without pairing - Tag provisioning or configuration **When NOT to use either:** - High-bandwidth streaming (use Wi-Fi or classic Bluetooth) - Cross-platform web apps (use Web Bluetooth / Web NFC) </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: BLE Manager Initialization and State Create one BleManager instance for the app lifetime. Check adapter state before operations. ```typescript import { BleManager, State } from "react-native-ble-plx"; const manager = new BleManager(); // Wait for Bluetooth to be ready manager.onStateChange((state) => { if (state === State.PoweredOn) { // Safe to scan } }, true); // true = emit current state immediately // Cleanup on app teardown manager.destroy(); ``` **Why good:** single manager instance, state checked before operations, destroy called on cleanup, `true` flag emits current state immediately so you don't miss the initial PoweredOn See [examples/core.md](examples/core.md) for full initialization with background mode support and state restoration. --- ### Pattern 2: BLE Scanning with Filters Filter scans by service UUIDs for battery efficiency. Stop scanning as soon as you find the target device. ```typescript const HEART_RATE_SERVICE_UUID = "0000180d-0000-1000-8000-00805f9b34fb"; const SCAN_TIMEOUT_MS = 10000; manager.startDeviceScan( [HEART_RATE_SERVICE_UUID], // Filter by service UUID -- null scans ALL devices { allowDuplicates: false }, (error, device) => { if (error) { /* handle */ return; } if (device?.name === "MyDevice") { manager.stopDeviceScan(); // Connect to device } }, ); // Always set a timeout to stop scanning setTimeout(() => manager.stopDeviceScan(), SCAN_TIMEOUT_MS); ``` **Why good:** UUID filter reduces battery drain, scan timeout prevents indefinite scanning, duplicates disabled to reduce callback noise, scan stopped once target found See [examples/core.md](examples/core.md) for complete scanning with Android scan modes and permission handling. --- ### Pattern 3: BLE Connection and GATT Discovery Connect, discover services/characteristics, then operate. Always monitor for disconnects. ```typescript const device = await manager.connectToDevice(deviceId, { requestMTU: 512, // Android only -- request larger MTU during connection }); // MUST discover before read/write/monitor await device.discoverAllServicesAndCharacteristics(); // Monitor for unexpected disconnects const disconnectSubscription = device.onDisconnected( (error, disconnectedDevice) => { // Handle reconnection logic }, ); // Cleanup disconnectSubscription.remove(); await device.cancelConnection(); ``` **Why good:** MTU requested during connection (Android), GATT discovered before operations, disconnect monitored for reconnection, subscription removed on cleanup See [examples/core.md](examples/core.md) for full connection flow with retry logic and error handling. --- ### Pattern 4: Reading and Writing BLE Characteristics All values are Base64-encoded. Decode after read, encode before write. ```typescript import { decode as atob, encode as btoa } from "base-64"; const SERVICE_UUID = "0000180d-0000-1000-8000-00805f9b34fb"; const CHAR_UUID = "00002a37-0000-1000-8000-00805f9b34fb"; // Read const characteristic = await device.readCharacteristicForService( SERVICE_UUID, CHAR_UUID, ); const decodedValue = atob(characteristic.value ?? ""); // Write with response (acknowledged) const base64Value = btoa("command-data"); await device.writeCharacteristicWithResponseForService( SERVICE_UUID, CHAR_UUID, base64Value, ); // Write without response (faster, no acknowledgment) await device.writeCharacteristicWithoutResponseForService( SERVICE_UUID, CHAR_UUID, base64Value, ); ``` **Why good:** named constants for UUIDs, Base64 decode/encode explicit, write-with-response used for reliable delivery, write-without-response available for speed See [examples/core.md](examples/core.md) for complete read/write patterns with error handling. --- ### Pattern 5: Characteristic Monitoring (Notifications) Subscribe to characteristic value changes. Returns a Subscription that MUST be removed. ```typescript const subscription = device.monitorCharacteristicForService( SERVICE_UUID, CHAR_UUID, (error, characteristic) => { if (error) { /* handle */ return; } const value = atob(characteristic?.value ?? ""); // Process incoming notification }, ); // CRITICAL: Remove on cleanup subscription.remove(); ``` **Why good:** subscription stored for cleanup, error handled in callback, value decoded from Base64 **Gotcha:** Check `characteristic.isNotifiable` or `characteristic.isIndicatable` before monitoring -- not all characteristics support notifications. See [examples/core.md](examples/core.md) for monitoring with React hooks and cleanup patterns. --- ### Pattern 6: MTU Negotiation Larger MTU = fewer packets for big payloads. iOS negotiates automatically (up to 187 bytes). Android requires explicit request. ```typescript import { Platform } from "react-native"; const DESIRED_MTU = 512; const BLE_HEADER_BYTES = 3; // Request MTU after connection (Android only -- iOS auto-negotiates) if (Platform.OS === "android") { const updatedDevice = await device.requestMTU(DESIRED_MTU); const usableBytes = updatedDevice.mtu - BLE_HEADER_BYTES; // usableBytes is the max payload per packet } ``` **Why good:** platform check avoids unnecessary call on iOS, named constants for MTU and header size, usable bytes calculated correctly (MTU minus 3-byte ATT header) See [reference.md](reference.md) for MTU size recommendations by use case. --- ### Pattern 7: NFC NDEF Read Request NDEF technology, read the tag, clean up in finally block. ```typescript import NfcManager, { NfcTech } from "react-native-nfc-manager"; // Initialize once at app start await NfcManager.start(); async function readNdefTag(): Promise<string | null> { try { await NfcManager.requestTechnology(NfcTech.Ndef); const tag = await NfcManager.getTag(); if (tag?.ndefMessage && tag.ndefMessage.length > 0) { // tag.ndefMessage is an array of NDEF records // Each record has: tnf, type, id, payload (all number arrays) return String.fromCharCode(...tag.ndefMessage[0].payload); } return null; } catch (error) { // User cancelled or tag not found return null; } finally { // CRITICAL: Always release the NFC session await NfcManager.cancelTechnologyRequest(); } } ``` **Why good:** technology request and cancel in try/finally, error handled for user cancellation, start() called once at app initialization, tag null-checked before access See [examples/nfc.md](examples/nfc.md) for complete NDEF reading with record parsing. --- ### Pattern 8: NFC NDEF Write Request technology, encode the message, write, release. ```typescript import NfcManager, { NfcTech, Ndef } from "react-native-nfc-manager"; async function writeNdefTag(url: string): Promise<boolean> { try { await NfcManager.requestTechnology(NfcTech.Ndef); const bytes = Ndef.encodeMessage([Ndef.uriRecord(url)]); await NfcManager.ndefHandler.writeNdefMessage(bytes); return true; } catch (error) { return false; } finally { await NfcManager.cancelTechnologyRequest(); } } ``` **Why good:** Ndef utility encodes records properly, try/finally ensures cleanup, boolean return signals success/failure See [examples/nfc.md](examples/nfc.md) for text records, multi-record messages, and platform-specific handling. --- ### Pattern 9: BLE Permissions BLE permissions differ significantly across Android versions. iOS requires Info.plist entries. ```typescript import { Platform, PermissionsAndroid } from "react-native"; async function requestBlePermissions(): Promise<boolean> { if (Platform.OS === "android") { const apiLevel = Platform.Version; if (apiLevel >= 31) { // Android 12+: BLUETOOTH_SCAN + BLUETOOTH_CONNECT const results = await PermissionsAndroid.requestMultiple([ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, ]); return Object.values(results).every((r) => r === "granted"); } // Android <12: ACCESS_FINE_LOCATION const result = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, ); return result === "granted"; } // iOS: permissions handled via Info.plist (NSBluetoothAlwaysUsageDescription) return true; } ``` **Why good:** Android API level checked for correct permission set, Android 12+ uses new Bluetooth permissions, pre-12 falls back to location permission, iOS handled via plist See [reference.md](reference.md) for the full permission matrix across platforms and Android versions. </patterns> --- <decision_framework> ## Decision Framework ### BLE vs NFC ``` What kind of hardware interaction? | +-> Continuous connection with a peripheral (sensor, wearable)? | +-> BLE -- long-lived connection with GATT operations | +-> One-tap read/write (tag, card)? | +-> NFC -- session-based, tap and go | +-> Background monitoring of nearby devices? | +-> BLE -- background scanning with state restoration | +-> Quick device provisioning (write config to tag)? +-> NFC -- write NDEF record, tap target device ``` ### BLE Write Method ``` Which write method? | +-> Data MUST arrive reliably? | +-> writeCharacteristicWithResponseForService (acknowledged, slower) | +-> Speed matters more than reliability? | +-> writeCharacteristicWithoutResponseForService (fire-and-forget, faster) | +-> Large payload (> MTU)? +-> Negotiate higher MTU first, then use write-with-response ``` ### NFC Technology Selection ``` What kind of NFC tag? | +-> Standard NDEF content (URL, text, MIME)? | +-> NfcTech.Ndef (iOS + Android) | +-> ISO 14443-3A tag (raw commands)? | +-> NfcTech.NfcA (iOS + Android) | +-> Smart card / ISO 7816 (APDU commands)? | +-> NfcTech.IsoDep (iOS + Android) | +-> Mifare Classic (Android only)? | +-> NfcTech.MifareClassic | +-> Mifare Ultralight (Android only)? +-> NfcTech.MifareUltralight ``` </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - **Skipping `discoverAllServicesAndCharacteristics()` after connecting** -- read/write/monitor calls fail silently or throw errors because GATT structure is not cached - **Not removing BLE subscriptions on cleanup** -- leaked subscriptions fire callbacks after component unmount, causing "setState on unmounted component" crashes - **Forgetting `cancelTechnologyRequest()` in NFC finally block** -- NFC session stays locked, all subsequent NFC operations fail until app restart - **Creating multiple BleManager instances** -- each instance allocates native resources. Create ONE and share it. Call `destroy()` only once on app teardown. - **Scanning without UUID filter and without timeout** -- scans all devices indefinitely, drains battery rapidly **Medium Priority Issues:** - Not checking `State.PoweredOn` before scanning -- scanning while Bluetooth is off throws errors on some devices and does nothing on others - Not handling Base64 encoding/decoding for BLE characteristic values -- raw Base64 strings are not human-readable and cannot be compared directly - Using `writeCharacteristicWithoutResponseForService` for critical data -- fire-and-forget may lose data; use write-with-response for reliability - Not requesting MTU on Android -- default 23-byte MTU means 20 usable bytes per packet, making large transfers extremely slow - Hardcoding service/characteristic UUIDs inline instead of using named constants **Gotchas & Edge Cases:** - **iOS BLE device IDs are random UUIDs**, not MAC addresses -- they can change after Bluetooth is toggled or the device restarts. Do not persist iOS device IDs for reconnection; use service UUID scanning instead. - **Android 12+ changed BLE permissions** -- `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` replaced `ACCESS_FINE_LOCATION` for BLE scanning (set `neverForLocation: true` in Expo config if scanning does not need location) - **Android 14+ defaults MTU to 517 bytes** -- `requestMTU` may be unnecessary on newer Android devices. Check `device.mtu` after connection. - **iOS auto-negotiates MTU up to 187 bytes** -- calling `requestMTU` on iOS has no effect - **`onDeviceDisconnected` fires once per registration** -- you must re-register the listener after each reconnection if you want continued disconnect monitoring - **NfcTech.NfcB, NfcF, NfcV are Android-only** -- iOS has different equivalents (Iso15693IOS, FelicaIOS). Always check platform before requesting a technology. - **NFC on iOS shows a system scan dialog** -- you cannot customize it beyond the `alertMessage`. On Android, scanning is silent. - **BLE `allowDuplicates` is iOS-only** -- Android always emits duplicates. Deduplicate in your scan callback using a Set of device IDs. - **HCE (Host Card Emulation) is NOT supported by react-native-nfc-manager** -- use a dedicated library like `react-native-hce` for card emulation - **NFC `getTag()` returns the last discovered tag** -- if no tag was tapped during the session, it returns the previous tag. Always request technology first. </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** **(You MUST call `destroy()` on BleManager when deallocating resources -- leaking the manager causes native memory leaks and zombie listeners)** **(You MUST call `discoverAllServicesAndCharacteristics()` after connecting before any read/write/monitor operations -- GATT structure is not available until discovered)** **(You MUST call `cancelTechnologyRequest()` in a finally block after every NFC operation -- failing to release the NFC session blocks subsequent scans)** **(You MUST check BLE adapter state (`PoweredOn`) before scanning -- scanning while powered off or unauthorized throws errors silently on some devices)** **(You MUST remove all BLE subscriptions (scan listeners, characteristic monitors, disconnect listeners) on cleanup -- leaked subscriptions cause crashes after component unmount)** **Failure to follow these rules will cause native crashes, memory leaks, blocked NFC sessions, and battery drain.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.