feat: 系统控制模块 - 硬件抽象层
新增: - ScreenController 屏幕控制接口 + FiseScreenController 实现(ROM广播) - NfcController NFC控制接口 + FiseNfcController 实现(sysfs读写) - VibrationController 振动接口 + FiseVibrationController 实现(13种方案+音频) - SystemStateMonitor 系统状态监听(电量、蓝牙状态广播) - DeviceModule Hilt硬件抽象绑定 - 8个音频文件(res/raw/) - AppEvent 新增4个系统状态事件 修改: - MainActivity 注册 SystemStateMonitor - HomeFragment 硬件验证demo(熄屏/振动/NFC/电量实时显示) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5680613e86
commit
c7ae11a574
@@ -0,0 +1,126 @@
|
||||
package com.xiaoqu.watch.device.nfc
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileReader
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* FISE 定制 ROM NFC/RFID 控制实现
|
||||
* 通过 sysfs 文件读写控制 RFID 模块(FISE ROM 特有)
|
||||
*
|
||||
* 硬件接口:
|
||||
* - 上电:读 /sys/bus/i2c/drivers/rfid/1-0050/power_on
|
||||
* - 断电:读 /sys/bus/i2c/drivers/rfid/1-0050/power_off
|
||||
* - 读卡:读 /sys/bus/i2c/drivers/rfid/1-0050/typeA_id(轮询)
|
||||
*/
|
||||
@Singleton
|
||||
class FiseNfcController @Inject constructor() : NfcController {
|
||||
|
||||
companion object {
|
||||
/** RFID 上电文件路径 */
|
||||
private const val RFID_POWER_ON = "/sys/bus/i2c/drivers/rfid/1-0050/power_on"
|
||||
/** RFID 断电文件路径 */
|
||||
private const val RFID_POWER_OFF = "/sys/bus/i2c/drivers/rfid/1-0050/power_off"
|
||||
/** RFID 读卡文件路径 */
|
||||
private const val RFID_TYPEA_ID = "/sys/bus/i2c/drivers/rfid/1-0050/typeA_id"
|
||||
/** 轮询间隔(毫秒) */
|
||||
private const val SCAN_INTERVAL_MS = 1000L
|
||||
}
|
||||
|
||||
/** NFC 开关状态 */
|
||||
private var _isOpen = false
|
||||
|
||||
/** 轮询协程 Job */
|
||||
private var scanJob: Job? = null
|
||||
|
||||
/** 协程作用域 */
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
override fun isOpen(): Boolean = _isOpen
|
||||
|
||||
/** 开启 NFC:读 power_on 文件触发硬件上电 */
|
||||
override fun open() {
|
||||
Timber.d("NFC控制: 开启")
|
||||
readFile(RFID_POWER_ON)
|
||||
_isOpen = true
|
||||
}
|
||||
|
||||
/** 关闭 NFC:读 power_off 文件触发硬件断电,停止轮询 */
|
||||
override fun close() {
|
||||
Timber.d("NFC控制: 关闭")
|
||||
stopScan()
|
||||
readFile(RFID_POWER_OFF)
|
||||
_isOpen = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始轮询读卡
|
||||
* 每 1 秒读一次 typeA_id 文件,解析出卡号后回调
|
||||
* @param callback 读到卡后的回调,参数为 nfcId
|
||||
*/
|
||||
override fun startScan(callback: (nfcId: String) -> Unit) {
|
||||
// 先停止已有的轮询
|
||||
stopScan()
|
||||
|
||||
Timber.d("NFC控制: 开始轮询读卡")
|
||||
scanJob = scope.launch {
|
||||
while (isActive) {
|
||||
try {
|
||||
val content = readFile(RFID_TYPEA_ID)
|
||||
if (content.isNotEmpty()) {
|
||||
// 解析卡号:取第 12 位之后的内容,去除空格
|
||||
// 对应旧版:typeaId.substring(12).replace(/\s*/g, "")
|
||||
val nfcId = if (content.length > 12) {
|
||||
content.substring(12).replace("\\s+".toRegex(), "")
|
||||
} else {
|
||||
content.replace("\\s+".toRegex(), "")
|
||||
}
|
||||
|
||||
if (nfcId.isNotEmpty()) {
|
||||
Timber.d("NFC控制: 读到卡号 $nfcId")
|
||||
// 切回主线程回调
|
||||
withContext(Dispatchers.Main) {
|
||||
callback(nfcId)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "NFC控制: 读卡异常")
|
||||
}
|
||||
delay(SCAN_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止轮询 */
|
||||
override fun stopScan() {
|
||||
scanJob?.cancel()
|
||||
scanJob = null
|
||||
Timber.d("NFC控制: 停止轮询")
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 sysfs 文件内容
|
||||
* @param path 文件路径
|
||||
* @return 文件内容(读取失败返回空字符串)
|
||||
*/
|
||||
private fun readFile(path: String): String {
|
||||
return try {
|
||||
val file = File(path)
|
||||
if (!file.exists()) {
|
||||
Timber.w("NFC控制: 文件不存在 $path")
|
||||
return ""
|
||||
}
|
||||
BufferedReader(FileReader(file)).use { reader ->
|
||||
reader.readLine() ?: ""
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "NFC控制: 读取文件失败 $path")
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xiaoqu.watch.device.nfc
|
||||
|
||||
/**
|
||||
* NFC/RFID 控制接口(硬件抽象层)
|
||||
* 封装 NFC 开关和读卡操作,换设备时只需替换实现类
|
||||
*/
|
||||
interface NfcController {
|
||||
/** NFC 是否已开启 */
|
||||
fun isOpen(): Boolean
|
||||
/** 开启 NFC 电源 */
|
||||
fun open()
|
||||
/** 关闭 NFC 电源 */
|
||||
fun close()
|
||||
/** 开始轮询读卡,读到卡后回调 nfcId */
|
||||
fun startScan(callback: (nfcId: String) -> Unit)
|
||||
/** 停止轮询 */
|
||||
fun stopScan()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.xiaoqu.watch.device.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.PowerManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* FISE 定制 ROM 屏幕控制实现
|
||||
* 通过系统广播控制亮屏/熄屏(FISE ROM 特有)
|
||||
*/
|
||||
@Singleton
|
||||
class FiseScreenController @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) : ScreenController {
|
||||
|
||||
companion object {
|
||||
/** FISE ROM 亮屏广播 */
|
||||
private const val ACTION_SCREEN_ON = "com.fise.turn_screen_on"
|
||||
/** FISE ROM 熄屏广播 */
|
||||
private const val ACTION_SCREEN_OFF = "com.fise.turn_screen_off"
|
||||
}
|
||||
|
||||
/** 获取屏幕是否亮着(通过 PowerManager 标准 API) */
|
||||
override fun isScreenOn(): Boolean {
|
||||
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return pm.isScreenOn
|
||||
}
|
||||
|
||||
/** 亮屏:发送 FISE ROM 自定义广播 */
|
||||
override fun turnOn() {
|
||||
Timber.d("屏幕控制: 亮屏")
|
||||
context.sendBroadcast(Intent(ACTION_SCREEN_ON))
|
||||
}
|
||||
|
||||
/** 熄屏:发送 FISE ROM 自定义广播 */
|
||||
override fun turnOff() {
|
||||
Timber.d("屏幕控制: 熄屏")
|
||||
context.sendBroadcast(Intent(ACTION_SCREEN_OFF))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xiaoqu.watch.device.screen
|
||||
|
||||
/**
|
||||
* 屏幕控制接口(硬件抽象层)
|
||||
* 封装亮屏/熄屏操作,换设备时只需替换实现类
|
||||
*/
|
||||
interface ScreenController {
|
||||
/** 获取屏幕是否亮着 */
|
||||
fun isScreenOn(): Boolean
|
||||
/** 亮屏 */
|
||||
fun turnOn()
|
||||
/** 熄屏 */
|
||||
fun turnOff()
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.xiaoqu.watch.device.sensor
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Vibrator
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* FISE 振动控制实现
|
||||
* ���持简单振动和方案振动(嵌套循环 + 音频反馈)
|
||||
*
|
||||
* 振动算法(与旧版 shockStore.js 一致):
|
||||
* 外层循环 shockCycleTimes 次 {
|
||||
* 内层循环 shockTimes 次 {
|
||||
* 振动 shockTime 秒
|
||||
* 休息 shockIntervalTime 秒
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@Singleton
|
||||
class FiseVibrationController @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) : VibrationController {
|
||||
|
||||
/** 系统振动器 */
|
||||
@Suppress("DEPRECATION")
|
||||
private val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
||||
/** 当前振动协程 Job */
|
||||
private var patternJob: Job? = null
|
||||
|
||||
/** 协程作用域 */
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
/** 当�� MediaPlayer(播放完毕后释��) */
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
|
||||
/** 执行一次���单振动 */
|
||||
@Suppress("DEPRECATION")
|
||||
override fun vibrate(durationMs: Long) {
|
||||
Timber.d("振动: ${durationMs}ms")
|
||||
vibrator.vibrate(durationMs)
|
||||
}
|
||||
|
||||
/**
|
||||
* 按方案执行��动(���循环和音频反馈)
|
||||
* @param pattern 振动方案
|
||||
*/
|
||||
override fun executePattern(pattern: VibrationPattern) {
|
||||
// 先停止之前的振动
|
||||
stop()
|
||||
|
||||
Timber.d("振动方案: ${pattern.planName}(planId=${pattern.planId})")
|
||||
|
||||
// 播放音频(如果启用且有音频资源)
|
||||
if (pattern.voiceState && pattern.audioResId != 0) {
|
||||
playAudio(pattern.audioResId)
|
||||
}
|
||||
|
||||
// 执行振动(如果启用)
|
||||
if (pattern.shockState) {
|
||||
patternJob = scope.launch {
|
||||
executePatternLoop(pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止当前振动和音频 */
|
||||
override fun stop() {
|
||||
// 停止振动��程
|
||||
patternJob?.cancel()
|
||||
patternJob = null
|
||||
// 停止硬件振动
|
||||
vibrator.cancel()
|
||||
// 停止音频
|
||||
releaseMediaPlayer()
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行振动方案的嵌套循环
|
||||
* 外层:shockCycleTimes 次
|
||||
* 内层:shockTimes 次振动 + 休息
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private suspend fun executePatternLoop(pattern: VibrationPattern) {
|
||||
val vibrateDurationMs = pattern.shockTime * 1000L
|
||||
val intervalMs = pattern.shockIntervalTime * 1000L
|
||||
|
||||
// 外层循环:频次循环
|
||||
for (cycle in 0 until pattern.shockCycleTimes) {
|
||||
// 内层循环:每频次振动次数
|
||||
for (times in 0 until pattern.shockTimes) {
|
||||
// 振动
|
||||
vibrator.vibrate(vibrateDurationMs)
|
||||
// 等待振动��成 + 休息间隔
|
||||
delay(vibrateDurationMs + intervalMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放音频反馈
|
||||
* @param resId 音频资源 ID(R.raw.xxx)
|
||||
*/
|
||||
private fun playAudio(resId: Int) {
|
||||
try {
|
||||
// 释放上一个 MediaPlayer
|
||||
releaseMediaPlayer()
|
||||
// 创建并播放
|
||||
mediaPlayer = MediaPlayer.create(context, resId)?.apply {
|
||||
setOnCompletionListener {
|
||||
// 播放完毕自动释放
|
||||
it.release()
|
||||
mediaPlayer = null
|
||||
}
|
||||
start()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "振动: 音频播放失败")
|
||||
}
|
||||
}
|
||||
|
||||
/** 释放 MediaPlayer 防止内存泄漏 */
|
||||
private fun releaseMediaPlayer() {
|
||||
try {
|
||||
mediaPlayer?.release()
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
}
|
||||
mediaPlayer = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xiaoqu.watch.device.sensor
|
||||
|
||||
/**
|
||||
* 振动控制接口(硬件抽象层)
|
||||
* 封装简单振动和方案振动(含循环+音频)
|
||||
*/
|
||||
interface VibrationController {
|
||||
/** 执行一次简单振动 */
|
||||
fun vibrate(durationMs: Long)
|
||||
/** 按方案执行振动(含循环和音频反馈) */
|
||||
fun executePattern(pattern: VibrationPattern)
|
||||
/** 停止当前振动 */
|
||||
fun stop()
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.xiaoqu.watch.device.sensor
|
||||
|
||||
import com.xiaoqu.watch.R
|
||||
|
||||
/**
|
||||
* 13 种默认振动方案
|
||||
* 对应旧版 shockStore.js 中的 shockParams
|
||||
* 参数可被服务端下发覆盖
|
||||
*/
|
||||
object VibrationDefaults {
|
||||
|
||||
/** 根据 planId 获取默认振动方案 */
|
||||
fun getPattern(planId: Int): VibrationPattern? = patterns[planId]
|
||||
|
||||
/** 所有默认方案 */
|
||||
val patterns: Map<Int, VibrationPattern> = mapOf(
|
||||
// planId 2: 新消息
|
||||
2 to VibrationPattern(
|
||||
planId = 2, planName = "新消息",
|
||||
shockTime = 1, shockTimes = 2, shockIntervalTime = 1, shockCycleTimes = 1,
|
||||
audioResId = 0
|
||||
),
|
||||
// planId 3: 未读提醒
|
||||
3 to VibrationPattern(
|
||||
planId = 3, planName = "未读提醒",
|
||||
shockTime = 1, shockTimes = 3, shockIntervalTime = 1, shockCycleTimes = 1,
|
||||
audioResId = 0
|
||||
),
|
||||
// planId 4: 单次打卡成功
|
||||
4 to VibrationPattern(
|
||||
planId = 4, planName = "单次打卡成功",
|
||||
shockTime = 1, shockTimes = 1, shockIntervalTime = 0, shockCycleTimes = 1,
|
||||
audioResId = R.raw.punch_success
|
||||
),
|
||||
// planId 5: 批量打卡成功
|
||||
5 to VibrationPattern(
|
||||
planId = 5, planName = "批量打卡成功",
|
||||
shockTime = 1, shockTimes = 2, shockIntervalTime = 1, shockCycleTimes = 1,
|
||||
audioResId = R.raw.punch_success
|
||||
),
|
||||
// planId 6: 进入打卡范围
|
||||
6 to VibrationPattern(
|
||||
planId = 6, planName = "进入打卡范围",
|
||||
shockTime = 1, shockTimes = 1, shockIntervalTime = 0, shockCycleTimes = 1,
|
||||
audioResId = R.raw.open_success
|
||||
),
|
||||
// planId 7: 打卡失败
|
||||
7 to VibrationPattern(
|
||||
planId = 7, planName = "打卡失败",
|
||||
shockTime = 1, shockTimes = 3, shockIntervalTime = 1, shockCycleTimes = 2,
|
||||
audioResId = 0
|
||||
),
|
||||
// planId 8: NFC 开启
|
||||
8 to VibrationPattern(
|
||||
planId = 8, planName = "NFC开启",
|
||||
shockTime = 1, shockTimes = 1, shockIntervalTime = 0, shockCycleTimes = 1,
|
||||
audioResId = R.raw.open_punch
|
||||
),
|
||||
// planId 9: NFC 关闭
|
||||
9 to VibrationPattern(
|
||||
planId = 9, planName = "NFC关闭",
|
||||
shockTime = 1, shockTimes = 1, shockIntervalTime = 0, shockCycleTimes = 1,
|
||||
audioResId = R.raw.close_punch
|
||||
),
|
||||
// planId 10: 离线
|
||||
10 to VibrationPattern(
|
||||
planId = 10, planName = "离线",
|
||||
shockTime = 1, shockTimes = 2, shockIntervalTime = 1, shockCycleTimes = 1,
|
||||
audioResId = R.raw.offline
|
||||
),
|
||||
// planId 11: 开门失败
|
||||
11 to VibrationPattern(
|
||||
planId = 11, planName = "开门失败",
|
||||
shockTime = 1, shockTimes = 2, shockIntervalTime = 1, shockCycleTimes = 1,
|
||||
audioResId = R.raw.open_failed
|
||||
),
|
||||
// planId 12: 无权限开门
|
||||
12 to VibrationPattern(
|
||||
planId = 12, planName = "无权限开门",
|
||||
shockTime = 1, shockTimes = 2, shockIntervalTime = 1, shockCycleTimes = 1,
|
||||
audioResId = R.raw.no_auth_open
|
||||
),
|
||||
// planId 13: 正在开锁
|
||||
13 to VibrationPattern(
|
||||
planId = 13, planName = "正在开锁",
|
||||
shockTime = 1, shockTimes = 1, shockIntervalTime = 0, shockCycleTimes = 1,
|
||||
audioResId = R.raw.open_locking
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.xiaoqu.watch.device.sensor
|
||||
|
||||
/**
|
||||
* 振动方案数据类
|
||||
* 对应旧版 shockStore.js 中的振动参数
|
||||
*
|
||||
* 振动算法:外层循环 shockCycleTimes 次,每次内层振动 shockTimes 次,
|
||||
* 每次振动 shockTime 秒,振动间休息 shockIntervalTime 秒
|
||||
*/
|
||||
data class VibrationPattern(
|
||||
/** 方案 ID(2-13) */
|
||||
val planId: Int,
|
||||
/** 方案名称 */
|
||||
val planName: String,
|
||||
/** 单次振动时长(秒) */
|
||||
val shockTime: Int,
|
||||
/** 每频次振动次数 */
|
||||
val shockTimes: Int,
|
||||
/** 振动间休息时长(秒) */
|
||||
val shockIntervalTime: Int,
|
||||
/** 频次循环次数 */
|
||||
val shockCycleTimes: Int,
|
||||
/** 是否启用振动 */
|
||||
val shockState: Boolean = true,
|
||||
/** 是否启用声音 */
|
||||
val voiceState: Boolean = true,
|
||||
/** 音频资源 ID(R.raw.xxx),0 表示无音频 */
|
||||
val audioResId: Int = 0
|
||||
)
|
||||
Reference in New Issue
Block a user