feat: 蓝牙扫描与iBeacon模块

核心功能:
- BleScanner: BLE扫描封装,SCAN_MODE_LOW_POWER低功耗
- IBeaconParser: 从ScanRecord解析UUID/Major/Minor/TxPower
- BluetoothScanManager: 状态机管理常规/特殊双模式
  - 常规模式:周期扫描→上报API→检查新任务
  - 特殊模式:RSSI采样→均值判定→任务匹配→唤醒屏幕
- 设备过滤:名称含RFstar + 功率0<p<=100

集成:
- HomeFragment: MQTT type=4更新蓝牙参数,type=5控制启停
- 解绑时停止扫描
- 电量同步给扫描管理器

新增6文件,修改2文件。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-04-29 12:06:59 +09:30
parent 866063b21c
commit e7fa7b3b1d
8 changed files with 630 additions and 2 deletions

View File

@@ -0,0 +1,57 @@
package com.xiaoqu.watch.data.bluetooth
/**
* 蓝牙扫描参数(服务端通过 MQTT type=4 下发,本地有默认值兜底)
* 对应旧版 bluetoothStore.js 的 dictParams
*/
data class BleScanParams(
/** 常规模式扫描轮数 */
val bluetoothRoutineCycle: Int = 1,
/** 常规模式每轮扫描时间(秒) */
val bluetoothRoutineCycleTime: Int = 20,
/** 常规模式采样次数 */
val bluetoothRoutineCycleTimes: Int = 1,
/** 特殊模式扫描轮数 */
val bluetoothSpecialCycle: Int = 5,
/** 特殊模式采样次数(也是信标移除阈值) */
val bluetoothSpecialCycleTimes: Int = 5,
/** RSSI 信号强度阈值(平均值 >= 此值判定为在范围内) */
val bluetoothSign: Int = 1,
/** 特殊模式→常规模式超时(秒) */
val specialToRoutineTime: Int = 3
) {
/** 常规模式扫描时长(毫秒) */
val routineScanMs: Long
get() = ((bluetoothRoutineCycle - bluetoothRoutineCycleTime).coerceAtLeast(1)) * 1000L
/** 常规模式休息时长(毫秒) */
val routineRestMs: Long
get() = bluetoothRoutineCycleTime * 1000L
/** 特殊模式扫描时长(毫秒) */
val specialScanMs: Long
get() = ((bluetoothSpecialCycle - bluetoothSpecialCycleTimes).coerceAtLeast(1)) * 1000L
/** 特殊模式休息时长(毫秒) */
val specialRestMs: Long
get() = bluetoothSpecialCycleTimes * 1000L
/** 特殊→常规超时(毫秒) */
val specialTimeoutMs: Long
get() = specialToRoutineTime * 1000L
companion object {
/** 从 MQTT JSON 解析参数,缺失字段用默认值 */
fun fromJson(json: org.json.JSONObject): BleScanParams {
return BleScanParams(
bluetoothRoutineCycle = json.optInt("bluetoothRoutineCycle", 1),
bluetoothRoutineCycleTime = json.optInt("bluetoothRoutineCycleTime", 20),
bluetoothRoutineCycleTimes = json.optInt("bluetoothRoutineCycleTimes", 1),
bluetoothSpecialCycle = json.optInt("bluetoothSpecialCycle", 5),
bluetoothSpecialCycleTimes = json.optInt("bluetoothSpecialCycleTimes", 5),
bluetoothSign = json.optInt("bluetoothSign", 1),
specialToRoutineTime = json.optInt("specialToRoutineTime", 3)
)
}
}
}

View File

@@ -0,0 +1,22 @@
package com.xiaoqu.watch.data.bluetooth
/**
* iBeacon 设备BLE 扫描结果解析后)
* 用于上报服务端和特殊模式 RSSI 追踪
*/
data class IBeaconDevice(
/** 蓝牙 MAC 地址 */
val mac: String,
/** 信号强度 (dBm, 负值, 如 -65) */
val rssi: Int,
/** iBeacon UUID */
val uuid: String,
/** iBeacon Major */
val major: Int,
/** iBeacon Minor */
val minor: Int,
/** 发射功率 (dBm) */
val txPower: Int,
/** 设备名称 */
val name: String
)

