尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于蓝牙RSSI测距的Android防丢器实现:从原理到代码实战

基于蓝牙RSSI测距的Android防丢器实现:从原理到代码实战 最近在开发一个智能家居项目时遇到了一个非常实际的问题手机随手一放就找不到了。虽然市面上有各种“查找手机”的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。IDEAndroid Studio (推荐最新稳定版如 Giraffe/Hedgehog)。JDKAndroid Studio 自带或 JDK 11。目标设备两部支持BLE蓝牙4.0及以上的Android设备手机A和手机B或一部手机和一部支持Android的智能手表。建议系统版本为 Android 5.0 (API 21) 以上以获得更完整的BLE API支持。2.2 项目配置在Android Studio中新建一个空项目选择Empty Views Activity模板。配置应用的build.gradle文件确保使用较新的API。本文示例基于compileSdk和targetSdk版本 34。权限声明蓝牙功能需要用户授权。在AndroidManifest.xml文件中添加以下权限!-- 用于扫描和连接BLE设备 -- uses-permission android:nameandroid.permission.BLUETOOTH / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN / !-- Android 12 (API 31) 及以上需要此权限进行蓝牙扫描 -- uses-permission android:nameandroid.permission.BLUETOOTH_SCAN / !-- Android 12 (API 31) 及以上需要此权限进行蓝牙广播 -- uses-permission android:nameandroid.permission.BLUETOOTH_ADVERTISE / !-- Android 12 (API 31) 及以上需要此权限进行蓝牙连接 -- uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / !-- 用于在Android 6.0上动态申请位置权限因为BLE扫描需要 -- uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / !-- 如果使用粗略定位可以替代FINE_LOCATION -- uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION /注意对于 Android 12BLUETOOTH_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。核心步骤获取BluetoothAdapter。检查设备是否支持BLE广播 (BluetoothAdapter.getBluetoothLeAdvertiser)。构建广播设置 (AdvertiseSettings.Builder)设置广播功率模式ADVERTISE_MODE_LOW_POWER平衡功耗和发现概率、发射功率级别ADVERTISE_TX_POWER_LOW和是否可连接。构建广播数据 (AdvertiseData.Builder)包含设备名和我们自定义的服务UUID以便扫描端过滤。启动广播。3.2 扫描端寻找并“聆听”手机扫描端需要持续扫描周围环境过滤出目标设备通过服务UUID或设备名并持续读取其RSSI值。关键类BluetoothLeScanner(用于扫描)ScanFilter,ScanSettings,ScanCallback。核心步骤获取BluetoothAdapter和BluetoothLeScanner。动态申请必要的权限Android 6.0的位置权限Android 12的蓝牙扫描权限。配置扫描设置 (ScanSettings.Builder)设置扫描模式SCAN_MODE_LOW_LATENCY高频率扫描更耗电但响应快、回调类型等。配置扫描过滤器 (ScanFilter.Builder)通过服务UUID或设备名精确过滤目标设备避免处理无关设备节省电量。注册扫描回调 (ScanCallback)在onScanResult方法中获取ScanResult对象进而得到目标设备的MAC地址和关键的RSSI值。实现“Claude”逻辑在回调中将RSSI值与预设阈值进行比较或分析其变化趋势例如连续几次扫描RSSI持续下降然后决定是否触发提醒。4. 完整实战案例双端代码实现接下来我们将把上述原理转化为可运行的代码。为了清晰我们创建两个简单的Activity分别模拟广播端和扫描端。4.1 广播端实现 (AdvertiserActivity)这个Activity负责启动和停止BLE广播。1. 布局文件 (activity_advertiser.xml)?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding20dp android:gravitycenter TextView android:idid/tv_status android:layout_widthwrap_content android:layout_heightwrap_content android:text广播状态未启动 android:textSize18sp android:layout_marginBottom30dp/ Button android:idid/btn_start_advertise android:layout_widthwrap_content android:layout_heightwrap_content android:text启动广播 android:layout_marginBottom10dp/ Button android:idid/btn_stop_advertise android:layout_widthwrap_content android:layout_heightwrap_content android:text停止广播 android:enabledfalse/ /LinearLayout2. 核心逻辑代码 (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 version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding20dp TextView android:idid/tv_target_info android:layout_widthmatch_parent android:layout_heightwrap_content android:text目标设备未找到\n当前RSSI-- dBm\n状态等待扫描 android:textSize16sp android:layout_marginBottom20dp/ Button android:idid/btn_start_scan android:layout_widthmatch_parent android:layout_heightwrap_content android:text开始扫描 android:layout_marginBottom10dp/ Button android:idid/btn_stop_scan android:layout_widthmatch_parent android:layout_heightwrap_content android:text停止扫描 android:enabledfalse/ TextView android:idid/tv_log android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 android:background#f5f5f5 android:text日志\n android:textSize12sp android:scrollbarsvertical android:padding5dp/ /LinearLayout2. 核心逻辑代码 (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 ArrayListInteger 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(); ListScanFilter 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 运行与验证准备两部Android设备一部安装应用后运行AdvertiserActivity作为被寻找的手机另一部运行ScannerActivity作为寻找设备。启动广播在设备A上点击“启动广播”。如果系统版本12会请求BLUETOOTH_ADVERTISE权限。开始扫描在设备B上点击“开始扫描”。系统会请求必要的位置和蓝牙权限根据版本不同。观察效果将两部手机放在一起扫描端日志会显示较高的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查看AdvertiseCallback的onStartSuccess/Failure回调。4. 确认两端使用的UUID字符串完全一致。可以先移除ScanFilter扫描所有设备看是否能发现目标设备名。RSSI值波动剧烈无法稳定判断1. 环境干扰Wi-Fi、微波炉、其他蓝牙设备。2. 设备握持姿势或遮挡物影响。3. 扫描模式过于激进或保守。1. 更换测试环境远离干扰源。2. 采用历史平均算法如本文示例来平滑RSSI值避免单次波动误触发。3. 调整ScanSettings的SCAN_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:usesPermissionFlagsneverForLocation属性但本例需要位置权限进行传统扫描。广播启动失败错误码 1 或 41. 设备硬件不支持。2. 广播数据超出31字节限制。3. 已有其他应用在广播。1. 检查设备是否支持BLE Peripheral角色大部分手机支持但部分旧设备或定制系统可能不支持。2. 简化AdvertiseData减少包含的信息量。3. 尝试重启蓝牙或设备。6. 最佳实践与工程建议将Demo转化为一个健壮、可用的功能还需要考虑更多工程细节。6.1 权限处理的兼容性Android权限模型随版本变化很大。务必做好版本判断。private boolean checkAndRequestPermissions() { ListString 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_LOCATION或FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE类型Android 14让用户知晓后台活动。智能扫描间隔当设备在“正常”状态时可以降低扫描频率如每5秒一次当进入“预警”状态时提高频率如每秒一次。及时释放资源在Activity的onPause或onStop中及时停止扫描在onDestroy中释放回调引用。6.4 增强用户体验语音反馈集成TextToSpeech让“Claude”用语音播报“您的手机似乎在沙发附近”或“手机信号变弱请注意”。可视化信号在UI上绘制一个信号强度条或雷达图直观显示手机的大致方向和距离变化趋势。多设备支持管理一个可信任设备列表如家人的手机、智能手表任一设备发现目标手机信号异常都可向主机报警。通过以上步骤你已经实现了一个基于蓝牙RSSI的、具备基础智能判断能力的“手机防丢器”原型。这个项目不仅是一个实用的工具更是一个深入理解Android BLE开发、近场感知和状态机设计的绝佳案例。你可以在此基础上继续探索蓝牙5.1的寻向功能AoA/AoD以实现更精准的方位判断或者将其集成到你的智能家居中枢App中让“Claude”真正成为你数字生活的贴心助手。
返回列表