From e7fa7b3b1d2e18b7ccbe7c73e06d0e57f4dd66d5 Mon Sep 17 00:00:00 2001 From: dongliang Date: Wed, 29 Apr 2026 12:06:59 +0930 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=93=9D=E7=89=99=E6=89=AB=E6=8F=8F?= =?UTF-8?q?=E4=B8=8EiBeacon=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心功能: - BleScanner: BLE扫描封装,SCAN_MODE_LOW_POWER低功耗 - IBeaconParser: 从ScanRecord解析UUID/Major/Minor/TxPower - BluetoothScanManager: 状态机管理常规/特殊双模式 - 常规模式:周期扫描→上报API→检查新任务 - 特殊模式:RSSI采样→均值判定→任务匹配→唤醒屏幕 - 设备过滤:名称含RFstar + 功率0 --- .../watch/data/bluetooth/BleScanParams.kt | 57 ++++ .../watch/data/bluetooth/IBeaconDevice.kt | 22 ++ .../watch/device/bluetooth/BleScanner.kt | 130 ++++++++ .../watch/device/bluetooth/IBeaconParser.kt | 84 +++++ .../java/com/xiaoqu/watch/di/NetworkModule.kt | 7 + .../xiaoqu/watch/network/api/BluetoothApi.kt | 15 + .../service/manager/BluetoothScanManager.kt | 299 ++++++++++++++++++ .../com/xiaoqu/watch/ui/home/HomeFragment.kt | 18 +- 8 files changed, 630 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/xiaoqu/watch/data/bluetooth/BleScanParams.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/data/bluetooth/IBeaconDevice.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/device/bluetooth/BleScanner.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/device/bluetooth/IBeaconParser.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/network/api/BluetoothApi.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/service/manager/BluetoothScanManager.kt diff --git a/app/src/main/java/com/xiaoqu/watch/data/bluetooth/BleScanParams.kt b/app/src/main/java/com/xiaoqu/watch/data/bluetooth/BleScanParams.kt new file mode 100644 index 0000000..4526932 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/data/bluetooth/BleScanParams.kt @@ -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) + ) + } + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/data/bluetooth/IBeaconDevice.kt b/app/src/main/java/com/xiaoqu/watch/data/bluetooth/IBeaconDevice.kt new file mode 100644 index 0000000..066afcf --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/data/bluetooth/IBeaconDevice.kt @@ -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 +) diff --git a/app/src/main/java/com/xiaoqu/watch/device/bluetooth/BleScanner.kt b/app/src/main/java/com/xiaoqu/watch/device/bluetooth/BleScanner.kt new file mode 100644 index 0000000..0d0543e --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/device/bluetooth/BleScanner.kt @@ -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_POWER(CLAUDE.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 + + /** 初始��蓝牙适配器 */ + 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 { + if (!isAvailable()) { + Timber.w("BLE扫描: 蓝牙不可用") + return emptyList() + } + + return suspendCancellableCoroutine { cont -> + val devices = mutableListOf() + val seenMacs = mutableSetOf() // 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 + } + + // ���时后停止扫描并返回结果 + 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 + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/device/bluetooth/IBeaconParser.kt b/app/src/main/java/com/xiaoqu/watch/device/bluetooth/IBeaconParser.kt new file mode 100644 index 0000000..fef18a4 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/device/bluetooth/IBeaconParser.kt @@ -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-19,big-endian) + val major = ((mfgData[18].toInt() and 0xFF) shl 8) or (mfgData[19].toInt() and 0xFF) + + // 解析 Minor(字节 20-21,big-endian) + val minor = ((mfgData[20].toInt() and 0xFF) shl 8) or (mfgData[21].toInt() and 0xFF) + + // 解析 TX Power(字节 22,signed 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 + ) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/di/NetworkModule.kt b/app/src/main/java/com/xiaoqu/watch/di/NetworkModule.kt index bb37484..5505607 100644 --- a/app/src/main/java/com/xiaoqu/watch/di/NetworkModule.kt +++ b/app/src/main/java/com/xiaoqu/watch/di/NetworkModule.kt @@ -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) + } } diff --git a/app/src/main/java/com/xiaoqu/watch/network/api/BluetoothApi.kt b/app/src/main/java/com/xiaoqu/watch/network/api/BluetoothApi.kt new file mode 100644 index 0000000..a7b742b --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/network/api/BluetoothApi.kt @@ -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): ApiResponse> +} diff --git a/app/src/main/java/com/xiaoqu/watch/service/manager/BluetoothScanManager.kt b/app/src/main/java/com/xiaoqu/watch/service/manager/BluetoothScanManager.kt new file mode 100644 index 0000000..261cf37 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/service/manager/BluetoothScanManager.kt @@ -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() + + /** 协程作用域(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): List>? { + // 构造请求体 + val deviceList = devices.map { device -> + hashMapOf( + "mac" to device.mac, + "rssi" to device.rssi + ) + } + val requestParams = hashMapOf( + "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> ?: 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>, + val rssiSamples: MutableList = mutableListOf(), + var missCount: Int = 0 + ) + + /** 切换到特殊模式(1s 延迟) */ + private fun transitionToSpecial(tasks: List>) { + 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>) { + state = State.SPECIAL + Timber.d("蓝牙扫描: 进入特殊模式 (${tasks.size} 个任务)") + + // 按 MAC 分组建立追踪器 + val trackers = mutableMapOf() + 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>).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() + + 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>() + 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>) { + if (matchedTasks.isNotEmpty()) { + // 唤醒屏幕 + screenController.turnOn() + // 通知任务模块 + scope.launch { + eventBus.emit(AppEvent.PunchTaskListRefresh) + } + Timber.d("蓝牙扫描: 已通知任务模块刷新") + } + // 恢复常规模式 + startRoutineLoop() + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt index 63854a1..ace6ee6 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt @@ -47,6 +47,7 @@ class HomeFragment : BaseFragment() { @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() { // 电量变化 is AppEvent.BatteryChanged -> { statusBar.updateBattery(event.level, event.isCharging) + bluetoothScanManager.currentPower = event.level } // 蓝牙状态变化 is AppEvent.BluetoothStateChanged -> { @@ -395,8 +397,9 @@ class HomeFragment : BaseFragment() { fetchStatistics() } 3 -> { - // 解绑 → 清除数据 → 跳绑定页 + // 解绑 → 停止蓝牙 → 清除数据 → 跳绑定页 Timber.d("首页: 收到解绑消息") + bluetoothScanManager.stop() userPrefs.clear() findNavController().navigate(R.id.action_home_to_bind) } @@ -431,6 +434,12 @@ class HomeFragment : BaseFragment() { 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() { /** * 处理 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 参数变更消息失败") }