View File

@@ -0,0 +1,130 @@
package com.xiaoqu.watch.device.bluetooth
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.bluetooth.le.BluetoothLeScanner
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import com.xiaoqu.watch.data.bluetooth.IBeaconDevice
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
/**
* BLE 扫描封装(硬件抽象层)
*
* 使用 BluetoothLeScanner 扫描 BLE 设备,通过 IBeaconParser 过滤解析 iBeacon。
* 扫描模式固定使用 SCAN_MODE_LOW_POWERCLAUDE.md 功耗要求)。
*/
@Singleton
class BleScanner @Inject constructor(
@ApplicationContext private val context: Context
) {
private var bluetoothAdapter: BluetoothAdapter? = null
private var leScanner: BluetoothLeScanner? = null
private var currentCallback: ScanCallback? = null
/** 初始<E5889D><E5A78B>蓝牙适配器 */
private fun ensureAdapter(): Boolean {
if (bluetoothAdapter == null) {
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
bluetoothAdapter = manager?.adapter
}
if (leScanner == null) {
leScanner = bluetoothAdapter?.bluetoothLeScanner
}
return leScanner != null
}
/** 蓝牙是否可用(适配器存在且已开启) */
fun isAvailable(): Boolean {
ensureAdapter()
return bluetoothAdapter?.isEnabled == true && leScanner != null
}
/**
* 执行一次 BLE 扫描(挂起函数)
* @param durationMs 扫描时长(毫秒)
* @return 扫描到的 iBeacon 设备列表
*/
suspend fun scanOnce(durationMs: Long): List<IBeaconDevice> {
if (!isAvailable()) {
Timber.w("BLE扫描: 蓝牙不可用")
return emptyList()
}
return suspendCancellableCoroutine { cont ->
val devices = mutableListOf<IBeaconDevice>()
val seenMacs = mutableSetOf<String>() // MAC 去重
val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = IBeaconParser.parse(result) ?: return
// 同一次扫描内 MAC 去重,保留最新 RSSI
if (device.mac !in seenMacs) {
seenMacs.add(device.mac)
devices.add(device)
}
}
override fun onScanFailed(errorCode: Int) {
Timber.w("BLE扫描失败: errorCode=$errorCode")
stopCurrentScan()
if (cont.isActive) cont.resume(emptyList())
}
}
currentCallback = callback
// 低功耗扫描模式
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.build()
try {
leScanner?.startScan(null, settings, callback)
Timber.d("BLE扫描: 开始 (${durationMs}ms)")
} catch (e: SecurityException) {
Timber.w(e, "BLE扫描: 权限不足")
if (cont.isActive) cont.resume(emptyList())
return@suspendCancellableCoroutine
}
// <20><><EFBFBD>时后停止扫描并返回结果
cont.invokeOnCancellation { stopCurrentScan() }
// 用协程 delay 实现超时(由调用方管理)
// 这里立即返回,调用方用 withTimeout 或手动 delay + resume
// 改为:注册定时停止
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
stopCurrentScan()
if (cont.isActive) {
Timber.d("BLE扫描: 完成,发现 ${devices.size} 个 iBeacon")
cont.resume(devices)
}
}, durationMs)
}
}
/** 停止当前扫描 */
fun stopCurrentScan() {
try {
currentCallback?.let { leScanner?.stopScan(it) }
} catch (e: Exception) {
Timber.w(e, "BLE扫描: 停止异常")
}
currentCallback = null
}
/** 强制停止所有扫描(生命周期结束时调用) */
fun shutdown() {
stopCurrentScan()
leScanner = null
bluetoothAdapter = null
}
}

View File

