基于蓝牙RSSI测距的Android防丢器实现:从原理到代码实战
2026/8/10 13:25:08 网站建设 项目流程

最近在开发一个智能家居项目时,遇到了一个非常实际的问题:手机随手一放就找不到了。虽然市面上有各种“查找手机”的App,但它们大多依赖网络或声音,在复杂环境或静音模式下就失效了。这让我思考,能否利用设备上更底层、更普适的硬件——蓝牙,来实现一个更可靠的近场查找方案?恰好,结合当下热门的AI助手概念,一个“Claude用蓝牙信号帮用户找回手机”的想法便应运而生。

本文将为你完整拆解这个创意的技术实现路径。无论你是对Android蓝牙开发感兴趣的初学者,还是想为现有App增加智能感知功能的开发者,都能从中获得一套从原理到代码的闭环解决方案。我们将从蓝牙信号强度(RSSI)测距原理讲起,逐步构建一个可以监听信号、判断距离、并触发语音或振动提醒的完整Demo。你会发现,抛开复杂的网络协议,仅用设备自带的蓝牙模块,就能实现一个相当实用的“防丢器”功能。

1. 背景与核心概念:为什么是蓝牙?

在深入代码之前,我们首先要理解选择蓝牙技术作为“寻物”核心的原因,并厘清几个关键概念。

1.1 蓝牙在近场感知中的优势

与Wi-Fi、GPS等定位技术相比,蓝牙(特别是低功耗蓝牙BLE)在短距离、室内环境下的设备发现与距离感知方面具有独特优势:

  • 低功耗:BLE设计初衷就是节能,可以长时间保持广播或扫描状态,对手机电量影响小。
  • 普适性:现代智能手机、平板、笔记本几乎100%配备蓝牙模块,无需额外硬件。
  • 近场精度:通过接收信号强度指示(RSSI),可以在数米范围内对距离变化做出相对灵敏的响应,非常适合“就在身边但找不到”的场景。
  • 无需网络:蓝牙点对点通信不依赖互联网,即使在飞行模式或没有Wi-Fi的环境下也能工作。

我们的“Claude找手机”功能,本质上就是利用手机(作为外围设备/Peripheral)不断广播信号,另一个设备(如智能手表、平板或另一部手机,作为中心设备/Central)持续扫描并分析信号强度,当信号弱到一定程度(意味着手机可能被拿走或遗忘)或强到一定程度(意味着手机就在很近但看不见的地方)时,触发提醒。

1.2 核心概念解析

  • RSSI (Received Signal Strength Indicator):接收信号强度指示,单位为dBm。其值通常为负,越接近0表示信号越强。它是我们判断距离远近的核心依据。但需要注意,RSSI受环境(障碍物、干扰源)影响很大,不能直接换算为精确米数,更适合做相对距离判断和阈值告警。
  • BLE (Bluetooth Low Energy):蓝牙低功耗。我们实现此功能主要基于BLE协议,因为它功耗低,适合持续广播。传统经典蓝牙(如用于音频传输的A2DP)则不适合。
  • 广播 (Advertising) 与扫描 (Scanning)
    • 广播者 (Advertiser/Peripheral):被动角色,周期性地向外发送包含自身标识信息(如设备名、服务UUID)的数据包。本例中,“被寻找的手机”就扮演这个角色。
    • 扫描者 (Scanner/Central):主动角色,监听周围设备的广播包,并可以读取其中的信息和对应的RSSI值。本例中,“用于寻找的设备”(如手表)扮演这个角色。
  • Claude的定位:在这里,“Claude”是一个拟人化的智能助手概念。在实际代码中,它代表一套集成了蓝牙监控、RSSI分析、阈值判断以及触发交互(语音合成、振动)的逻辑模块。我们可以将其实现为一个后台服务或一个管理类。

2. 环境准备与版本说明

在开始编码前,请确保你的开发环境已就绪。本文以Android平台为例进行实现,因为Android提供了丰富且统一的蓝牙API。

