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:
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user