@@ -0,0 +1,84 @@
package com.xiaoqu.watch.device.bluetooth
import android.bluetooth.le.ScanResult
import com.xiaoqu.watch.data.bluetooth.IBeaconDevice
/**
* iBeacon 广播数据解析器
*
* iBeacon 标准广播格式Manufacturer Specific Data
* - Company ID: 0x004C (Apple)
* - Beacon Type: 0x02, 0x15
* - UUID: 16 bytes
* - Major: 2 bytes (big-endian)
* - Minor: 2 bytes (big-endian)
* - TX Power: 1 byte (signed)
*
* 过滤规则(来自旧版 blueUtils.js
* - 设备名包含 "RFstar"
* - 0 < txPower <= 100
* - UUID/Major/Minor 均有效
*/
object IBeaconParser {
/** iBeacon 标识Company ID (0x4C00 little-endian) + Type (0x0215) */
private const val IBEACON_COMPANY_ID = 0x004C
private const val IBEACON_TYPE_BYTE1 = 0x02.toByte()
private const val IBEACON_TYPE_BYTE2 = 0x15.toByte()
/** 设备名过滤关键字 */
private const val DEVICE_NAME_FILTER = "RFstar"
/**
* 从 BLE 扫描结果解析 iBeacon 设备
* @return 解析成功且通过过滤返回 IBeaconDevice否则 null
*/
fun parse(scanResult: ScanResult): IBeaconDevice? {
val record = scanResult.scanRecord ?: return null
val deviceName = record.deviceName ?: scanResult.device?.name ?: ""
// 过滤:设备名必须包含 RFstar
if (!deviceName.contains(DEVICE_NAME_FILTER, ignoreCase = true)) return null
// 从 Manufacturer Specific Data 中解析 iBeacon
val mfgData = record.getManufacturerSpecificData(IBEACON_COMPANY_ID)
?: return null
// 检查 iBeacon 类型标识(前 2 字节)
if (mfgData.size < 23) return null
if (mfgData[0] != IBEACON_TYPE_BYTE1 || mfgData[1] != IBEACON_TYPE_BYTE2) return null
// 解析 UUID字节 2-17
val uuid = buildString {
for (i in 2..17) {
append(String.format("%02X", mfgData[i]))
if (i == 5 || i == 7 || i == 9 || i == 11) append("-")
}
}
// 解析 Major字节 18-19big-endian
val major = ((mfgData[18].toInt() and 0xFF) shl 8) or (mfgData[19].toInt() and 0xFF)
// 解析 Minor字节 20-21big-endian
val minor = ((mfgData[20].toInt() and 0xFF) shl 8) or (mfgData[21].toInt() and 0xFF)
// 解析 TX Power字节 22signed byte
val txPower = mfgData[22].toInt()
// 过滤:功率有效且 UUID/Major/Minor 非空
if (txPower <= 0 || txPower > 100) return null
if (uuid.isBlank() || major == 0 && minor == 0) return null
val mac = scanResult.device?.address ?: return null
return IBeaconDevice(
mac = mac,
rssi = scanResult.rssi,
uuid = uuid,
major = major,
minor = minor,
txPower = txPower,
name = deviceName
)
}
}

View File

@@ -5,6 +5,7 @@ import com.google.gson.GsonBuilder
import com.xiaoqu.watch.network.EnvConfig
import com.xiaoqu.watch.network.SignatureInterceptor
import com.xiaoqu.watch.network.UnbindInterceptor
import com.xiaoqu.watch.network.api.BluetoothApi
import com.xiaoqu.watch.network.api.CommonApi
import com.xiaoqu.watch.network.api.PunchApi
import com.xiaoqu.watch.network.api.TaskApi
@@ -76,4 +77,10 @@ object NetworkModule {
fun providePunchApi(retrofit: Retrofit): PunchApi {
return retrofit.create(PunchApi::class.java)
}
@Provides
@Singleton
fun provideBluetoothApi(retrofit: Retrofit): BluetoothApi {
return retrofit.create(BluetoothApi::class.java)
}
}

View File

@@ -0,0 +1,15 @@
package com.xiaoqu.watch.network.api
import com.xiaoqu.watch.network.ApiResponse
import retrofit2.http.Body
import retrofit2.http.POST
/**
* 蓝牙扫描相关 API
*/
interface BluetoothApi {
/** 上报蓝牙信标扫描结果,返回匹配的任务列表 */
@POST("watchTask/bluetoothScanTask")
suspend fun uploadScanResult(@Body params: HashMap<String, Any>): ApiResponse<List<Any>>
}