2.1 开发环境

  • 操作系统:Windows 10/11, macOS 或 Linux。
  • IDE:Android Studio (推荐最新稳定版,如 Giraffe/Hedgehog)。
  • JDK:Android Studio 自带或 JDK 11+。
  • 目标设备:两部支持BLE(蓝牙4.0及以上)的Android设备(手机A和手机B),或一部手机和一部支持Android的智能手表。建议系统版本为 Android 5.0 (API 21) 以上,以获得更完整的BLE API支持。

2.2 项目配置

  1. 在Android Studio中新建一个空项目,选择Empty Views Activity模板。
  2. 配置应用的build.gradle文件,确保使用较新的API。本文示例基于compileSdktargetSdk版本 34。
  3. 权限声明:蓝牙功能需要用户授权。在AndroidManifest.xml文件中添加以下权限:
    <!-- 用于扫描和连接BLE设备 --> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <!-- Android 12 (API 31) 及以上需要此权限进行蓝牙扫描 --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <!-- Android 12 (API 31) 及以上需要此权限进行蓝牙广播 --> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> <!-- Android 12 (API 31) 及以上需要此权限进行蓝牙连接 --> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <!-- 用于在Android 6.0+上动态申请位置权限,因为BLE扫描需要 --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- 如果使用粗略定位,可以替代FINE_LOCATION --> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    注意:对于 Android 12+,BLUETOOTH_SCAN,BLUETOOTH_ADVERTISE,BLUETOOTH_CONNECT是必须声明的运行时权限,并且需要处理动态权限申请。

3. 核心原理与流程拆解

整个“Claude找手机”功能可以拆解为两个相对独立的模块:广播端(手机A,被找的设备)和扫描端(手机B/手表,用来找的设备)。两者协同工作的流程如下:

sequenceDiagram participant A as 手机A (广播端) participant B as 手机B/手表 (扫描端) participant C as Claude逻辑模块 Note over A,C: 初始化阶段 A->>A: 启动BLE广播服务 B->>B: 启动BLE扫描服务 B->>B: 动态申请定位/蓝牙权限 loop 持续扫描 B->>A: 扫描到广播包 B->>B: 解析广播包,获取RSSI B->>C: 上报设备标识与RSSI C->>C: 分析RSSI变化趋势 alt RSSI持续减弱且低于阈值 C->>B: 触发“设备远离”警报 B->>B: 发出语音/振动提醒 else RSSI突然增强且高于阈值 C->>C: 可能设备就在附近 C->>B: 触发“设备靠近”提示 B->>B: 发出轻柔提示音 end end

下面,我们分别深入两个端点的核心实现逻辑。

3.1 广播端:让手机“被看见”

广播端的目标是让自己成为一个可被发现的BLE外围设备。我们需要创建一个唯一的服务UUID,并开始广播。

关键类BluetoothLeAdvertiser(用于广播),AdvertiseSettings,AdvertiseData

核心步骤

  1. 获取BluetoothAdapter
  2. 检查设备是否支持BLE广播 (BluetoothAdapter.getBluetoothLeAdvertiser)。
  3. 构建广播设置 (AdvertiseSettings.Builder),设置广播功率模式(ADVERTISE_MODE_LOW_POWER平衡功耗和发现概率)、发射功率级别(ADVERTISE_TX_POWER_LOW)和是否可连接。
  4. 构建广播数据 (AdvertiseData.Builder),包含设备名和我们自定义的服务UUID,以便扫描端过滤。
  5. 启动广播。

3.2 扫描端:寻找并“聆听”手机

扫描端需要持续扫描周围环境,过滤出目标设备(通过服务UUID或设备名),并持续读取其RSSI值。

关键类BluetoothLeScanner(用于扫描),ScanFilter,ScanSettings,ScanCallback

核心步骤

  1. 获取BluetoothAdapterBluetoothLeScanner
  2. 动态申请必要的权限(Android 6.0+的位置权限,Android 12+的蓝牙扫描权限)。
  3. 配置扫描设置 (ScanSettings.Builder),设置扫描模式(SCAN_MODE_LOW_LATENCY高频率扫描,更耗电但响应快)、回调类型等。
  4. 配置扫描过滤器 (ScanFilter.Builder),通过服务UUID或设备名精确过滤目标设备,避免处理无关设备,节省电量。
  5. 注册扫描回调 (ScanCallback),在onScanResult方法中获取ScanResult对象,进而得到目标设备的MAC地址和关键的RSSI值。
  6. 实现“Claude”逻辑:在回调中,将RSSI值与预设阈值进行比较,或分析其变化趋势(例如,连续几次扫描RSSI持续下降),然后决定是否触发提醒。

