On this page
Testing Bluetooth Low Energy (BLE) functionality represents one of the most notoriously brittle frontiers in modern Android mobile software engineering. Connected smart devices—ranging from medical continuous glucose monitors (CGMs), industrial IoT telemetry gateways, and automotive digital keys to fitness wearables, point-of-sale BLE payment readers, and smart locks—depend directly on physical 2.4 GHz radio-frequency (RF) chipsets, dynamic signal RSSI attenuation, hardware link-layer bonding, and asynchronous Generic Attribute Profile (GATT) server sessions. In automated continuous integration (CI) environments, Android Studio virtual emulators provide either non-functional Bluetooth stubs or synthetic HAL emulations that cannot simulate real-world radio attenuation, multipath RF interference, peripheral advertising intervals, or genuine GATT connection latency. Furthermore, in-process testing frameworks like Espresso and UI Automator struggle fundamentally at system security boundaries; when Android's operating system triggers a system-level Bluetooth pairing dialog or requests runtime permissions (BLUETOOTH_SCAN, BLUETOOTH_CONNECT), in-process test runners throw target view not found exceptions because the security modal belongs to the out-of-process Android system UI (com.android.settings or com.android.systemui). Operating over the Model Context Protocol (MCP), ScreenSync MCP bridges Claude Code directly to physical Android devices. By pairing native Android MediaProjection display streaming (~94 ms median latency over LAN, requiring zero root and zero ADB overhead) with ADB-driven hardware control and Logcat telemetry, developers can execute comprehensive end-to-end Bluetooth Low Energy test suites directly from their terminal.
Key Takeaways
- Real RF Hardware Grounding: ScreenSync MCP streams authentic Android display frames and coordinates real hardware Bluetooth testing, bypassing the non-functional Bluetooth stubs and simulated radios of Android Studio emulators.
- Out-of-Process System Dialog Handling: While Espresso crashes on OS-level Bluetooth pairing request dialogs and PIN prompts, Claude Code inspects the system UI tree via
get_ui_hierarchyand dispatches native touch events viacontrol_tap. - Zero-ADB Viewfinder Streaming: ScreenSync MCP captures device displays via Android's native MediaProjection API at approximately 94 milliseconds median latency, preserving full ADB bandwidth for high-speed hardware control and telemetry.
- GATT Callback & Telemetry Triage: Debug notoriously opaque Android Bluetooth faults—such as the infamous
GATT_ERROR133, connection parameter renegotiation drops, and bonding timeouts—in real time usingget_logcat. - Android 12+ Runtime Permission Verification: Automate the verification of split Bluetooth permissions (
BLUETOOTH_SCAN,BLUETOOTH_CONNECT,BLUETOOTH_ADVERTISE) and location prerequisites across fresh installations. - Unified 226-Tool MCP Ecosystem: ScreenSync MCP equips terminal agents with 29 phone and desktop tools (12
control_*, 3os_*, 14 capture/telemetry) plus 197web_*browser tools to validate multi-device and hybrid IoT web portals.
The Android BLE Testing Dilemma: Virtual Emulators, Espresso, and RF Blind Spots
Android Bluetooth Low Energy testing fails on virtual emulators and in-process runners because synthetic stubs cannot replicate physical 2.4 GHz RF propagation, dynamic RSSI variance, or hardware GATT negotiation. Espresso cannot interact with out-of-process system pairing dialogs. ScreenSync MCP bridges this gap by combining physical display capture with terminal-driven ADB control.
Automated mobile software testing has achieved remarkable maturity across standard UI flows, database transactions, and RESTful network interactions. However, Bluetooth Low Energy (BLE) testing remains stubbornly resistant to traditional test automation paradigms. When engineering teams build connected device applications, they confront a harsh architectural divide between Android's simulated runtime environments and authentic physical radio hardware.
1. The Emulator Hardware Barrier
Android Studio virtual devices (AVDs) running on QEMU-based hypervisors do not possess real physical 2.4 GHz Bluetooth transceivers. Although Android emulators provide an option to emulate Bluetooth via host adapter passthrough or synthetic rootcanal software stacks, this approach breaks down in production-grade testing:
- HCI Passthrough Flaws: Host-to-guest Bluetooth Host Controller Interface (HCI) bridging often suffers from severe driver impedance mismatches between the host operating system (Windows, macOS, or Linux) and the guest Android HAL.
- No Peripheral RF Simulation: Synthetic emulator stacks cannot simulate physical advertising packets, packet collision in congested RF environments, path loss, or fluctuating Received Signal Strength Indication (RSSI) values as devices move.
- Timing & Connection Intervals: Emulators execute in virtualized CPU clocks where high-resolution connection event timing (e.g., 7.5 ms to 4000 ms connection intervals) is distorted by host CPU scheduling and hypervisor thread preemption.
2. The Espresso In-Process Sandbox Limitation
Espresso has long served as the gold standard for in-process Android UI testing. Compiled directly into the instrumentation test runner alongside the target application, Espresso synchronizes with Android's main UI looper (Looper.getMainLooper()) and idling resources. However, this tight process coupling becomes an architectural liability during Bluetooth testing:
- Process Boundary Enforcement: Android enforces strict sandboxing between applications. The moment an application requests pairing or bonding with a secure BLE peripheral, the Android OS interrupts the app by launching an out-of-process system modal dialog (
com.android.settings.bluetooth.BluetoothPairingDialogor a system overlay managed bycom.android.systemui). - Fatal Instrumentation Exceptions: Because Espresso's view matchers (
onView(withText("Pair"))) can only search within the target application's view hierarchy, Espresso cannot perceive the system dialog. The test runner stalls until timing out with a fatalNoMatchingViewException, aborting the automated test suite. - Runtime Permission Blindness: Android 12 (API level 31) split Bluetooth access into separate runtime permissions. When an app requests
android.permission.BLUETOOTH_SCANorandroid.permission.BLUETOOTH_CONNECT, the OS permission dialog is rendered bycom.google.android.permissioncontroller. In-process frameworks cannot interact with this modal, forcing teams to bypass permission testing entirely using artificialGrantPermissionRulestubs.
3. The Brittleness of Mock BLE Stubs
To circumvent emulator and framework limitations, developers frequently resort to mocking Android's Bluetooth APIs using libraries like Mockito or Robolectric. They mock BluetoothManager, BluetoothAdapter, and BluetoothGattCallback. While unit testing state machines with mock stubs provides basic code coverage, it offers zero assurance of real-world functionality:
- Callback Sequence Hallucinations: In real Android hardware,
BluetoothGattCallback.onConnectionStateChangedoes not fire immediately; it involves physical link-layer handshakes, encryption exchanges, and firmware state transitions. Mock stubs simulate idealized, deterministic callback ordering that masks real-world race conditions. - OEM Stack Divergence: Android's Bluetooth stack (Fluoride/Fluoride-GD in modern Android) behaves differently across device manufacturers. Samsung One UI, Google Pixel stock Android, and Xiaomi HyperOS apply distinct aggressive power-saving policies, background connection timeouts, and MTU negotiation limits that mock stubs never expose.
ScreenSync MCP resolves this dilemma by giving terminal-based AI agents—specifically Claude Code—direct multimodal perception and hardware control over authentic, physical Android devices.
| Testing Dimension | Android Studio Emulator | Mock BLE Stubs (Mockito / Robolectric) | Cloud Device Farms (SauceLabs / BrowserStack) | ScreenSync MCP + Claude Code (Physical Device) |
|---|---|---|---|---|
| Real RF & Peripheral Discovery | Synthetic stubs only; cannot discover real hardware BLE peripherals | Completely mocked; zero actual radio-frequency interaction | Limited; devices isolated in faraday cages cannot reach external peripherals | Direct hardware RF; scans and connects to real physical BLE beacons and sensors |
| System Pairing PIN Dialogs | Fails or hangs; cannot interact with out-of-process system modals | Bypassed; pairing logic is stubbed out in software | Slow and brittle; high network latency causes modal dismissal timeouts | Full interaction; inspects system UI tree via get_ui_hierarchy and taps PIN |
| GATT Lifecycle & Callback Timing | Distorted by hypervisor scheduling; hides real connection latency | Instantaneous and artificial; masks real-world race conditions | High WAN latency (500-1500 ms) distorts event sequence logging | Authentic hardware timing; captures exact GATT state transitions in real time |
| RSSI Fluctuation & Distance Simulation | Unsupported; static synthetic signal values | Mocked; fixed integer values | Impossible; stationary devices in server racks | Realistic RF propagation; tests live RSSI filtering, sorting, and beacon trilateration |
| HCI & Driver Crash Triage (Logcat) | Generic emulator HAL; hides OEM driver crashes and firmware faults | Impossible; no hardware driver execution occurs | Restricted logcat streaming; limited ADB shell command permissions | Unrestricted telemetry; streams raw driver logs and GATT faults via get_logcat |
| Local Execution Latency & Zero Egress | Fast locally, but provides zero hardware confidence | Instantaneous, but provides zero hardware confidence | High WAN latency (500-1500 ms) and heavy recurring bandwidth costs | Local Wi-Fi or USB reverse socket (~94 ms median); zero cloud data egress |
| Setup & Maintenance Overhead | Heavy RAM footprint; requires nested virtualization and host drivers | Complex codebase refactoring to inject mock interfaces | Expensive recurring SaaS billing and proprietary client SDKs | Free local desktop hub (Node.js 18+), free Android app, open MCP protocol |
Android Bluetooth Low Energy Architecture: ScanFilters, GATT Callbacks, and Runtime Permissions
Android BLE architecture organizes wireless communication into peripheral advertising, central scanning via BluetoothLeScanner, and bidirectional attribute exchange via BluetoothGatt. On Android 12+, apps require explicit BLUETOOTH_SCAN and BLUETOOTH_CONNECT runtime permissions. ScreenSync MCP allows Claude Code to validate scanner filters, GATT state callbacks, and permission dialogs on physical hardware.
To design comprehensive automated tests, software engineers must master the core architectural components of Android's Bluetooth Low Energy stack. Android implements the standard Bluetooth 4.0+ Generic Attribute Profile (GATT) specification, adopting a client-server paradigm where the Android device typically operates as a GATT Client (Central device) communicating with a remote GATT Server (Peripheral device).
1. Peripheral Scanning and ScanSettings
Bluetooth Low Energy devices advertise their presence by broadcasting structured 31-byte advertising packets (and optional scan response packets) containing their device name, transmit power level, manufacturer data, and Service UUIDs. In Android, central scanning is managed by BluetoothLeScanner:
val scanner = bluetoothAdapter.bluetoothLeScanner
val scanFilters = listOf(
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(UUID.fromString("0000FFE0-0000-1000-8000-00805F9B34FB")))
.build()
)
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.build()
scanner.startScan(scanFilters, scanSettings, scanCallback)
During testing, apps must balance scan aggressiveness against battery consumption. Using SCAN_MODE_LOW_LATENCY guarantees 100% duty-cycle scanning suitable for active pairing screens, whereas background scanning must use SCAN_MODE_LOW_POWER. If an app fails to enforce strict ScanFilter constraints, the Android OS throttles scanning or floods the application with hundreds of unrelated ambient advertising packets, causing UI stutter.
2. The Android 12+ Permission Overhaul
Starting in Android 12 (API level 31), Google overhauled Bluetooth permissions to decouple Bluetooth hardware access from coarse location tracking. Applications targeting Android 12 or higher must declare and request the following runtime permissions:
android.permission.BLUETOOTH_SCAN: Required to discover nearby BLE devices. If the application does not derive physical user location from Bluetooth beacons, developers can addandroid:usesPermissionFlags="neverForLocation"to the manifest declaration, exempting the app from requesting background location.android.permission.BLUETOOTH_CONNECT: Required to query device names, establish GATT connections, read and write characteristics, and manage pairing bonds.android.permission.BLUETOOTH_ADVERTISE: Required only if the mobile device acts as a peripheral broadcasting advertisements.- Legacy Support (Android 11 and earlier): Devices running Android 11 (API 30) or earlier still require
ACCESS_FINE_LOCATIONorACCESS_COARSE_LOCATIONplus system location services (GPS) toggled ON to perform BLE scanning.
Developers must verify that their applications request these permissions gracefully. When building apps with modern reactive UI toolchains, developers can apply testing Android runtime permissions with Claude Code and MCP to verify permission dialogues and edge cases, and integrate with testing Jetpack Compose apps with Claude Code and MCP for reactive state validation.
3. Production Kotlin Implementation: Jetpack Compose BLE Manager
The following production-grade Kotlin implementation demonstrates a defensive BLE scanning and GATT connection architecture in Jetpack Compose. It includes asynchronous scanning, connection state management, MTU negotiation, and characteristic streaming:
package com.example.screensync.bluetooth
import android.annotation.SuppressLint
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.Context
import android.os.ParcelUuid
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.util.UUID
// Target Peripheral Service and Characteristic UUIDs
val SERVICE_UUID: UUID = UUID.fromString("0000FFE0-0000-1000-8000-00805F9B34FB")
val DATA_CHAR_UUID: UUID = UUID.fromString("0000FFE1-0000-1000-8000-00805F9B34FB")
data class DiscoveredDevice(
val device: BluetoothDevice,
val name: String,
val address: String,
val rssi: Int
)
enum class BleConnectionState {
DISCONNECTED,
CONNECTING,
DISCOVERING_SERVICES,
CONNECTED,
FAILED
}
@SuppressLint("MissingPermission")
class BleViewModel(private val context: Context) : ViewModel() {
private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
private var bleScanner: BluetoothLeScanner? = null
private var activeGatt: BluetoothGatt? = null
private val _discoveredDevices = MutableStateFlow<List<DiscoveredDevice>>(emptyList())
val discoveredDevices: StateFlow<List<DiscoveredDevice>> = _discoveredDevices.asStateFlow()
private val _connectionState = MutableStateFlow(BleConnectionState.DISCONNECTED)
val connectionState: StateFlow<BleConnectionState> = _connectionState.asStateFlow()
private val _telemetryData = MutableStateFlow("No telemetry received")
val telemetryData: StateFlow<String> = _telemetryData.asStateFlow()
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
val name = result.scanRecord?.deviceName ?: device.name ?: "Unknown BLE Device"
val address = device.address
val rssi = result.rssi
viewModelScope.launch {
val currentList = _discoveredDevices.value.toMutableList()
val existingIndex = currentList.indexOfFirst { it.address == address }
val updatedItem = DiscoveredDevice(device, name, address, rssi)
if (existingIndex >= 0) {
currentList[existingIndex] = updatedItem
} else {
currentList.add(updatedItem)
}
_discoveredDevices.value = currentList.sortedByDescending { it.rssi }
}
}
override fun onScanFailed(errorCode: Int) {
Log.e("ScreenSyncBLE", "BLE Scan Failed with error code: $errorCode")
}
}
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
Log.i("ScreenSyncBLE", "onConnectionStateChange: status=$status, newState=$newState")
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
_connectionState.value = BleConnectionState.DISCOVERING_SERVICES
Log.i("ScreenSyncBLE", "Connected to GATT server. Requesting MTU 512...")
gatt.requestMtu(512)
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
_connectionState.value = BleConnectionState.DISCONNECTED
Log.w("ScreenSyncBLE", "Disconnected from GATT server. Closing GATT instance.")
gatt.close()
activeGatt = null
}
} else {
Log.e("ScreenSyncBLE", "GATT Error encountered: status=$status")
_connectionState.value = BleConnectionState.FAILED
gatt.close()
activeGatt = null
}
}
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
Log.i("ScreenSyncBLE", "MTU changed to $mtu with status: $status")
Log.i("ScreenSyncBLE", "Discovering services...")
gatt.discoverServices()
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
val service = gatt.getService(SERVICE_UUID)
val characteristic = service?.getCharacteristic(DATA_CHAR_UUID)
if (characteristic != null) {
_connectionState.value = BleConnectionState.CONNECTED
Log.i("ScreenSyncBLE", "Target characteristic found. Enabling notifications...")
gatt.setCharacteristicNotification(characteristic, true)
} else {
Log.e("ScreenSyncBLE", "Target characteristic $DATA_CHAR_UUID not found!")
_connectionState.value = BleConnectionState.FAILED
}
} else {
Log.e("ScreenSyncBLE", "Service discovery failed with status: $status")
_connectionState.value = BleConnectionState.FAILED
}
}
@Deprecated("Deprecated in Java")
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val payload = String(characteristic.value, Charsets.UTF_8)
Log.i("ScreenSyncBLE", "Incoming telemetry notification: $payload")
_telemetryData.value = payload
}
}
fun startScanning() {
_discoveredDevices.value = emptyList()
bleScanner = bluetoothAdapter?.bluetoothLeScanner
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
bleScanner?.startScan(null, settings, scanCallback)
Log.i("ScreenSyncBLE", "Started low-latency BLE scan.")
}
fun stopScanning() {
bleScanner?.stopScan(scanCallback)
Log.i("ScreenSyncBLE", "Stopped BLE scan.")
}
fun connectToDevice(device: BluetoothDevice) {
stopScanning()
_connectionState.value = BleConnectionState.CONNECTING
Log.i("ScreenSyncBLE", "Connecting to device: ${device.address}")
// TRANSPORT_LE is critical for Dual-Mode Bluetooth chips
activeGatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
}
fun disconnect() {
activeGatt?.disconnect()
}
}
@Composable
fun BleScannerScreen(viewModel: BleViewModel) {
val devices by viewModel.discoveredDevices.collectAsState()
val state by viewModel.connectionState.collectAsState()
val telemetry by viewModel.telemetryData.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Status: $state",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "Telemetry: $telemetry",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 4.dp)
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = { viewModel.startScanning() }) {
Text("Start Scan")
}
Button(onClick = { viewModel.stopScanning() }) {
Text("Stop Scan")
}
Button(onClick = { viewModel.disconnect() }) {
Text("Disconnect")
}
}
Divider(modifier = Modifier.padding(vertical = 8.dp))
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(devices, key = { it.address }) { item ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(text = item.name, style = MaterialTheme.typography.bodyLarge)
Text(text = item.address, style = MaterialTheme.typography.bodySmall)
Text(text = "RSSI: ${item.rssi} dBm", style = MaterialTheme.typography.bodySmall)
}
Button(onClick = { viewModel.connectToDevice(item.device) }) {
Text("Connect")
}
}
}
}
}
}
}
| BLE Lifecycle Phase | Key API / Callback | Primary Failure Mode | ScreenSync MCP Verification Tool | Success Assertion Criteria |
|---|---|---|---|---|
| Adapter Initialization | BluetoothAdapter.isEnabled() |
Bluetooth radio disabled or in airplane mode | get_logcat + ADB shell |
Command cmd bluetooth_manager enable succeeds; status is active |
| Runtime Permissions | requestPermissions(BLUETOOTH_SCAN) |
Permission denied; scan throws SecurityException |
get_ui_hierarchy + control_tap |
System dialog detected; agent taps "While using the app" |
| Peripheral Advertising Discovery | ScanCallback.onScanResult() |
Empty scan list; scanner throttled by Android OS | get_ui_hierarchy + get_latest_screenshot |
Peripheral card renders in LazyColumn with authentic MAC and RSSI |
| GATT Handshake | connectGatt(TRANSPORT_LE) |
Connection fails with status 133 or link timeout | get_logcat |
Logcat records onConnectionStateChange: status=0, newState=2 |
| MTU Negotiation | BluetoothGatt.requestMtu(512) |
Callback ignored or dropped by peripheral firmware | get_logcat |
Logcat records onMtuChanged: mtu=512, status=0 |
| Service Discovery | BluetoothGatt.discoverServices() |
Service list empty or target characteristic missing | get_ui_hierarchy + get_logcat |
UI state advances to CONNECTED; target UUID verified in logcat |
| Data Notification Streaming | onCharacteristicChanged() |
Notifications not enabled on CCCD descriptor | get_ui_hierarchy + get_logcat |
Telemetry string updates dynamically on screen without latency drift |
| Graceful Teardown | BluetoothGatt.close() |
Unclosed handles cause memory leaks and handle exhaustion | get_logcat |
Logcat confirms gatt.close() invoked; zero zombie client connections |
How ScreenSync MCP Bridges Claude Code to Physical Bluetooth Hardware
ScreenSync MCP bridges Claude Code to physical Bluetooth hardware via a decoupled three-tier architecture: local stdio process communication for agent reasoning, MediaProjection streaming for sub-100ms visual confirmation, and ADB daemon sockets for hardware control. This enables terminal agents to inspect peripheral dashboards, confirm system pairing dialogs, and monitor GATT telemetry simultaneously.
The architectural breakthrough of ScreenSync MCP lies in its decoupled multi-modal foundation. When testing complex wireless hardware interactions, an AI coding agent requires three distinct operational capabilities: cognitive reasoning over source code, visual inspection of rendered displays, and unrestricted operating system control.
1. The Multi-Tier Architecture
ScreenSync MCP divides responsibility across three high-performance tiers:
- Cognitive Agent Plane (Local stdio Process Pipe): Claude Code executes in the developer's workstation terminal. It communicates with the ScreenSync desktop hub over standard input/output (
stdio) using JSON-RPC. Because communication is local, tool calls execute in sub-millisecond timeframes, eliminating the latency and overhead of cloud orchestration layers. - Visual Verification Plane (Native Android MediaProjection): Display capture is completely decoupled from ADB. The ScreenSync MCP Android client (available free on Google Play as
com.screensync.mcp) initiates a nativeMediaProjectionsession upon user consent. It streams hardware-accelerated video frames over local Wi-Fi or USB reverse sockets directly to the desktop hub at ~94 ms median latency. Because display streaming bypasses ADB, the USB ADB channel remains 100% free for high-throughput commands and Logcat streaming. - Hardware & OS Control Plane (ADB Daemon Socket): Operating system automation, input event injection, and telemetry extraction execute through direct ADB sockets over physical USB or wireless debugging. Claude Code invokes device control tools (
control_tap,control_swipe,control_type) and queries system diagnostics (get_ui_hierarchy,get_logcat).
2. Defeating the System Dialog Barrier
The single greatest obstacle in mobile test automation has always been interacting with out-of-process system dialogs. When an Android app initiates a secure Bluetooth bonding sequence, the Android operating system displays a system-level pairing modal:
<!-- DUMP FROM get_ui_hierarchy DURING BLUETOOTH PAIRING -->
<node package="com.android.settings" class="android.widget.FrameLayout">
<node class="android.widget.TextView" text="Pair with Smart Beacon?" />
<node class="android.widget.EditText" resource-id="com.android.settings:id/pin_values" text="" />
<node class="android.widget.Button" resource-id="android:id/button2" text="Cancel" />
<node class="android.widget.Button" resource-id="android:id/button1" text="Pair" />
</node>
Because ScreenSync MCP's get_ui_hierarchy extracts the complete active Window Manager node tree—regardless of whether the top window belongs to your app or the Android OS—Claude Code perceives the system dialog immediately. It locates the PIN input field, enters the passkey via control_type, and clicks "Pair" via control_tap. What previously broke Espresso and Appium CI runs now executes autonomously in seconds.
3. The 226-Tool Ecosystem Advantage
ScreenSync MCP provides an extensive catalog of 226 MCP tools:
- 29 Phone & Desktop Tools: 12 device control tools (
control_tap,control_swipe,control_type,control_key,control_open_url,control_launch_app), 3 operating system automation tools (os_mouse_click,os_type,os_hotkey), and 14 capture and telemetry tools (get_latest_screenshot,get_ui_hierarchy,get_logcat,record_screen,get_device_status). - 197 Browser Tools: The
web_*suite operates through an authenticated Chrome/Edge browser extension. In connected IoT environments, mobile devices frequently sync telemetry to enterprise cloud dashboards. With ScreenSync MCP, Claude Code can trigger a Bluetooth data transfer on the Android phone and immediately verify that the telemetry payload arrived in an administrative web portal using browser tools likeweb_navigateandweb_table_extract. For an exhaustive breakdown of every capability, consult our complete Android MCP tool reference deep dive.
| Architectural Plane | Transport Channel | Median Latency | Role in BLE Test Automation |
|---|---|---|---|
| Cognitive Agent Plane | Local stdio JSON-RPC |
< 1 ms | Claude Code parses developer instructions, plans BLE test flows, inspects logs, and refactors Kotlin code |
| Visual Verification Plane | Native MediaProjection (Wi-Fi / USB) | ~94 ms | Streams live 60 FPS display frames; validates scanner animations, radar widgets, and RSSI graphs |
| OS & Hardware Control Plane | ADB Daemon Socket (USB / Wireless) | 10-25 ms | Injects precision touch taps, enters pairing PINs, toggles radio states, and pipes Logcat telemetry |
| Enterprise Browser Plane | Authenticated MV3 Extension | 15-40 ms | Validates cloud telemetry dashboards, backend MQTT bridges, and web pairing portals in real Chrome tabs |
Workstation Setup: Desktop Hub, Physical Device, and Claude Code Registration
Setting up ScreenSync MCP for Bluetooth testing requires starting the Node.js 18+ desktop hub, registering the stdio MCP server in Claude Code, and connecting a physical Android 7.0+ device via USB or Wi-Fi. Establishing ADB reverse port forwarding ensures uninterrupted MediaProjection streaming during wireless peripheral testing.
Engineering teams can deploy this automated testing environment on Windows, macOS, or Linux through five streamlined steps:
Step 1: Workstation Environment Prerequisites
Ensure your development machine satisfies the following baseline requirements:
- Host Workstation: Windows 10/11, macOS, or Linux running Node.js 18 or higher with Android Platform Tools (
adb) installed and present in your systemPATH. - Physical Smartphone: An authentic Android device running Android 7.0 (API level 24) or higher connected via USB cable or local Wi-Fi.
- Application Build: Debug APK installed on the phone containing your Bluetooth Low Energy implementation.
- Physical BLE Peripheral: A hardware BLE beacon, smart sensor, or secondary test phone running a BLE peripheral advertiser.
Step 2: Unpack and Launch the Desktop Hub
Download the ScreenSync MCP desktop hub distribution from screensyncmcp.tech and extract the archive to a permanent directory on your workstation. Run the platform launcher:
# On Windows (PowerShell or Command Prompt)
cd C:\tools\screensync-hub
.\start-hub.bat
# On macOS or Linux (Terminal)
cd ~/tools/screensync-hub
chmod +x start-hub.sh
./start-hub.sh
The desktop hub initializes an HTTP server on port 3000 to ingest local MediaProjection video frames and starts a standard Model Context Protocol server over stdio. The console output displays your unique authentication token (SCREEN_SYNC_TOKEN). For complete setup instructions, visit the official ScreenSync MCP setup guide.
Step 3: Register ScreenSync MCP with Claude Code
Register the desktop hub with the Claude Code CLI in your terminal:
claude mcp add screensync node "/path/to/screensync-hub/screensync-hub.js"
If you are developing inside Claude Desktop, configure claude_desktop_config.json (Settings > Developer > Edit Config):
{
"mcpServers": {
"screensync": {
"command": "node",
"args": ["/path/to/screensync-hub/screensync-hub.js"],
"env": {
"SCREEN_SYNC_TOKEN": "your_authenticated_token_here"
}
}
}
}
(Note: ScreenSync MCP integrates with local stdio MCP clients including Claude Code, Claude Desktop, Cursor, and VS Code agent mode. ChatGPT's web application requires remote HTTPS endpoints and does not support local stdio processes).
Step 4: Install the Mobile App and Authorize MediaProjection
- Download and install the free ScreenSync MCP application (package
com.screensync.mcp) from Google Play. - Launch the application. Allow local network mDNS discovery to automatically locate your workstation hub on the local Wi-Fi subnet, or scan the pairing QR code displayed in the hub console.
- Tap Start Projection and confirm Android's native MediaProjection screen capture consent prompt.
- Unlock Developer Options on your smartphone (Settings > About Phone > tap Build Number 7 times) and enable USB Debugging (or Wireless Debugging). If you prefer wireless operation, review our guide on pairing Android wireless debugging to an MCP server without USB.
- Verify ADB connectivity by running
adb devicesin your workstation terminal.
Step 5: Establish USB Reverse Port Forwarding
To ensure completely uninterrupted video streaming—even when testing airplane mode or operating in radio-shielded hardware labs—configure USB reverse socket forwarding:
adb reverse tcp:3000 tcp:3000
With reverse port forwarding active, MediaProjection video frames flow directly across the physical USB cable to port 3000 on your workstation, guaranteeing sub-100ms display latency regardless of Wi-Fi congestion.
Practical BLE QA Workflows: Scanning, System Pairing Dialogs, GATT Exploration, and Telemetry
Practical BLE testing workflows validate four critical phases: discovering advertised peripherals, confirming system-level Bluetooth pairing dialogs, reading and writing GATT characteristics, and handling connection drop recovery. Claude Code automates these workflows by combining semantic UI tree extraction with real-time MediaProjection frame analysis, touch injection, and Logcat telemetry parsing. If your peripheral streaming also involves audio output or headset disconnections, see our guide on testing Android audio and Media3 with Claude Code and MCP.
Workflow 1: Validating BLE Scan Discovery and RSSI Sorting
The initial phase of any Bluetooth testing suite is confirming that the mobile application discovers nearby advertising peripherals, extracts manufacturer data correctly, and sorts items by signal strength.
Developer Prompt:
"Claude, launch our BLE scanner app com.example.screensync.bluetooth. Verify that Bluetooth scanning initializes within 2 seconds. Tap the 'Start Scan' button. Ensure that at least one peripheral appears in the list within 5 seconds. Assert that the peripheral displays a valid MAC address and an RSSI value between -30 dBm and -95 dBm. If no devices appear, capture logcat and flag whether the scan was throttled."
Step 1: Launch and Extract Layout Hierarchy
Claude Code calls control_launch_app with package com.example.screensync.bluetooth. Once loaded, it calls get_ui_hierarchy to confirm that the activity is in the foreground and locate the "Start Scan" button:
<node text="Start Scan" resource-id="com.example.screensync.bluetooth:id/btn_scan" clickable="true" />
Step 2: Dispatch Scan Tap and Assert Discovered Peripherals
Claude Code invokes control_tap on the coordinates of the "Start Scan" button. It waits 3000 milliseconds and queries get_ui_hierarchy again. It parses the returned nodes inside the LazyColumn:
<node class="android.widget.TextView" text="Smart Sensor 01" />
<node class="android.widget.TextView" text="C4:4F:33:18:A2:9B" />
<node class="android.widget.TextView" text="RSSI: -58 dBm" />
Claude Code confirms that the device was discovered, parses the RSSI integer (-58), and asserts that it falls within expected physical proximity boundaries.
Step 3: Visual Frame Inspection via get_latest_screenshot
To ensure the Compose UI didn't glitch or render cropped card boundaries, Claude Code calls get_latest_screenshot. It inspects the rendered frame captured via MediaProjection, confirming that the radar scan indicator and peripheral cards are crisp, legible, and visually balanced.
Workflow 2: Traversing OS Bluetooth Pairing Request & PIN Dialogs
Secure peripherals require cryptographic bonding. When the app initiates a connection, the Android OS triggers a system pairing prompt.
Developer Prompt:
"Claude, connect to the peripheral with MAC C4:4F:33:18:A2:9B. When the Android system Bluetooth pairing dialog appears, enter the security PIN '123456' and confirm pairing. Verify via get_logcat that bonding succeeds and transitions to BOND_BONDED."
Step 1: Trigger Peripheral Connection
Claude Code locates the "Connect" button adjacent to C4:4F:33:18:A2:9B in the UI hierarchy and calls control_tap.
Step 2: Detect System Pairing Dialog in com.android.settings
Two seconds later, Claude Code calls get_ui_hierarchy. It detects that the top window has transitioned from the test app to the Android settings package:
<node package="com.android.settings" class="android.widget.FrameLayout">
<node class="android.widget.TextView" text="Bluetooth pairing request" />
<node class="android.widget.EditText" resource-id="com.android.settings:id/pin_values" focused="true" />
<node class="android.widget.Button" resource-id="android:id/button1" text="Pair" />
</node>
Step 3: Enter PIN and Tap "Pair"
Claude Code calls control_type with text "123456". It then locates the "Pair" button node (android:id/button1) and calls control_tap.
Step 4: Confirm Bonding in Logcat
Claude Code calls get_logcat with filter BluetoothDevice:
D BluetoothDevice: onBondingStateChanged: device C4:4F:33:18:A2:9B, bondState: BOND_BONDING
D BluetoothDevice: onBondingStateChanged: device C4:4F:33:18:A2:9B, bondState: BOND_BONDED
I ScreenSyncBLE: Peripheral successfully bonded and encrypted.
The agent verifies that the bond state transitioned to BOND_BONDED, confirming cryptographic key exchange.
Workflow 3: GATT Service Exploration and Characteristic Write/Notification
Once bonded, the application negotiates MTU size, discovers GATT services, and subscribes to characteristic notifications.
Developer Prompt:
"Claude, verify that the BLE connection establishes GATT services. Confirm that MTU increases to 512, target characteristic 0000FFE1 is discovered, and telemetry notifications begin streaming. Assert that the incoming telemetry text updates on the screen within 4 seconds."
Step 1: Assert Service Discovery and MTU via Logcat
Claude Code calls get_logcat with filter ScreenSyncBLE:
I ScreenSyncBLE: Connected to GATT server. Requesting MTU 512...
I ScreenSyncBLE: MTU changed to 512 with status: 0
I ScreenSyncBLE: Discovering services...
I ScreenSyncBLE: Target characteristic found. Enabling notifications...
Step 2: Assert Telemetry Render in UI Hierarchy
Claude Code inspects get_ui_hierarchy to assert that the UI composable updated from "No telemetry received" to the live payload string:
<node class="android.widget.TextView" text="Telemetry: TEMP=24.5C;HUM=48%;BAT=98%" />
By verifying both the low-level Logcat stream and the high-level UI render, Claude Code guarantees full end-to-end stack integrity. For a comprehensive overview of autonomous test orchestration, explore our autonomous mobile app testing Claude Code guide.
Workflow 4: Simulating Connection Drops, Radio Toggles, and Reconnection Resiliency
In real-world mobile environments, users walk out of Bluetooth range or accidentally toggle Bluetooth off. Applications must handle connection losses gracefully without crashing.
Developer Prompt:
"Claude, simulate a sudden Bluetooth radio failure while connected. Disable Bluetooth using ADB shell. Assert that the app displays a 'DISCONNECTED' banner within 2 seconds without crashing. Then re-enable Bluetooth, trigger reconnection, and verify that telemetry resumes."
Step 1: Force Disable Bluetooth Radio via ADB
Claude Code executes the system command to power off the Bluetooth adapter:
adb shell cmd bluetooth_manager disable
Step 2: Assert Disconnection UI State
Claude Code immediately calls get_ui_hierarchy and asserts that the status label transitioned to disconnected:
<node class="android.widget.TextView" text="Status: DISCONNECTED" />
It confirms via get_logcat that BluetoothGattCallback.onConnectionStateChange fired with newState=0 and that the app cleanly invoked gatt.close().
Step 3: Re-Enable Bluetooth and Assert Auto-Reconnect
Claude Code turns the Bluetooth radio back on:
adb shell cmd bluetooth_manager enable
It monitors Logcat and UI hierarchy as the application's auto-reconnect logic detects the enabled adapter, scans for the previously bonded peripheral, and re-establishes the GATT session. Developers can evaluate broader battery and CPU impacts during these reconnect cycles by following our guide on automated Android performance profiling with AI.
| Bluetooth QA Scenario | ScreenSync MCP Tools Used | Primary Assertion Mechanism | Critical Defect Caught |
|---|---|---|---|
| BLE Scan Throttling | get_ui_hierarchy, get_logcat |
Logcat ScanCallback.onScanFailed |
App exceeds Android 7.0+ scan frequency limit (5 scans per 30s) |
| System Pairing PIN Failure | get_ui_hierarchy, control_tap, control_type |
UI hierarchy of com.android.settings |
Wrong PIN entry or system pairing modal dismissal timeout |
| GATT Status 133 Collision | get_logcat |
Logcat onConnectionStateChange: status=133 |
Zombie GATT instances or rapid reconnect without gatt.close() |
| MTU Truncation Bug | get_logcat, get_ui_hierarchy |
Telemetry string length analysis | Failure to request MTU causes 20-byte payload truncation |
| CCCD Notification Drop | control_tap, get_logcat |
Logcat descriptor write status | App forgets to write ENABLE_NOTIFICATION_VALUE to descriptor 0x2902 |
| Radio Toggle Crash | ADB shell + get_ui_hierarchy |
UI crash dialog detection | Missing null check on bluetoothAdapter when radio is powered off |
Triaging GATT Error 133, Bonding Timeouts, and Logcat Telemetry
Android GATT error 133 represents an internal Bluetooth stack failure caused by exhausted connection handles, rapid unclosed reconnect attempts, or hardware MTU negotiation conflicts. Claude Code triages these failures by streaming system Logcat via get_logcat, diagnosing driver state transitions, and verifying that applications properly invoke BluetoothGatt.close() to release hardware resources.
Every Android developer who has worked with Bluetooth Low Energy has encountered the dreaded GATT Status 133 (GATT_ERROR or 0x85 in hexadecimal). It is the most common, generic, and frustrating error returned by Android's BluetoothGattCallback.onConnectionStateChange.
1. Root Causes of GATT Status 133
Because Android's Bluetooth stack wraps hundreds of distinct underlying link-layer and driver failures into error code 133, diagnosing it requires disciplined telemetry analysis:
- Exhausted Connection Handles (The 32-Client Limit): Android's native Bluetooth daemon maintains an internal array of GATT client registrations. If an app calls
device.connectGatt()repeatedly without callingBluetoothGatt.close()on old instances, the stack runs out of client handles. Once full, every subsequent connection attempt immediately fails with status 133. - Rapid Reconnection Collisions: If an app attempts to reconnect immediately upon receiving
STATE_DISCONNECTEDwithout inserting a brief backoff delay (e.g., 300 to 1000 milliseconds), the host stack attempts to open a connection while the controller is still tearing down the previous link-layer session. - Firmware Timing & Connection Interval Mismatch: If the peripheral firmware rejects the connection parameters requested by Android, or if the peripheral stops acknowledging link-layer supervision packets, the connection times out and Android reports 133.
- Dual-Mode Transport Ambiguity: On modern smartphones with dual-mode (BR/EDR + BLE) Bluetooth chips, calling
device.connectGatt(context, false, callback)without specifyingBluetoothDevice.TRANSPORT_LEcauses Android to attempt connection over classic Bluetooth first, triggering an immediate 133 rejection on BLE-only peripherals.
2. Autonomous Logcat Triage with Claude Code
When an automated test run fails to connect, Claude Code invokes get_logcat with targeted tag filters (BluetoothGatt, bt_btif, BTAgent):
// LOGCAT TELEMETRY PARSED BY CLAUDE CODE
D BluetoothGatt: connect() - device: C4:4F:33:18:A2:9B, auto: false
D BluetoothGatt: registerApp()
D BluetoothGatt: registerApp() - registering client GATT app
D bt_btif : btif_gattc_open: status=0
E bt_btif : gattc_conn_cb: if=5 st=133 reason=0x0133
D BluetoothGatt: onClientConnectionState() - status=133 clientIf=5 device=C4:4F:33:18:A2:9B
Claude Code parses the raw log lines and correlates the failure mode:
clientIf=5: The client interface registration succeeded, proving that Android's handle pool is not exhausted.reason=0x0133: The failure originated from the link layer controller timing out during the initial encryption handshake.- Remediation: Claude Code modifies the application's connection retry policy, introducing exponential backoff and ensuring that
gatt.close()is strictly invoked insideSTATE_DISCONNECTEDbefore retrying.
// DEFENSIVE GATT TEARDOWN AND RETRY PATTERN
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_DISCONNECTED || status != BluetoothGatt.GATT_SUCCESS) {
Log.w("ScreenSyncBLE", "Cleaning up GATT handle after status=$status")
gatt.disconnect()
gatt.close() // MANDATORY: Free hardware handle in Android OS
// Exponential backoff before reconnecting
coroutineScope.launch {
delay(1000)
device.connectGatt(context, false, this@BleCallback, BluetoothDevice.TRANSPORT_LE)
}
}
}
| Logcat Tag & Error | Hex Code | Underlying Hardware Cause | Claude Code Autonomous Remediation |
|---|---|---|---|
GATT_ERROR |
0x85 (133) |
Exhausted GATT client handles or link-layer timeout | Enforce gatt.close() on every disconnect; add 1000ms retry backoff |
GATT_CONN_TIMEOUT |
0x08 (8) |
Peripheral went out of range or stopped advertising | Check RSSI proximity; assert peripheral battery and power state |
GATT_AUTH_FAIL |
0x05 (5) |
Bluetooth bonding key mismatch or PIN rejected | Clear bond state via device.removeBond(); re-trigger pairing dialog |
GATT_INSUF_AUTHENTICATION |
0x0F (15) |
Read/Write attempted on encrypted characteristic prior to bonding | Defer characteristic read until BOND_BONDED state is confirmed |
GATT_NO_RESOURCES |
0x80 (128) |
Android internal heap allocation failure in Bluetooth stack | Restart Bluetooth stack via cmd bluetooth_manager restart |
GATT_INSUF_ENCRYPTION |
0x0F (15) |
Peripheral requires MITM protection not supported by app | Update pairing capability to KeyboardDisplay in OS settings |
Implementation Checklist for Automated Android Bluetooth Testing
- [ ] Ensure workstation runs Node.js 18+ with Android Platform Tools
adbconfigured in systemPATH. - [ ] Unpack the ScreenSync MCP desktop hub and execute
start-hub.bat(Windows) orstart-hub.sh(macOS/Linux). - [ ] Register ScreenSync MCP with Claude Code via
claude mcp add screensync node "/path/to/screensync-hub.js". - [ ] Install the ScreenSync MCP client (version 2.5.7+) on physical Android 7.0+ hardware from Google Play.
- [ ] Authorize native MediaProjection screen capture on the Android device for low-latency (~94 ms) local display streaming.
- [ ] Execute
adb reverse tcp:3000 tcp:3000to guarantee uninterrupted video streaming during wireless peripheral tests. - [ ] Verify that runtime permissions (
BLUETOOTH_SCAN,BLUETOOTH_CONNECT) are requested and handled viacontrol_tap. - [ ] Validate that BLE scanning initializes with
ScanSettings.SCAN_MODE_LOW_LATENCYand populates peripheral cards within 5000 ms. - [ ] Automate system-level Bluetooth pairing request modals in
com.android.settingsusingget_ui_hierarchyandcontrol_type. - [ ] Confirm that MTU negotiation (
requestMtu(512)) and GATT service discovery complete cleanly with status 0. - [ ] Triage GATT status 133 errors and connection handle leaks using real-time Logcat filtering via
get_logcat. - [ ] Test connection recovery by toggling Bluetooth radios with
cmd bluetooth_manager disableand verifying auto-reconnection.
| Target Bluetooth QA Phase | Verification Command / Tool | Assertion Mechanism | Success Criteria |
|---|---|---|---|
| MCP Hub Registration | claude mcp list |
JSON-RPC stdio handshake | ScreenSync MCP registered with 226 tools active |
| Physical Display Mirror | get_latest_screenshot |
Native MediaProjection stream | Live BLE dashboard rendered at ~94 ms median latency |
| Runtime Permissions | get_ui_hierarchy + control_tap |
Permission dialog node inspection | Agent grants BLUETOOTH_SCAN without crashing |
| Peripheral Discovery | get_ui_hierarchy |
Accessibility node tree dump | Peripheral MAC and RSSI appear in Compose LazyColumn |
| System Pairing Traversal | control_type + control_tap |
System dialog node inspection | Enters PIN '123456' and clicks 'Pair' in com.android.settings |
| GATT Service Discovery | get_logcat |
Logcat filter ScreenSyncBLE |
State transitions to GATT_SUCCESS (status 0) |
| MTU 512 Negotiation | get_logcat |
Logcat onMtuChanged |
MTU size negotiated to 512 without truncation |
| Telemetry Streaming | get_ui_hierarchy |
Node text value inspection | Live sensor string renders on screen within 4000 ms |
| Radio Failure Recovery | ADB shell bluetooth_manager |
UI status text and Logcat | Status updates to DISCONNECTED; auto-reconnects on radio restore |
Questions, answered
Why can't Android Studio emulators reliably test Bluetooth Low Energy peripherals?
Android Studio emulators lack physical 2.4 GHz Bluetooth radio hardware and rely on synthetic software stubs. These emulators cannot simulate physical RF advertising, distance-based RSSI signal attenuation, or real GATT connection intervals, making physical Android devices with ScreenSync MCP essential for testing authentic wireless behavior.
How does ScreenSync MCP handle Android system Bluetooth pairing dialogs?
While in-process frameworks like Espresso crash because system pairing dialogs belong to out-of-process system packages like com.android.settings, ScreenSync MCP uses get_ui_hierarchy to inspect the full Window Manager node tree. Claude Code reads the dialog, enters the PIN with control_type, and confirms pairing via control_tap.
What causes the infamous Android GATT error 133 during automated testing?
GATT status 133 represents a generic stack failure caused by unclosed BluetoothGatt instances exhausting Android's 32-handle limit, rapid reconnect loops without backoff, or link-layer encryption timeouts. ScreenSync MCP streams Logcat via get_logcat, allowing Claude Code to diagnose the exact root cause and refactor connection retry logic.
Does ScreenSync MCP capture screen frames over ADB or root?
No. ScreenSync MCP captures device displays using Android's native MediaProjection API with one-time user authorization. Frames stream directly to the desktop hub over local Wi-Fi or reverse USB sockets at approximately 94 milliseconds median latency, requiring zero root privileges and consuming zero ADB bandwidth.
How do you test Android 12+ Bluetooth runtime permissions with Claude Code?
Android 12 requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT runtime permissions. When an app launches on a clean device, ScreenSync MCP captures the system permission dialog via get_ui_hierarchy. Claude Code identifies the 'While using the app' button and dispatches control_tap to grant permissions dynamically.
Can Claude Code simulate BLE connection drops and reconnection recovery?
Yes. Claude Code uses ADB shell commands such as cmd bluetooth_manager disable to force-kill the Bluetooth radio on demand. It then asserts that the application displays a disconnected banner without crashing, re-enables the radio, and verifies that the app's auto-reconnect logic restores GATT telemetry.
How does ScreenSync MCP support end-to-end IoT testing with web dashboards?
ScreenSync MCP provides 29 phone and desktop tools alongside 197 web_* browser tools. In connected IoT workflows, Claude Code can trigger a Bluetooth data transfer from a physical sensor to the Android app, then switch to a real Chrome tab to verify cloud data synchronization in an administrative portal.
Can ScreenSync MCP be used with Cursor and VS Code in addition to Claude Code?
Yes. ScreenSync MCP operates as a standard Model Context Protocol server over stdio. It integrates seamlessly with any MCP-compliant developer environment that supports local process execution, including Claude Code, Claude Desktop, Cursor, and VS Code agent mode.




