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,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()
}
}