4. 完整实战案例:双端代码实现

接下来,我们将把上述原理转化为可运行的代码。为了清晰,我们创建两个简单的Activity分别模拟广播端和扫描端。

4.1 广播端实现 (AdvertiserActivity)

这个Activity负责启动和停止BLE广播。

1. 布局文件 (activity_advertiser.xml)

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="20dp" android:gravity="center"> <TextView android:id="@+id/tv_status" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="广播状态:未启动" android:textSize="18sp" android:layout_marginBottom="30dp"/> <Button android:id="@+id/btn_start_advertise" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="启动广播" android:layout_marginBottom="10dp"/> <Button android:id="@+id/btn_stop_advertise" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="停止广播" android:enabled="false"/> </LinearLayout>

2. 核心逻辑代码 (AdvertiserActivity.java)

// 文件路径:app/src/main/java/com/example/findmyphone/AdvertiserActivity.java package com.example.findmyphone; import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.le.AdvertiseCallback; import android.bluetooth.le.AdvertiseData; import android.bluetooth.le.AdvertiseSettings; import android.bluetooth.le.BluetoothLeAdvertiser; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import java.util.UUID; @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public class AdvertiserActivity extends AppCompatActivity { private static final String TAG = "AdvertiserActivity"; // 自定义服务UUID,扫描端将用它来过滤设备 private static final UUID MY_SERVICE_UUID = UUID.fromString("0000b81d-0000-1000-8000-00805f9b34fb"); private static final int REQUEST_CODE_BLUETOOTH_PERMISSIONS = 100; private BluetoothAdapter mBluetoothAdapter; private BluetoothLeAdvertiser mBluetoothLeAdvertiser; private TextView mStatusText; private Button mStartBtn, mStopBtn; private boolean isAdvertising = false; private final AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() { @Override public void onStartSuccess(AdvertiseSettings settingsInEffect) { super.onStartSuccess(settingsInEffect); runOnUiThread(() -> { isAdvertising = true; mStatusText.setText("广播状态:正在广播"); mStartBtn.setEnabled(false); mStopBtn.setEnabled(true); Toast.makeText(AdvertiserActivity.this, "BLE广播已启动", Toast.LENGTH_SHORT).show(); }); } @Override public void onStartFailure(int errorCode) { super.onStartFailure(errorCode); runOnUiThread(() -> { isAdvertising = false; mStatusText.setText("广播状态:启动失败"); mStartBtn.setEnabled(true); mStopBtn.setEnabled(false); String errorMsg = "广播启动失败,错误码: " + errorCode; Toast.makeText(AdvertiserActivity.this, errorMsg, Toast.LENGTH_LONG).show(); }); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_advertiser); mStatusText = findViewById(R.id.tv_status); mStartBtn = findViewById(R.id.btn_start_advertise); mStopBtn = findViewById(R.id.btn_stop_advertise); // 初始化蓝牙适配器 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (mBluetoothAdapter == null) { Toast.makeText(this, "设备不支持蓝牙", Toast.LENGTH_LONG).show(); finish(); return; } if (!mBluetoothAdapter.isEnabled()) { Toast.makeText(this, "请先打开蓝牙", Toast.LENGTH_LONG).show(); // 在实际应用中,可以在这里引导用户打开蓝牙 finish(); return; } // 检查是否支持BLE广播(Android 5.0+) if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { Toast.makeText(this, "设备不支持BLE", Toast.LENGTH_LONG).show(); finish(); return; } mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser(); if (mBluetoothLeAdvertiser == null) { Toast.makeText(this, "设备不支持BLE广播", Toast.LENGTH_LONG).show(); finish(); return; } mStartBtn.setOnClickListener(v -> startAdvertising()); mStopBtn.setOnClickListener(v -> stopAdvertising()); } private void startAdvertising() { if (isAdvertising) { return; } // 动态权限检查(Android 12+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH_ADVERTISE}, REQUEST_CODE_BLUETOOTH_PERMISSIONS); return; } } AdvertiseSettings settings = new AdvertiseSettings.Builder() .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER) // 平衡功耗和发现概率 .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_LOW) // 发射功率 .setConnectable(false) // 本例不需要连接,只广播 .build(); AdvertiseData data = new AdvertiseData.Builder() .setIncludeDeviceName(true) // 包含设备名,方便识别 .addServiceUuid(new ParcelUuid(MY_SERVICE_UUID)) // 添加自定义服务UUID .build(); mBluetoothLeAdvertiser.startAdvertising(settings, data, mAdvertiseCallback); } private void stopAdvertising() { if (isAdvertising && mBluetoothLeAdvertiser != null) { mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback); runOnUiThread(() -> { isAdvertising = false; mStatusText.setText("广播状态:已停止"); mStartBtn.setEnabled(true); mStopBtn.setEnabled(false); Toast.makeText(AdvertiserActivity.this, "BLE广播已停止", Toast.LENGTH_SHORT).show(); }); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CODE_BLUETOOTH_PERMISSIONS) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { startAdvertising(); // 权限授予后重试 } else { Toast.makeText(this, "需要蓝牙广播权限才能继续", Toast.LENGTH_LONG).show(); } } } @Override protected void onDestroy() { stopAdvertising(); super.onDestroy(); } }