View File

@@ -0,0 +1,299 @@
package com.xiaoqu.watch.service.manager
import com.xiaoqu.watch.data.bluetooth.BleScanParams
import com.xiaoqu.watch.data.bluetooth.IBeaconDevice
import com.xiaoqu.watch.device.bluetooth.BleScanner
import com.xiaoqu.watch.device.screen.ScreenController
import com.xiaoqu.watch.event.AppEvent
import com.xiaoqu.watch.event.EventBus
import com.xiaoqu.watch.network.ApiResult
import com.xiaoqu.watch.network.api.BluetoothApi
import com.xiaoqu.watch.network.safeApiCall
import kotlinx.coroutines.*
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
/**
* 蓝牙扫描管理器(核心状态机)
*
* 管理常规/特殊双模式扫描:
* - 常规模式:周期性扫描 → 上报服务端 → 检查新任务
* - 特殊模式:精确 RSSI 采样 → 均值判定 → 触发任务通知
*
* 生命周期:
* - 上班(WorkStateChanged true) → start()
* - 下班/解绑 → stop()
* - MQTT type=4 → updateParams() → 重启
*/
@Singleton
class BluetoothScanManager @Inject constructor(
private val bleScanner: BleScanner,
private val bluetoothApi: BluetoothApi,
private val screenController: ScreenController,
private val eventBus: EventBus
) {
/** 扫描状态 */
enum class State { STOPPED, ROUTINE, TRANSITIONING, SPECIAL }
/** 当前状态 */
var state = State.STOPPED
private set
/** 扫描参数(可被 MQTT 更新) */
var params = BleScanParams()
private set
/** 上次 API 返回的任务列表(用于对比新任务) */
private var lastTaskIds = mutableSetOf<String>()
/** 协程作用域applicationScope不跟随 ViewModel */
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
/** 常规模式协程 */
private var routineJob: Job? = null
/** 特殊模式协程 */
private var specialJob: Job? = null
/** 当前电量(由外部更新) */
var currentPower: Int = 100
/** 当前网络类型(由外部更新) */
var currentNetworkType: String = ""
/**
* 启动蓝牙扫描(常规模式)
* 调用前确保蓝牙可用
*/
fun start() {
if (state != State.STOPPED) {
Timber.d("蓝牙扫描: 已在运行,忽略 start()")
return
}
if (!bleScanner.isAvailable()) {
Timber.w("蓝牙扫描: 蓝牙不可用,无法启动")
return
}
Timber.d("蓝牙扫描: 启动常规模式")
startRoutineLoop()
}
/** 停止所有扫描 */
fun stop() {
Timber.d("蓝牙扫描: 停止 (当前状态=$state)")
routineJob?.cancel()
specialJob?.cancel()
bleScanner.stopCurrentScan()
state = State.STOPPED
lastTaskIds.clear()
}
/**
* 更新扫描参数MQTT type=4
* 如果正在扫描,自动重启
*/
fun updateParams(newParams: BleScanParams) {
Timber.d("蓝牙扫描: 参数更新 $newParams")
params = newParams
if (state != State.STOPPED) {
// 停止 → 延迟 500ms → 重启(对齐旧版 index.vue 行为)
stop()
scope.launch {
delay(500)
start()
}
}
}
// ===== 常规模式 =====
/** 启动常规扫描循环 */
private fun startRoutineLoop() {
routineJob = scope.launch {
state = State.ROUTINE
Timber.d("蓝牙扫描: 进入常规循环 (扫描${params.routineScanMs}ms, 休息${params.routineRestMs}ms)")
while (isActive && state == State.ROUTINE) {
// 1. 扫描一轮
val devices = bleScanner.scanOnce(params.routineScanMs)
// 2. 上报服务端
if (devices.isNotEmpty()) {
val newTasks = uploadAndCheckTasks(devices)
if (newTasks != null) {
// 3. 有新任务 → 切换特殊模式
transitionToSpecial(newTasks)
return@launch // 退出常规循环
}
}
// 4. 休息
delay(params.routineRestMs)
}
}
}
/**
* 上报设备并检查新任务
* @return 有新任务返回任务列表,无新任务或失败返回 null
*/
private suspend fun uploadAndCheckTasks(devices: List<IBeaconDevice>): List<Map<String, Any>>? {
// 构造请求体
val deviceList = devices.map { device ->
hashMapOf<String, Any>(
"mac" to device.mac,
"rssi" to device.rssi
)
}
val requestParams = hashMapOf<String, Any>(
"devices" to deviceList,
"power" to currentPower,
"networkType" to currentNetworkType
)
val result = safeApiCall { bluetoothApi.uploadScanResult(requestParams) }
if (result !is ApiResult.Success || result.data == null) {
return null // API 失败,静默继续
}
// 解析任务列表
@Suppress("UNCHECKED_CAST")
val tasks = result.data as? List<Map<String, Any>> ?: return null
if (tasks.isEmpty()) return null
// 对比新旧任务
val currentIds = tasks.mapNotNull { it["id"]?.toString() }.toSet()
val newIds = currentIds - lastTaskIds
if (newIds.isEmpty()) {
// 无新任务
lastTaskIds = currentIds.toMutableSet()
return null
}
Timber.d("蓝牙扫描: 发现 ${newIds.size} 个新任务")
lastTaskIds = currentIds.toMutableSet()
return tasks
}
// ===== 特殊模式 =====
/** 信标追踪器 */
private data class BeaconTracker(
val mac: String,
val tasks: List<Map<String, Any>>,
val rssiSamples: MutableList<Int> = mutableListOf(),
var missCount: Int = 0
)
/** 切换到特殊模式1s 延迟) */
private fun transitionToSpecial(tasks: List<Map<String, Any>>) {
state = State.TRANSITIONING
bleScanner.stopCurrentScan()
Timber.d("蓝牙扫描: 准备进入特殊模式 (1s 延迟)")
specialJob = scope.launch {
delay(1000)
if (state == State.TRANSITIONING) {
startSpecialMode(tasks)
}
}
}
/** 启动特殊模式 RSSI 采样 */
private suspend fun startSpecialMode(tasks: List<Map<String, Any>>) {
state = State.SPECIAL
Timber.d("蓝牙扫描: 进入特殊模式 (${tasks.size} 个任务)")
// 按 MAC 分组建立追踪器
val trackers = mutableMapOf<String, BeaconTracker>()
for (task in tasks) {
val mac = task["mac"]?.toString() ?: continue
val tracker = trackers.getOrPut(mac) { BeaconTracker(mac, mutableListOf()) }
@Suppress("UNCHECKED_CAST")
(tracker.tasks as MutableList<Map<String, Any>>).add(task)
}
// 超时协程
val timeoutJob = scope.launch {
delay(params.specialTimeoutMs)
Timber.d("蓝牙扫描: 特殊模式超时")
}
// 采样循环
while (state == State.SPECIAL && !timeoutJob.isCompleted) {
val devices = bleScanner.scanOnce(params.specialScanMs)
// 更新 RSSI
val deviceByMac = devices.associateBy { it.mac }
val toRemove = mutableListOf<String>()
for ((mac, tracker) in trackers) {
val device = deviceByMac[mac]
if (device != null) {
tracker.missCount = 0
tracker.rssiSamples.add(device.rssi)
} else {
tracker.missCount++
}
// 连续 N 次未检测到 → 移除
if (tracker.missCount >= params.bluetoothSpecialCycleTimes) {
toRemove.add(mac)
}
}
toRemove.forEach { trackers.remove(it) }
// 所有信标消失
if (trackers.isEmpty()) {
Timber.d("蓝牙扫描: 特殊模式 - 信标全部消失")
timeoutJob.cancel()
onSpecialComplete(emptyList())
return
}
// 检查是否有信标达标
val matched = mutableListOf<Map<String, Any>>()
for ((_, tracker) in trackers) {
if (tracker.rssiSamples.size >= params.bluetoothSpecialCycleTimes) {
// 取最后 N 个样本算平均
val samples = tracker.rssiSamples.takeLast(params.bluetoothSpecialCycleTimes)
val average = samples.sum().toDouble() / samples.size
if (average >= params.bluetoothSign) {
matched.addAll(tracker.tasks)
Timber.d("蓝牙扫描: 信标 ${tracker.mac} 达标 (RSSI均值=$average, 阈值=${params.bluetoothSign})")
}
}
}
if (matched.isNotEmpty()) {
Timber.d("蓝牙扫描: 特殊模式 - ${matched.size} 个任务匹配")
timeoutJob.cancel()
onSpecialComplete(matched)
return
}
delay(params.specialRestMs)
}
// 超时退出
if (state == State.SPECIAL) {
Timber.d("蓝牙扫描: 特殊模式超时退出")
onSpecialComplete(emptyList())
}
}
/** 特殊模式结束处理 */
private fun onSpecialComplete(matchedTasks: List<Map<String, Any>>) {
if (matchedTasks.isNotEmpty()) {
// 唤醒屏幕
screenController.turnOn()
// 通知任务模块
scope.launch {
eventBus.emit(AppEvent.PunchTaskListRefresh)
}
Timber.d("蓝牙扫描: 已通知任务模块刷新")
}
// 恢复常规模式
startRoutineLoop()
}
}

