src/skills/mobile-hardware-ble-nfc/SKILL.md
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
npx skillsauth add agents-inc/skills mobile-hardware-ble-nfcInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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>
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:
When NOT to use:
Key patterns covered:
Detailed Resources:
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:
When to use BLE:
When to use NFC:
When NOT to use either:
Create one BleManager instance for the app lifetime. Check adapter state before operations.
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 for full initialization with background mode support and state restoration.
Filter scans by service UUIDs for battery efficiency. Stop scanning as soon as you find the target device.
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 for complete scanning with Android scan modes and permission handling.
Connect, discover services/characteristics, then operate. Always monitor for disconnects.
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 for full connection flow with retry logic and error handling.
All values are Base64-encoded. Decode after read, encode before write.
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 for complete read/write patterns with error handling.
Subscribe to characteristic value changes. Returns a Subscription that MUST be removed.
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 for monitoring with React hooks and cleanup patterns.
Larger MTU = fewer packets for big payloads. iOS negotiates automatically (up to 187 bytes). Android requires explicit request.
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 for MTU size recommendations by use case.
Request NDEF technology, read the tag, clean up in finally block.
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 for complete NDEF reading with record parsing.
Request technology, encode the message, write, release.
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 for text records, multi-record messages, and platform-specific handling.
BLE permissions differ significantly across Android versions. iOS requires Info.plist entries.
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 for the full permission matrix across platforms and Android versions.
</patterns><decision_framework>
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
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
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>
High Priority Issues:
discoverAllServicesAndCharacteristics() after connecting -- read/write/monitor calls fail silently or throw errors because GATT structure is not cachedcancelTechnologyRequest() in NFC finally block -- NFC session stays locked, all subsequent NFC operations fail until app restartdestroy() only once on app teardown.Medium Priority Issues:
State.PoweredOn before scanning -- scanning while Bluetooth is off throws errors on some devices and does nothing on otherswriteCharacteristicWithoutResponseForService for critical data -- fire-and-forget may lose data; use write-with-response for reliabilityGotchas & Edge Cases:
BLUETOOTH_SCAN and BLUETOOTH_CONNECT replaced ACCESS_FINE_LOCATION for BLE scanning (set neverForLocation: true in Expo config if scanning does not need location)requestMTU may be unnecessary on newer Android devices. Check device.mtu after connection.requestMTU on iOS has no effectonDeviceDisconnected fires once per registration -- you must re-register the listener after each reconnection if you want continued disconnect monitoringalertMessage. On Android, scanning is silent.allowDuplicates is iOS-only -- Android always emits duplicates. Deduplicate in your scan callback using a Set of device IDs.react-native-hce for card emulationgetTag() 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>
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>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events