4.2 扫描端与Claude逻辑实现 (ScannerActivity)

这个Activity负责扫描目标设备,分析RSSI,并在达到条件时触发提醒(这里用Toast和Log模拟,实际可替换为语音、振动)。

1. 布局文件 (activity_scanner.xml)

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="20dp"> <TextView android:id="@+id/tv_target_info" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="目标设备:未找到\n当前RSSI:-- dBm\n状态:等待扫描" android:textSize="16sp" android:layout_marginBottom="20dp"/> <Button android:id="@+id/btn_start_scan" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="开始扫描" android:layout_marginBottom="10dp"/> <Button android:id="@+id/btn_stop_scan" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="停止扫描" android:enabled="false"/> <TextView android:id="@+id/tv_log" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:background="#f5f5f5" android:text="日志:\n" android:textSize="12sp" android:scrollbars="vertical" android:padding="5dp"/> </LinearLayout>

2. 核心逻辑代码 (ScannerActivity.java)

// 文件路径:app/src/main/java/com/example/findmyphone/ScannerActivity.java package com.example.findmyphone; import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import java.util.ArrayList; import java.util.List; import java.util.UUID; @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public class ScannerActivity extends AppCompatActivity { private static final String TAG = "ScannerActivity"; // 必须与广播端一致 private static final UUID TARGET_SERVICE_UUID = UUID.fromString("0000b81d-0000-1000-8000-00805f9b34fb"); private static final int REQUEST_CODE_PERMISSIONS = 200; // RSSI阈值 (dBm). 需要根据实际环境测试调整。 private static final int RSSI_THRESHOLD_FAR = -80; // 信号弱,认为设备远离 private static final int RSSI_THRESHOLD_NEAR = -50; // 信号强,认为设备很近 private static final int HISTORY_SIZE = 5; // 用于计算平均RSSI的历史记录大小 private BluetoothAdapter mBluetoothAdapter; private BluetoothLeScanner mBluetoothLeScanner; private boolean isScanning = false; private TextView mTargetInfoText, mLogText; private Button mStartBtn, mStopBtn; private Handler mHandler = new Handler(Looper.getMainLooper()); private String mTargetDeviceAddress = null; private ArrayList<Integer> mRssiHistory = new ArrayList<>(); private int mLastAlertState = 0; // 0:无警报,1:近场提示,2:远离警报 private ScanCallback mScanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { super.onScanResult(callbackType, result); // 通过服务UUID过滤目标设备 if (result.getScanRecord() != null && result.getScanRecord().getServiceUuids() != null && result.getScanRecord().getServiceUuids().contains(new ParcelUuid(TARGET_SERVICE_UUID))) { final String deviceAddress = result.getDevice().getAddress(); final int rssi = result.getRssi(); final String deviceName = result.getScanRecord().getDeviceName(); deviceName = (deviceName != null) ? deviceName : "未知设备"; // 更新UI和日志 runOnUiThread(() -> { mTargetDeviceAddress = deviceAddress; String info = String.format("目标设备:%s (%s)\n当前RSSI:%d dBm\n状态:已锁定", deviceName, deviceAddress.substring(0,8)+"...", rssi); mTargetInfoText.setText(info); appendLog(String.format("发现目标,RSSI=%d", rssi)); }); // 调用Claude逻辑核心进行分析 analyzeRssiAndAlert(rssi); } } @Override public void onScanFailed(int errorCode) { super.onScanFailed(errorCode); runOnUiThread(() -> { appendLog("扫描失败,错误码: " + errorCode); Toast.makeText(ScannerActivity.this, "扫描失败", Toast.LENGTH_SHORT).show(); stopScan(); }); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_scanner); mTargetInfoText = findViewById(R.id.tv_target_info); mLogText = findViewById(R.id.tv_log); mStartBtn = findViewById(R.id.btn_start_scan); mStopBtn = findViewById(R.id.btn_stop_scan); // 初始化蓝牙 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled() || !getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { Toast.makeText(this, "蓝牙不可用或不支持BLE", Toast.LENGTH_LONG).show(); finish(); return; } mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); if (mBluetoothLeScanner == null) { Toast.makeText(this, "无法获取BLE扫描器", Toast.LENGTH_LONG).show(); finish(); return; } mStartBtn.setOnClickListener(v -> startScan()); mStopBtn.setOnClickListener(v -> stopScan()); } private void startScan() { if (isScanning) return; // 动态权限申请(Android 12+ 和 6.0+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_PERMISSIONS); return; } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_PERMISSIONS); return; } } // 权限检查通过,开始扫描 doStartScan(); } private void doStartScan() { ScanFilter filter = new ScanFilter.Builder() .setServiceUuid(new ParcelUuid(TARGET_SERVICE_UUID)) // 关键:只扫描携带特定UUID的设备 .build(); List<ScanFilter> filters = new ArrayList<>(); filters.add(filter); ScanSettings settings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 低延迟,快速响应 .build(); mBluetoothLeScanner.startScan(filters, settings, mScanCallback); isScanning = true; runOnUiThread(() -> { mStartBtn.setEnabled(false); mStopBtn.setEnabled(true); appendLog("开始扫描目标设备..."); }); } private void stopScan() { if (isScanning && mBluetoothLeScanner != null) { mBluetoothLeScanner.stopScan(mScanCallback); isScanning = false; runOnUiThread(() -> { mStartBtn.setEnabled(true); mStopBtn.setEnabled(false); mTargetDeviceAddress = null; mRssiHistory.clear(); mLastAlertState = 0; mTargetInfoText.setText("目标设备:未找到\n当前RSSI:-- dBm\n状态:扫描停止"); appendLog("扫描已停止"); }); } } /** * Claude逻辑核心:分析RSSI并触发提醒 * @param rssi 当前接收到的信号强度 */ private void analyzeRssiAndAlert(int rssi) { // 1. 更新历史记录 mRssiHistory.add(rssi); if (mRssiHistory.size() > HISTORY_SIZE) { mRssiHistory.remove(0); } // 2. 计算近期平均RSSI,平滑波动 int avgRssi = 0; for (int val : mRssiHistory) { avgRssi += val; } avgRssi = mRssiHistory.isEmpty() ? rssi : avgRssi / mRssiHistory.size(); // 3. 根据平均RSSI判断状态 runOnUiThread(() -> { if (avgRssi > RSSI_THRESHOLD_NEAR && mLastAlertState != 1) { // 信号很强,设备很可能在很近的地方(比如在口袋里或沙发缝里) appendLog(String.format("[Claude] 信号强劲(%ddBm),设备就在附近!", avgRssi)); Toast.makeText(ScannerActivity.this, "📱 手机好像就在身边,仔细找找!", Toast.LENGTH_SHORT).show(); // 这里可以触发短振动或轻柔提示音 mLastAlertState = 1; } else if (avgRssi < RSSI_THRESHOLD_FAR && mLastAlertState != 2) { // 信号很弱,设备可能被带远或遗忘 appendLog(String.format("[Claude] 信号微弱(%ddBm),设备可能正在远离!", avgRssi)); Toast.makeText(ScannerActivity.this, "⚠️ 手机可能被拿走了或遗忘在远处!", Toast.LENGTH_LONG).show(); // 这里可以触发强烈振动、响铃或语音播报 mLastAlertState = 2; } else if (avgRssi >= RSSI_THRESHOLD_FAR && avgRssi <= RSSI_THRESHOLD_NEAR) { // 信号在正常范围内,清除警报状态 if (mLastAlertState != 0) { appendLog(String.format("[Claude] 信号恢复正常(%ddBm)。", avgRssi)); mLastAlertState = 0; } } }); } private void appendLog(final String msg) { runOnUiThread(() -> mLogText.append(msg + "\n")); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CODE_PERMISSIONS) { boolean allGranted = true; for (int result : grantResults) { if (result != PackageManager.PERMISSION_GRANTED) { allGranted = false; break; } } if (allGranted) { doStartScan(); } else { Toast.makeText(this, "需要位置和蓝牙权限才能扫描", Toast.LENGTH_LONG).show(); } } } @Override protected void onDestroy() { stopScan(); super.onDestroy(); } }