View File

@@ -47,6 +47,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
@Inject lateinit var userPrefs: UserPrefs
@Inject lateinit var eventBus: EventBus
@Inject lateinit var taskApi: TaskApi
@Inject lateinit var bluetoothScanManager: com.xiaoqu.watch.service.manager.BluetoothScanManager
/** 考勤打卡 ViewModel */
private val punchViewModel: PunchViewModel by viewModels()
@@ -381,6 +382,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
// 电量变化
is AppEvent.BatteryChanged -> {
statusBar.updateBattery(event.level, event.isCharging)
bluetoothScanManager.currentPower = event.level
}
// 蓝牙状态变化
is AppEvent.BluetoothStateChanged -> {
@@ -395,8 +397,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
fetchStatistics()
}
3 -> {
// 解绑 → 清除数据 → 跳绑定页
// 解绑 → 停止蓝牙 → 清除数据 → 跳绑定页
Timber.d("首页: 收到解绑消息")
bluetoothScanManager.stop()
userPrefs.clear()
findNavController().navigate(R.id.action_home_to_bind)
}
@@ -431,6 +434,12 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
if (action == 0 || action == 1) {
val isWorking = action == 1
punchViewModel.handleWorkStateChange(isWorking)
// 上班 → 启动蓝牙扫描;下班 → 停止蓝牙扫描
if (isWorking) {
bluetoothScanManager.start()
} else {
bluetoothScanManager.stop()
}
}
} catch (e: Exception) {
Timber.w(e, "解析 MQTT 工作状态消息失败")
@@ -439,16 +448,21 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
/**
* 处理 MQTT type=4 系统参数变更
* 从中提取 nfcOpenTime 更新 NFC 超时时间
* 更新 NFC 超时时间 + 蓝牙扫描参数
*/
private fun handleMqttParamsUpdate(rawJson: String) {
try {
val json = org.json.JSONObject(rawJson)
// NFC 超时
val nfcOpenTime = json.optInt("nfcOpenTime", -1)
if (nfcOpenTime > 0) {
punchViewModel.nfcTimeoutMs = nfcOpenTime * 1000L
Timber.d("首页: NFC超时更新为 ${nfcOpenTime}s")
}
// 蓝牙扫描参数
val bleParams = com.xiaoqu.watch.data.bluetooth.BleScanParams.fromJson(json)
bluetoothScanManager.updateParams(bleParams)
Timber.d("首页: 蓝牙扫描参数已更新")
} catch (e: Exception) {
Timber.w(e, "解析 MQTT 参数变更消息失败")
}