4.3 运行与验证

  1. 准备两部Android设备:一部安装应用后运行AdvertiserActivity(作为被寻找的手机),另一部运行ScannerActivity(作为寻找设备)。
  2. 启动广播:在设备A上点击“启动广播”。如果系统版本>=12,会请求BLUETOOTH_ADVERTISE权限。
  3. 开始扫描:在设备B上点击“开始扫描”。系统会请求必要的位置和蓝牙权限(根据版本不同)。
  4. 观察效果
    • 将两部手机放在一起,扫描端日志会显示较高的RSSI值(如 -45 dBm),并可能触发“设备就在附近”的Toast提示。
    • 将设备A(广播端)拿远,观察设备B(扫描端)的RSSI值逐渐变小(如 -85 dBm),当低于阈值RSSI_THRESHOLD_FAR(-80)时,会触发“设备可能正在远离”的警报。
    • 将设备A拿回,警报解除。

预期输出(Logcat和UI)

开始扫描目标设备... 发现目标,RSSI=-45 [Claude] 信号强劲(-45dBm),设备就在附近! 发现目标,RSSI=-78 [Claude] 信号恢复正常(-78dBm)。 发现目标,RSSI=-85 [Claude] 信号微弱(-85dBm),设备可能正在远离!

5. 常见问题与排查思路

在实际开发和测试中,你可能会遇到以下问题:

问题现象可能原因排查思路与解决方案
扫描不到任何设备1. 蓝牙未开启。
2. 缺少必要权限(尤其是Android 6.0+的位置权限和Android 12+的BLUETOOTH_SCAN权限)。
3. 广播端未成功启动广播。
4. 扫描过滤器(UUID)与广播端不匹配。
1. 检查两部设备的蓝牙开关。
2. 在应用设置中手动授予权限,或检查代码中动态申请逻辑是否完整。
3. 检查广播端Logcat,查看AdvertiseCallbackonStartSuccess/Failure回调。
4. 确认两端使用的UUID字符串完全一致。可以先移除ScanFilter,扫描所有设备,看是否能发现目标设备名。
RSSI值波动剧烈,无法稳定判断1. 环境干扰(Wi-Fi、微波炉、其他蓝牙设备)。
2. 设备握持姿势或遮挡物影响。
3. 扫描模式过于激进或保守。
1. 更换测试环境,远离干扰源。
2. 采用历史平均算法(如本文示例)来平滑RSSI值,避免单次波动误触发。
3. 调整ScanSettingsSCAN_MODESCAN_MODE_LOW_LATENCY响应快但波动大,SCAN_MODE_LOW_POWER更稳定但更新慢。
应用在后台扫描被系统停止Android系统对后台BLE扫描有严格限制以节省电量。1. 前台服务:如果需要持续后台扫描,必须启动一个前台服务,并显示持续的通知。
2. 扫描策略:使用ScanSettings.Builder().setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)等减少回调频率。
Android 12+ 权限申请被拒绝权限声明或请求方式不正确。1. 确保AndroidManifest.xml中正确声明了BLUETOOTH_SCAN,BLUETOOTH_ADVERTISE,BLUETOOTH_CONNECT权限。
2. 对于BLUETOOTH_SCAN,如果仅扫描而不需要获取物理位置,可以在声明时添加android:usesPermissionFlags="neverForLocation"属性,但本例需要位置权限进行传统扫描。
广播启动失败,错误码 1 或 41. 设备硬件不支持。
2. 广播数据超出31字节限制。
3. 已有其他应用在广播。
1. 检查设备是否支持BLE Peripheral角色(大部分手机支持,但部分旧设备或定制系统可能不支持)。
2. 简化AdvertiseData,减少包含的信息量。
3. 尝试重启蓝牙或设备。

6. 最佳实践与工程建议

将Demo转化为一个健壮、可用的功能,还需要考虑更多工程细节。

6.1 权限处理的兼容性

Android权限模型随版本变化很大。务必做好版本判断。

private boolean checkAndRequestPermissions() { List<String> permissionsNeeded = new ArrayList<>(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { permissionsNeeded.add(Manifest.permission.BLUETOOTH_SCAN); } // ... 其他BLUETOOTH_* 权限 } // Android 6.0 (M) 到 Android 11 需要位置权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { permissionsNeeded.add(Manifest.permission.ACCESS_FINE_LOCATION); } } if (!permissionsNeeded.isEmpty()) { ActivityCompat.requestPermissions(this, permissionsNeeded.toArray(new String[0]), REQUEST_CODE); return false; } return true; }

6.2 优化“Claude”的判断逻辑

  • 动态阈值校准:不同手机型号、不同环境下的RSSI基线不同。可以在应用启动时,让用户将两部手机放在“正常距离”(如1米),记录此时的RSSI作为基准,然后基于此基准设置偏移量阈值,而非固定绝对值。
  • 状态机管理:引入更精细的状态(如“正常”、“预警”、“警报”、“丢失”),并设置进入/退出每个状态的条件(如连续N次扫描低于阈值),避免状态频繁跳变。
  • 结合其他传感器:可以结合加速度计判断设备是否被移动,结合光感判断是否从口袋/包里取出,提供更准确的上下文。

6.3 功耗与性能优化

  • 后台服务:将扫描逻辑放在ForegroundService中,并获取FOREGROUND_SERVICE_TYPE_LOCATIONFOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE类型(Android 14+),让用户知晓后台活动。
  • 智能扫描间隔:当设备在“正常”状态时,可以降低扫描频率(如每5秒一次);当进入“预警”状态时,提高频率(如每秒一次)。
  • 及时释放资源:在Activity的onPauseonStop中及时停止扫描,在onDestroy中释放回调引用。

6.4 增强用户体验

  • 语音反馈:集成TextToSpeech,让“Claude”用语音播报“您的手机似乎在沙发附近”或“手机信号变弱,请注意”。
  • 可视化信号:在UI上绘制一个信号强度条或雷达图,直观显示手机的大致方向和距离变化趋势。
  • 多设备支持:管理一个可信任设备列表(如家人的手机、智能手表),任一设备发现目标手机信号异常都可向主机报警。

通过以上步骤,你已经实现了一个基于蓝牙RSSI的、具备基础智能判断能力的“手机防丢器”原型。这个项目不仅是一个实用的工具,更是一个深入理解Android BLE开发、近场感知和状态机设计的绝佳案例。你可以在此基础上,继续探索蓝牙5.1的寻向功能(AoA/AoD)以实现更精准的方位判断,或者将其集成到你的智能家居中枢App中,让“Claude”真正成为你数字生活的贴心助手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询