增加语言选择
@ -28,11 +28,15 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import com.example.smarthome.data.BackgroundManager
|
||||
import com.example.smarthome.data.LanguageManager
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// 初始化语言管理器
|
||||
LanguageManager.init(this)
|
||||
|
||||
// 隐藏状态栏和导航栏
|
||||
hideStatusBar()
|
||||
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
package com.example.smarthome.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.example.smarthome.R
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* 背景壁纸管理器
|
||||
*/
|
||||
object BackgroundManager {
|
||||
private const val PREFS_NAME = "background_prefs"
|
||||
private const val KEY_BACKGROUND = "selected_background"
|
||||
|
||||
private val _selectedBackground = MutableStateFlow(0)
|
||||
val selectedBackground: StateFlow<Int> = _selectedBackground.asStateFlow()
|
||||
|
||||
// 可用的背景列表
|
||||
val backgrounds = listOf(
|
||||
BackgroundOption(
|
||||
id = 0,
|
||||
name = "丝绸白",
|
||||
resourceId = R.drawable.background1,
|
||||
thumbId = R.drawable.background1_thumb,
|
||||
description = "优雅的白色丝绸质感",
|
||||
isDark = false
|
||||
),
|
||||
BackgroundOption(
|
||||
id = 1,
|
||||
name = "星空海",
|
||||
resourceId = R.drawable.background2,
|
||||
thumbId = R.drawable.background2_thumb,
|
||||
description = "深邃的星空与荧光海浪",
|
||||
isDark = true
|
||||
),
|
||||
BackgroundOption(
|
||||
id = 2,
|
||||
name = "紫金绸",
|
||||
resourceId = R.drawable.background3,
|
||||
thumbId = R.drawable.background3_thumb,
|
||||
description = "紫色金丝流动质感",
|
||||
isDark = true
|
||||
)
|
||||
)
|
||||
|
||||
fun init(context: Context) {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
_selectedBackground.value = prefs.getInt(KEY_BACKGROUND, 0)
|
||||
}
|
||||
|
||||
fun setBackground(context: Context, backgroundId: Int) {
|
||||
_selectedBackground.value = backgroundId
|
||||
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putInt(KEY_BACKGROUND, backgroundId)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun getCurrentBackgroundResource(): Int {
|
||||
val id = _selectedBackground.value
|
||||
return backgrounds.getOrNull(id)?.resourceId ?: R.drawable.background1
|
||||
}
|
||||
}
|
||||
|
||||
data class BackgroundOption(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val resourceId: Int,
|
||||
val thumbId: Int,
|
||||
val description: String,
|
||||
val isDark: Boolean = false
|
||||
)
|
||||
|
||||
// Compose 便捷方法
|
||||
@Composable
|
||||
fun rememberSelectedBackground(): Int {
|
||||
val context = LocalContext.current
|
||||
remember { BackgroundManager.init(context) }
|
||||
val selected by BackgroundManager.selectedBackground.collectAsState()
|
||||
return BackgroundManager.backgrounds.getOrNull(selected)?.resourceId ?: R.drawable.background1
|
||||
}
|
||||
476
app/src/main/java/com/example/smarthome/data/CanService.kt
Normal file
@ -0,0 +1,476 @@
|
||||
package com.example.smarthome.data
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
|
||||
/**
|
||||
* CAN 总线服务
|
||||
*
|
||||
* 依赖库(需要在 build.gradle 中添加):
|
||||
* implementation("com.vi:vioserial:x.x.x") // 串口通信库
|
||||
*
|
||||
* CAN 帧格式(与原代码一致):
|
||||
* AA 01 00 08 00 00 XX XX [8字节数据] 7A
|
||||
* - AA: 帧头
|
||||
* - 01 00: 固定
|
||||
* - 08: 数据长度(8字节)
|
||||
* - 00 00 XX XX: CAN ID(格口地址)
|
||||
* - [8字节]: 命令数据(ASCII HEX)
|
||||
* - 7A: 帧尾
|
||||
*/
|
||||
|
||||
// ==================== 常量定义 ====================
|
||||
|
||||
object CanConstants {
|
||||
const val TAG = "CanService"
|
||||
|
||||
// 默认串口配置
|
||||
const val DEFAULT_COM = "/dev/ttyS4" // 方派10寸PAD
|
||||
// const val DEFAULT_COM = "/dev/ttysWK1" // 萤火虫10寸PAD
|
||||
// const val DEFAULT_COM = "/dev/ttyS3" // 梵骏10寸PAD
|
||||
|
||||
const val BAUD_RATE = 115200
|
||||
|
||||
// CAN 帧格式
|
||||
const val FRAME_HEAD = "AA0100"
|
||||
const val FRAME_DATA_LEN = "08"
|
||||
const val FRAME_TAIL = "7A"
|
||||
|
||||
// 门锁状态(根据锁类型可能相反)
|
||||
const val DOOR_OPEN_STATUS = "1" // 新锁:1=开
|
||||
const val DOOR_CLOSE_STATUS = "0" // 新锁:0=关
|
||||
}
|
||||
|
||||
// ==================== CAN 命令定义 ====================
|
||||
|
||||
/**
|
||||
* CAN 命令枚举(与原代码一致)
|
||||
*/
|
||||
object CanCommands {
|
||||
// 门锁控制
|
||||
const val LON = "LON" // 开锁
|
||||
const val LOF = "LOF" // 关锁
|
||||
const val LST = "LST" // 查询锁状态
|
||||
|
||||
// 电源控制
|
||||
const val PON = "PON" // 上电
|
||||
const val POF = "POF" // 断电
|
||||
|
||||
// LED 控制
|
||||
const val RON = "RON" // 红灯亮
|
||||
const val GON = "GON" // 绿灯亮
|
||||
const val AOF = "AOF" // 灯灭
|
||||
|
||||
// 设备信息读取
|
||||
const val PVD = "PVD" // 读取 PID/VID
|
||||
const val MFG = "MFG" // 读取制造商
|
||||
const val PRO = "PRO" // 读取产品名
|
||||
const val DSN = "DSN" // 读取设备序列号
|
||||
const val CON = "CON" // 读取连接状态
|
||||
|
||||
// 固件相关
|
||||
const val VER = "1VER" // 读取固件版本
|
||||
const val LEN = "1LEN" // 固件长度
|
||||
const val MD5 = "1MD5" // 固件MD5
|
||||
const val MDE = "1MDE" // 固件MD5结束
|
||||
}
|
||||
|
||||
// ==================== 数据模型 ====================
|
||||
|
||||
/**
|
||||
* CAN 消息
|
||||
*/
|
||||
data class CanMessage(
|
||||
val cellName: String, // 格口名称,如 "A01"
|
||||
val command: String, // 命令,如 "LON"
|
||||
val data: String = "", // 附加数据
|
||||
val rawFrame: String = "" // 原始帧数据
|
||||
)
|
||||
|
||||
/**
|
||||
* CAN 响应
|
||||
*/
|
||||
data class CanResponse(
|
||||
val cellName: String,
|
||||
val command: String,
|
||||
val value: String,
|
||||
val rawData: String
|
||||
)
|
||||
|
||||
// ==================== CAN 服务接口 ====================
|
||||
|
||||
interface ICanService {
|
||||
val isConnected: StateFlow<Boolean>
|
||||
val lastResponse: StateFlow<CanResponse?>
|
||||
|
||||
// 串口管理
|
||||
fun openCom(comPath: String = CanConstants.DEFAULT_COM, baudRate: Int = CanConstants.BAUD_RATE): Boolean
|
||||
fun closeCom()
|
||||
|
||||
// 发送命令
|
||||
fun sendCmd(cellName: String, cmd: String)
|
||||
fun sendData(addr: String, cmd: String, data: String)
|
||||
|
||||
// 门锁控制
|
||||
fun openDoor(cellName: String)
|
||||
fun closeDoor(cellName: String)
|
||||
fun queryDoorStatus(cellName: String)
|
||||
|
||||
// 电源控制
|
||||
fun powerOn(cellName: String)
|
||||
fun powerOff(cellName: String)
|
||||
|
||||
// LED 控制
|
||||
fun ledRed(cellName: String)
|
||||
fun ledGreen(cellName: String)
|
||||
fun ledOff(cellName: String)
|
||||
|
||||
// 设备信息
|
||||
fun readDeviceSn(cellName: String)
|
||||
fun readDevicePvd(cellName: String)
|
||||
fun readDeviceMfg(cellName: String)
|
||||
fun readDevicePro(cellName: String)
|
||||
fun readConnectStatus(cellName: String)
|
||||
|
||||
// 批量操作
|
||||
fun openDoors(cellNames: List<String>)
|
||||
fun powerOnAll(cellNames: List<String>)
|
||||
fun powerOffAll(cellNames: List<String>)
|
||||
fun ledGreenAll(cellNames: List<String>)
|
||||
fun ledOffAll(cellNames: List<String>)
|
||||
|
||||
// 回调
|
||||
fun setResponseCallback(callback: (CanResponse) -> Unit)
|
||||
}
|
||||
|
||||
// ==================== CAN 服务实现(模拟)====================
|
||||
|
||||
class CanServiceImpl private constructor(
|
||||
private val context: Context
|
||||
) : ICanService {
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var instance: CanServiceImpl? = null
|
||||
|
||||
fun getInstance(context: Context): CanServiceImpl {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: CanServiceImpl(context.applicationContext).also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 状态
|
||||
private val _isConnected = MutableStateFlow(false)
|
||||
override val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()
|
||||
|
||||
private val _lastResponse = MutableStateFlow<CanResponse?>(null)
|
||||
override val lastResponse: StateFlow<CanResponse?> = _lastResponse.asStateFlow()
|
||||
|
||||
// 发送队列
|
||||
private val sendQueue: Queue<String> = LinkedList()
|
||||
|
||||
// 回调
|
||||
private var responseCallback: ((CanResponse) -> Unit)? = null
|
||||
|
||||
// TODO: 真实串口对象
|
||||
// private var comSerial: COMSerial? = null
|
||||
|
||||
// ==================== 串口管理 ====================
|
||||
|
||||
override fun openCom(comPath: String, baudRate: Int): Boolean {
|
||||
Log.d(CanConstants.TAG, "openCom: $comPath, baudRate: $baudRate")
|
||||
|
||||
// TODO: 打开真实串口
|
||||
// comSerial = COMSerial.instance()
|
||||
// val result = comSerial?.open(comPath, baudRate) ?: false
|
||||
// if (result) {
|
||||
// comSerial?.setOnDataListener(onComDataListener)
|
||||
// _isConnected.value = true
|
||||
// startSendThread()
|
||||
// startReceiveThread()
|
||||
// }
|
||||
// return result
|
||||
|
||||
// 模拟连接成功
|
||||
_isConnected.value = true
|
||||
return true
|
||||
}
|
||||
|
||||
override fun closeCom() {
|
||||
Log.d(CanConstants.TAG, "closeCom")
|
||||
_isConnected.value = false
|
||||
|
||||
// TODO: 关闭真实串口
|
||||
// comSerial?.close()
|
||||
// comSerial = null
|
||||
}
|
||||
|
||||
// ==================== 发送命令 ====================
|
||||
|
||||
override fun sendCmd(cellName: String, cmd: String) {
|
||||
val addr = convertCellNameToCanId(cellName)
|
||||
val convertCmd = stringToHexAscii(cmd.padEnd(7, ':').take(7))
|
||||
|
||||
val canPackage = buildCanFrame(addr, convertCmd)
|
||||
sendPortData(canPackage, "$cellName$cmd")
|
||||
}
|
||||
|
||||
override fun sendData(addr: String, cmd: String, data: String) {
|
||||
val convertCmd = when {
|
||||
cmd == CanCommands.VER -> stringToHexAscii(cmd.padEnd(8, ':'))
|
||||
cmd in listOf(CanCommands.LEN, CanCommands.MD5, CanCommands.MDE) ->
|
||||
stringToHexAscii(cmd) + data
|
||||
cmd.isEmpty() -> data.padEnd(16, ':'.code.toByte().toInt().toChar())
|
||||
else -> stringToHexAscii(cmd.padEnd(8, ':'))
|
||||
}
|
||||
|
||||
val canPackage = buildCanFrame("0000$addr", convertCmd)
|
||||
sendPortData(canPackage, "$addr$cmd")
|
||||
}
|
||||
|
||||
// ==================== 门锁控制 ====================
|
||||
|
||||
override fun openDoor(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.LON)
|
||||
}
|
||||
|
||||
override fun closeDoor(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.LOF)
|
||||
}
|
||||
|
||||
override fun queryDoorStatus(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.LST)
|
||||
}
|
||||
|
||||
// ==================== 电源控制 ====================
|
||||
|
||||
override fun powerOn(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.PON)
|
||||
}
|
||||
|
||||
override fun powerOff(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.POF)
|
||||
}
|
||||
|
||||
// ==================== LED 控制 ====================
|
||||
|
||||
override fun ledRed(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.RON)
|
||||
}
|
||||
|
||||
override fun ledGreen(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.GON)
|
||||
}
|
||||
|
||||
override fun ledOff(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.AOF)
|
||||
}
|
||||
|
||||
// ==================== 设备信息 ====================
|
||||
|
||||
override fun readDeviceSn(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.DSN)
|
||||
}
|
||||
|
||||
override fun readDevicePvd(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.PVD)
|
||||
}
|
||||
|
||||
override fun readDeviceMfg(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.MFG)
|
||||
}
|
||||
|
||||
override fun readDevicePro(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.PRO)
|
||||
}
|
||||
|
||||
override fun readConnectStatus(cellName: String) {
|
||||
sendCmd(cellName, CanCommands.CON)
|
||||
}
|
||||
|
||||
// ==================== 批量操作 ====================
|
||||
|
||||
override fun openDoors(cellNames: List<String>) {
|
||||
cellNames.forEach { openDoor(it) }
|
||||
}
|
||||
|
||||
override fun powerOnAll(cellNames: List<String>) {
|
||||
cellNames.forEach { powerOn(it) }
|
||||
}
|
||||
|
||||
override fun powerOffAll(cellNames: List<String>) {
|
||||
cellNames.forEach { powerOff(it) }
|
||||
}
|
||||
|
||||
override fun ledGreenAll(cellNames: List<String>) {
|
||||
cellNames.forEach { ledGreen(it) }
|
||||
}
|
||||
|
||||
override fun ledOffAll(cellNames: List<String>) {
|
||||
cellNames.forEach { ledOff(it) }
|
||||
}
|
||||
|
||||
// ==================== 回调 ====================
|
||||
|
||||
override fun setResponseCallback(callback: (CanResponse) -> Unit) {
|
||||
responseCallback = callback
|
||||
}
|
||||
|
||||
// ==================== 内部方法 ====================
|
||||
|
||||
private fun sendPortData(frame: String, info: String) {
|
||||
if (!_isConnected.value) {
|
||||
Log.w(CanConstants.TAG, "sendPortData: 串口未打开")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(CanConstants.TAG, "sendPortData: $info -> $frame")
|
||||
sendQueue.add("$frame$info")
|
||||
|
||||
// TODO: 真实发送
|
||||
// comSerial?.send(hexStringToBytes(frame))
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 CAN 帧
|
||||
*/
|
||||
private fun buildCanFrame(canId: String, data: String): String {
|
||||
return "${CanConstants.FRAME_HEAD}${CanConstants.FRAME_DATA_LEN}$canId$data${CanConstants.FRAME_TAIL}"
|
||||
}
|
||||
|
||||
/**
|
||||
* 格口名称转 CAN ID
|
||||
* 例如: "A01" -> "0101", "B02" -> "0202"
|
||||
*/
|
||||
private fun convertCellNameToCanId(cellName: String): String {
|
||||
if (cellName.length < 3) return "0000"
|
||||
|
||||
val cabinet = cellName[0] // A, B, C...
|
||||
val cell = cellName.substring(1).toIntOrNull() ?: 0
|
||||
|
||||
val cabinetId = (cabinet.uppercaseChar() - 'A' + 1).toString().padStart(2, '0')
|
||||
val cellId = cell.toString().padStart(2, '0')
|
||||
|
||||
return "$cabinetId$cellId"
|
||||
}
|
||||
|
||||
/**
|
||||
* CAN ID 转格口名称
|
||||
*/
|
||||
private fun convertCanIdToCellName(canId: String): String {
|
||||
if (canId.length < 4) return ""
|
||||
|
||||
val cabinetNum = canId.substring(0, 2).toIntOrNull() ?: 0
|
||||
val cellNum = canId.substring(2, 4).toIntOrNull() ?: 0
|
||||
|
||||
val cabinet = ('A' + cabinetNum - 1)
|
||||
return "$cabinet${cellNum.toString().padStart(2, '0')}"
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串转 HEX ASCII
|
||||
*/
|
||||
private fun stringToHexAscii(str: String): String {
|
||||
return str.map { Integer.toHexString(it.code).padStart(2, '0') }.joinToString("")
|
||||
}
|
||||
|
||||
/**
|
||||
* HEX 字符串转字节数组
|
||||
*/
|
||||
private fun hexStringToBytes(hex: String): ByteArray {
|
||||
val len = hex.length / 2
|
||||
val result = ByteArray(len)
|
||||
for (i in 0 until len) {
|
||||
result[i] = hex.substring(i * 2, i * 2 + 2).toInt(16).toByte()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的数据(由串口回调调用)
|
||||
*/
|
||||
internal fun handleReceivedData(data: String) {
|
||||
Log.d(CanConstants.TAG, "handleReceivedData: $data")
|
||||
|
||||
// 解析 CAN 帧
|
||||
// 格式: AA0100 08 CANID DATA 7A
|
||||
if (data.length < 26 || !data.startsWith("AA") || !data.endsWith("7A")) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val canId = data.substring(10, 14)
|
||||
val payload = data.substring(14, 30)
|
||||
|
||||
val cellName = convertCanIdToCellName(canId)
|
||||
val (cmd, value) = parsePayload(payload)
|
||||
|
||||
val response = CanResponse(
|
||||
cellName = cellName,
|
||||
command = cmd,
|
||||
value = value,
|
||||
rawData = data
|
||||
)
|
||||
|
||||
_lastResponse.value = response
|
||||
responseCallback?.invoke(response)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(CanConstants.TAG, "parseReceivedData error", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析数据负载
|
||||
*/
|
||||
private fun parsePayload(payload: String): Pair<String, String> {
|
||||
// 将 HEX 转回 ASCII
|
||||
val ascii = StringBuilder()
|
||||
for (i in payload.indices step 2) {
|
||||
val byte = payload.substring(i, i + 2).toInt(16)
|
||||
if (byte != 0x3A) { // 不是冒号
|
||||
ascii.append(byte.toChar())
|
||||
}
|
||||
}
|
||||
|
||||
val str = ascii.toString()
|
||||
val cmd = str.take(3)
|
||||
val value = str.drop(3).trim()
|
||||
|
||||
return cmd to value
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 便捷扩展 ====================
|
||||
|
||||
fun Context.getCanService(): ICanService = CanServiceImpl.getInstance(this)
|
||||
|
||||
// ==================== 操作类型常量(与原代码一致)====================
|
||||
|
||||
object OperationType {
|
||||
const val OPT_BORROW = 2 // 借
|
||||
const val OPT_RETURN = 3 // 还
|
||||
const val OPT_REPAIRDEVIN = 4 // 报修归还
|
||||
const val OPT_REPAIRDEVOUT = 5 // 维修设备出柜
|
||||
const val OPT_BATCHOUT = 6 // 批量出柜
|
||||
const val OPT_BATCHIN = 7 // 批量新增入柜
|
||||
const val OPT_RESETSELECTECELLS = 8 // 恢复异常格口状态
|
||||
const val OPT_REPAIRSELECTECELLS = 9 // 格口报修
|
||||
const val OPT_PONSELECTECELLS = 10 // 指定格口上电
|
||||
const val OPT_POFSELECTECELLS = 11 // 指定格口断电
|
||||
const val OPT_RONSELECTECELLS = 12 // 指定格口红灯亮
|
||||
const val OPT_GONSELECTECELLS = 13 // 指定格口绿灯亮
|
||||
const val OPT_AOFSELECTECELLS = 14 // 指定格口灯灭
|
||||
const val OPT_DEVINFSELECTECELLS = 15 // 指定格口设备信息
|
||||
const val OPT_FIRMWAREVERSION = 16 // 主板固件版本
|
||||
const val OPT_OPENDOORS = 17 // 打开柜门
|
||||
const val OPT_REPAIRCELLOPEN = 18 // 报修格口开门
|
||||
const val OPT_LOSTCELLRON = 19 // 丢失格口红灯
|
||||
const val OPT_DOUBLECHECK = 20 // 检测打开门的柜子格口数据是否异常
|
||||
}
|
||||
406
app/src/main/java/com/example/smarthome/data/DeviceManager.kt
Normal file
@ -0,0 +1,406 @@
|
||||
package com.example.smarthome.data
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 设备管理器 - 整合 MQTT 和 CAN 服务
|
||||
* 提供给 UI 层使用的统一接口
|
||||
*
|
||||
* 使用示例:
|
||||
* ```kotlin
|
||||
* @Composable
|
||||
* fun DeviceControlScreen() {
|
||||
* val deviceManager = rememberDeviceManager()
|
||||
*
|
||||
* // 初始化
|
||||
* LaunchedEffect(Unit) {
|
||||
* deviceManager.initialize()
|
||||
* }
|
||||
*
|
||||
* // 控制设备
|
||||
* Button(onClick = { deviceManager.openDoor("A01") }) {
|
||||
* Text("开门")
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class DeviceManager private constructor(
|
||||
private val context: Context
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "DeviceManager"
|
||||
|
||||
@Volatile
|
||||
private var instance: DeviceManager? = null
|
||||
|
||||
fun getInstance(context: Context): DeviceManager {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: DeviceManager(context.applicationContext).also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 服务
|
||||
private val mqttService: IMqttService = MqttServiceImpl.getInstance(context)
|
||||
private val canService: ICanService = CanServiceImpl.getInstance(context)
|
||||
|
||||
// 协程作用域
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
// 状态
|
||||
private val _isInitialized = MutableStateFlow(false)
|
||||
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
||||
|
||||
private val _deviceSn = MutableStateFlow("")
|
||||
val deviceSn: StateFlow<String> = _deviceSn.asStateFlow()
|
||||
|
||||
// 暴露服务状态
|
||||
val mqttConnectionState = mqttService.connectionState
|
||||
val canConnectionState = canService.isConnected
|
||||
val lastCanResponse = canService.lastResponse
|
||||
|
||||
// 当前操作
|
||||
private val _currentOperation = MutableStateFlow(-1)
|
||||
val currentOperation: StateFlow<Int> = _currentOperation.asStateFlow()
|
||||
|
||||
// 格口列表
|
||||
private val _openCellList = MutableStateFlow<List<String>>(emptyList())
|
||||
val openCellList: StateFlow<List<String>> = _openCellList.asStateFlow()
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
/**
|
||||
* 初始化设备管理器
|
||||
*/
|
||||
fun initialize(
|
||||
deviceSn: String = "",
|
||||
mqttHost: String = "",
|
||||
mqttPassword: String = "",
|
||||
comPath: String = CanConstants.DEFAULT_COM
|
||||
) {
|
||||
Log.d(TAG, "initialize")
|
||||
|
||||
_deviceSn.value = deviceSn
|
||||
|
||||
// 初始化 CAN 串口
|
||||
scope.launch {
|
||||
val canResult = canService.openCom(comPath)
|
||||
Log.d(TAG, "CAN openCom result: $canResult")
|
||||
}
|
||||
|
||||
// 初始化 MQTT(如果配置了)
|
||||
if (mqttHost.isNotEmpty() && deviceSn.isNotEmpty()) {
|
||||
scope.launch {
|
||||
mqttService.mqttInit(mqttHost, deviceSn, mqttPassword)
|
||||
mqttService.mqttConnect()
|
||||
}
|
||||
}
|
||||
|
||||
// 设置回调
|
||||
setupCallbacks()
|
||||
|
||||
_isInitialized.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭设备管理器
|
||||
*/
|
||||
fun shutdown() {
|
||||
Log.d(TAG, "shutdown")
|
||||
canService.closeCom()
|
||||
mqttService.mqttClose()
|
||||
_isInitialized.value = false
|
||||
}
|
||||
|
||||
private fun setupCallbacks() {
|
||||
// CAN 响应回调
|
||||
canService.setResponseCallback { response ->
|
||||
Log.d(TAG, "CAN Response: ${response.cellName} ${response.command} = ${response.value}")
|
||||
handleCanResponse(response)
|
||||
}
|
||||
|
||||
// MQTT 消息回调
|
||||
mqttService.setMessageCallback { topic, message ->
|
||||
Log.d(TAG, "MQTT Message: $topic -> $message")
|
||||
handleMqttMessage(topic, message)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 门锁控制 ====================
|
||||
|
||||
/**
|
||||
* 开门
|
||||
*/
|
||||
fun openDoor(cellName: String) {
|
||||
Log.d(TAG, "openDoor: $cellName")
|
||||
canService.ledGreen(cellName) // 先亮绿灯
|
||||
canService.openDoor(cellName) // 再开锁
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量开门
|
||||
*/
|
||||
fun openDoors(cellNames: List<String>) {
|
||||
Log.d(TAG, "openDoors: $cellNames")
|
||||
_openCellList.value = cellNames
|
||||
cellNames.forEach { cellName ->
|
||||
canService.ledGreen(cellName)
|
||||
}
|
||||
cellNames.forEach { cellName ->
|
||||
canService.openDoor(cellName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询门状态
|
||||
*/
|
||||
fun queryDoorStatus(cellName: String) {
|
||||
canService.queryDoorStatus(cellName)
|
||||
}
|
||||
|
||||
// ==================== 电源控制 ====================
|
||||
|
||||
/**
|
||||
* 格口上电
|
||||
*/
|
||||
fun powerOn(cellName: String) {
|
||||
canService.powerOn(cellName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格口断电
|
||||
*/
|
||||
fun powerOff(cellName: String) {
|
||||
canService.powerOff(cellName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上电
|
||||
*/
|
||||
fun powerOnAll(cellNames: List<String>) {
|
||||
canService.powerOnAll(cellNames)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量断电
|
||||
*/
|
||||
fun powerOffAll(cellNames: List<String>) {
|
||||
canService.powerOffAll(cellNames)
|
||||
}
|
||||
|
||||
// ==================== LED 控制 ====================
|
||||
|
||||
fun ledRed(cellName: String) = canService.ledRed(cellName)
|
||||
fun ledGreen(cellName: String) = canService.ledGreen(cellName)
|
||||
fun ledOff(cellName: String) = canService.ledOff(cellName)
|
||||
fun ledOffAll(cellNames: List<String>) = canService.ledOffAll(cellNames)
|
||||
|
||||
// ==================== 设备信息 ====================
|
||||
|
||||
/**
|
||||
* 读取设备序列号
|
||||
*/
|
||||
fun readDeviceSn(cellName: String) {
|
||||
canService.readDeviceSn(cellName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取设备连接状态
|
||||
*/
|
||||
fun readConnectStatus(cellName: String) {
|
||||
canService.readConnectStatus(cellName)
|
||||
}
|
||||
|
||||
// ==================== 业务操作 ====================
|
||||
|
||||
/**
|
||||
* 借设备
|
||||
*/
|
||||
fun borrowDevice(cellName: String, operator: String, userId: String) {
|
||||
Log.d(TAG, "borrowDevice: $cellName by $operator")
|
||||
_currentOperation.value = OperationType.OPT_BORROW
|
||||
|
||||
// 亮绿灯
|
||||
canService.ledGreen(cellName)
|
||||
// 开门
|
||||
canService.openDoor(cellName)
|
||||
|
||||
// 上报 MQTT(如果连接)
|
||||
if (mqttService.isMqttConnect) {
|
||||
val event = DeviceEvent(
|
||||
opType = "borrow",
|
||||
deviceCode = _deviceSn.value,
|
||||
cabinetName = cellName.take(1),
|
||||
devSn = "",
|
||||
doorName = cellName,
|
||||
devType = "",
|
||||
userId = userId,
|
||||
batchId = generateBatchId(),
|
||||
opTime = getCurrentTime(),
|
||||
doorInfo = DoorInfo(cellName = cellName)
|
||||
)
|
||||
mqttService.publishBorrowdevEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 还设备
|
||||
*/
|
||||
fun returnDevice(cellName: String, operator: String, userId: String) {
|
||||
Log.d(TAG, "returnDevice: $cellName by $operator")
|
||||
_currentOperation.value = OperationType.OPT_RETURN
|
||||
|
||||
// 亮绿灯
|
||||
canService.ledGreen(cellName)
|
||||
// 开门
|
||||
canService.openDoor(cellName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 报修归还
|
||||
*/
|
||||
fun repairReturn(cellName: String, operator: String) {
|
||||
Log.d(TAG, "repairReturn: $cellName")
|
||||
_currentOperation.value = OperationType.OPT_REPAIRDEVIN
|
||||
|
||||
// 亮红灯
|
||||
canService.ledRed(cellName)
|
||||
// 开门
|
||||
canService.openDoor(cellName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束当前操作
|
||||
*/
|
||||
fun endOperation() {
|
||||
Log.d(TAG, "endOperation")
|
||||
_currentOperation.value = -1
|
||||
|
||||
// 关闭所有灯
|
||||
_openCellList.value.forEach { cellName ->
|
||||
canService.ledOff(cellName)
|
||||
}
|
||||
_openCellList.value = emptyList()
|
||||
}
|
||||
|
||||
// ==================== 回调处理 ====================
|
||||
|
||||
private fun handleCanResponse(response: CanResponse) {
|
||||
when (response.command) {
|
||||
CanCommands.LON -> handleDoorOpenResponse(response)
|
||||
CanCommands.LST -> handleDoorStatusResponse(response)
|
||||
CanCommands.DSN -> handleDeviceSnResponse(response)
|
||||
CanCommands.CON -> handleConnectStatusResponse(response)
|
||||
CanCommands.PVD -> handlePvdResponse(response)
|
||||
CanCommands.MFG -> handleMfgResponse(response)
|
||||
CanCommands.PRO -> handleProResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDoorOpenResponse(response: CanResponse) {
|
||||
if (response.value == CanConstants.DOOR_OPEN_STATUS) {
|
||||
Log.d(TAG, "${response.cellName} 开门成功")
|
||||
// 开始监测门状态
|
||||
canService.queryDoorStatus(response.cellName)
|
||||
} else {
|
||||
Log.d(TAG, "${response.cellName} 开门失败")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDoorStatusResponse(response: CanResponse) {
|
||||
if (response.value == CanConstants.DOOR_CLOSE_STATUS) {
|
||||
Log.d(TAG, "${response.cellName} 门已关闭")
|
||||
// 门关闭后的处理
|
||||
canService.ledOff(response.cellName)
|
||||
|
||||
// 如果是还设备操作,读取设备SN
|
||||
if (_currentOperation.value == OperationType.OPT_RETURN ||
|
||||
_currentOperation.value == OperationType.OPT_BATCHIN) {
|
||||
canService.readDeviceSn(response.cellName)
|
||||
}
|
||||
} else {
|
||||
// 门还开着,继续监测
|
||||
canService.queryDoorStatus(response.cellName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeviceSnResponse(response: CanResponse) {
|
||||
Log.d(TAG, "${response.cellName} 设备SN: ${response.value}")
|
||||
// 上报设备信息到 MQTT
|
||||
if (mqttService.isMqttConnect) {
|
||||
val doorInfo = DoorInfo(
|
||||
cellName = response.cellName,
|
||||
devSn = response.value
|
||||
)
|
||||
mqttService.publishDb(doorInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConnectStatusResponse(response: CanResponse) {
|
||||
val connected = response.value == "1"
|
||||
Log.d(TAG, "${response.cellName} 设备连接状态: $connected")
|
||||
}
|
||||
|
||||
private fun handlePvdResponse(response: CanResponse) {
|
||||
Log.d(TAG, "${response.cellName} PVD: ${response.value}")
|
||||
}
|
||||
|
||||
private fun handleMfgResponse(response: CanResponse) {
|
||||
Log.d(TAG, "${response.cellName} MFG: ${response.value}")
|
||||
}
|
||||
|
||||
private fun handleProResponse(response: CanResponse) {
|
||||
Log.d(TAG, "${response.cellName} PRO: ${response.value}")
|
||||
}
|
||||
|
||||
private fun handleMqttMessage(topic: String, message: String) {
|
||||
// 处理 MQTT 下发的命令
|
||||
// 根据 topic 和 message 执行相应操作
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
private fun generateBatchId(): String {
|
||||
return (1..15).map { (0..9).random() }.joinToString("")
|
||||
}
|
||||
|
||||
private fun getCurrentTime(): String {
|
||||
val sdf = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||
return sdf.format(java.util.Date())
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Compose 集成 ====================
|
||||
|
||||
@Composable
|
||||
fun rememberDeviceManager(): DeviceManager {
|
||||
val context = LocalContext.current
|
||||
return remember { DeviceManager.getInstance(context) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberMqttConnectionState(): MqttConnectionState {
|
||||
val manager = rememberDeviceManager()
|
||||
val state by manager.mqttConnectionState.collectAsState()
|
||||
return state
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberCanConnectionState(): Boolean {
|
||||
val manager = rememberDeviceManager()
|
||||
val state by manager.canConnectionState.collectAsState()
|
||||
return state
|
||||
}
|
||||
437
app/src/main/java/com/example/smarthome/data/LanguageManager.kt
Normal file
@ -0,0 +1,437 @@
|
||||
package com.example.smarthome.data
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* 语言管理器 - 管理应用多语言
|
||||
*/
|
||||
object LanguageManager {
|
||||
|
||||
// 支持的语言
|
||||
enum class Language(val code: String, val displayName: String, val nativeName: String) {
|
||||
CHINESE("zh", "中文", "简体中文"),
|
||||
ENGLISH("en", "English", "English"),
|
||||
KOREAN("ko", "한국어", "한국어"),
|
||||
JAPANESE("ja", "日本語", "日本語"),
|
||||
RUSSIAN("ru", "Русский", "Русский")
|
||||
}
|
||||
|
||||
private val _currentLanguage = MutableStateFlow(Language.CHINESE)
|
||||
val currentLanguage: StateFlow<Language> = _currentLanguage
|
||||
|
||||
private const val PREFS_NAME = "language_prefs"
|
||||
private const val KEY_LANGUAGE = "selected_language"
|
||||
|
||||
fun init(context: Context) {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val savedCode = prefs.getString(KEY_LANGUAGE, Language.CHINESE.code) ?: Language.CHINESE.code
|
||||
_currentLanguage.value = Language.entries.find { it.code == savedCode } ?: Language.CHINESE
|
||||
}
|
||||
|
||||
fun setLanguage(context: Context, language: Language) {
|
||||
val currentLang = _currentLanguage.value
|
||||
if (currentLang == language) return // 语言没变,不需要重启
|
||||
|
||||
// 先同步保存语言设置(使用commit确保保存完成后再重启)
|
||||
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString(KEY_LANGUAGE, language.code)
|
||||
.commit() // 使用commit同步保存,而不是apply异步保存
|
||||
|
||||
_currentLanguage.value = language
|
||||
|
||||
// 重启应用
|
||||
restartApp(context)
|
||||
}
|
||||
|
||||
private fun restartApp(context: Context) {
|
||||
val packageManager = context.packageManager
|
||||
val intent = packageManager.getLaunchIntentForPackage(context.packageName)
|
||||
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
context.startActivity(intent)
|
||||
if (context is Activity) {
|
||||
context.finish()
|
||||
}
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
|
||||
// 获取翻译文本
|
||||
fun getString(key: String): String {
|
||||
return translations[_currentLanguage.value]?.get(key) ?: translations[Language.CHINESE]?.get(key) ?: key
|
||||
}
|
||||
|
||||
// 翻译字典
|
||||
private val translations: Map<Language, Map<String, String>> = mapOf(
|
||||
Language.CHINESE to mapOf(
|
||||
// 导航
|
||||
"nav_dashboard" to "控制台",
|
||||
"nav_scene" to "场景",
|
||||
"nav_automation" to "自动化",
|
||||
"nav_statistics" to "统计",
|
||||
"nav_security" to "安全",
|
||||
"nav_settings" to "设置",
|
||||
|
||||
// 设置页面
|
||||
"settings_title" to "设置",
|
||||
"settings_account" to "账户设置",
|
||||
"settings_profile" to "个人信息",
|
||||
"settings_profile_desc" to "管理您的个人资料",
|
||||
"settings_security" to "账户安全",
|
||||
"settings_security_desc" to "密码和安全设置",
|
||||
"settings_device" to "设备管理",
|
||||
"settings_auto_connect" to "自动连接",
|
||||
"settings_auto_connect_desc" to "自动连接到家庭网络",
|
||||
"settings_notifications" to "设备通知",
|
||||
"settings_notifications_desc" to "接收设备状态通知",
|
||||
"settings_display" to "显示设置",
|
||||
"settings_background" to "背景壁纸",
|
||||
"settings_dark_mode" to "深色模式",
|
||||
"settings_dark_mode_desc" to "使用深色主题",
|
||||
"settings_language" to "语言",
|
||||
"settings_about" to "关于",
|
||||
"settings_version" to "版本信息",
|
||||
"settings_privacy" to "隐私政策",
|
||||
"settings_privacy_desc" to "查看隐私政策",
|
||||
"settings_terms" to "用户协议",
|
||||
"settings_terms_desc" to "查看用户协议",
|
||||
"settings_logout" to "退出登录",
|
||||
|
||||
// 控制台
|
||||
"dashboard_title" to "控制台",
|
||||
"room_all" to "全屋",
|
||||
"room_living" to "客厅",
|
||||
"room_bedroom" to "卧室",
|
||||
"room_kitchen" to "厨房",
|
||||
"room_bathroom" to "浴室",
|
||||
|
||||
// 空调
|
||||
"ac_title" to "空调",
|
||||
"ac_mode_cool" to "制冷",
|
||||
"ac_mode_heat" to "制热",
|
||||
"ac_mode_dry" to "除湿",
|
||||
"ac_mode_fan" to "送风",
|
||||
"ac_fan_speed" to "风速",
|
||||
"ac_fan_auto" to "自动",
|
||||
"ac_fan_low" to "低",
|
||||
"ac_fan_medium" to "中",
|
||||
"ac_fan_high" to "高",
|
||||
|
||||
// 灯光
|
||||
"light_title" to "灯光",
|
||||
"light_brightness" to "亮度",
|
||||
|
||||
// 窗帘
|
||||
"curtain_title" to "窗帘",
|
||||
|
||||
// 天气
|
||||
"weather_humidity" to "湿度",
|
||||
|
||||
// 通用
|
||||
"add_device" to "+ 添加设备",
|
||||
"on" to "开",
|
||||
"off" to "关",
|
||||
"cancel" to "取消",
|
||||
"confirm" to "确认",
|
||||
"select_language" to "选择语言"
|
||||
),
|
||||
|
||||
Language.ENGLISH to mapOf(
|
||||
// Navigation
|
||||
"nav_dashboard" to "Dashboard",
|
||||
"nav_scene" to "Scenes",
|
||||
"nav_automation" to "Automation",
|
||||
"nav_statistics" to "Statistics",
|
||||
"nav_security" to "Security",
|
||||
"nav_settings" to "Settings",
|
||||
|
||||
// Settings
|
||||
"settings_title" to "Settings",
|
||||
"settings_account" to "Account Settings",
|
||||
"settings_profile" to "Profile",
|
||||
"settings_profile_desc" to "Manage your profile",
|
||||
"settings_security" to "Account Security",
|
||||
"settings_security_desc" to "Password and security settings",
|
||||
"settings_device" to "Device Management",
|
||||
"settings_auto_connect" to "Auto Connect",
|
||||
"settings_auto_connect_desc" to "Auto connect to home network",
|
||||
"settings_notifications" to "Device Notifications",
|
||||
"settings_notifications_desc" to "Receive device status notifications",
|
||||
"settings_display" to "Display Settings",
|
||||
"settings_background" to "Background",
|
||||
"settings_dark_mode" to "Dark Mode",
|
||||
"settings_dark_mode_desc" to "Use dark theme",
|
||||
"settings_language" to "Language",
|
||||
"settings_about" to "About",
|
||||
"settings_version" to "Version",
|
||||
"settings_privacy" to "Privacy Policy",
|
||||
"settings_privacy_desc" to "View privacy policy",
|
||||
"settings_terms" to "Terms of Service",
|
||||
"settings_terms_desc" to "View terms of service",
|
||||
"settings_logout" to "Log Out",
|
||||
|
||||
// Dashboard
|
||||
"dashboard_title" to "Dashboard",
|
||||
"room_all" to "All Rooms",
|
||||
"room_living" to "Living Room",
|
||||
"room_bedroom" to "Bedroom",
|
||||
"room_kitchen" to "Kitchen",
|
||||
"room_bathroom" to "Bathroom",
|
||||
|
||||
// AC
|
||||
"ac_title" to "Air Conditioner",
|
||||
"ac_mode_cool" to "Cool",
|
||||
"ac_mode_heat" to "Heat",
|
||||
"ac_mode_dry" to "Dry",
|
||||
"ac_mode_fan" to "Fan",
|
||||
"ac_fan_speed" to "Fan Speed",
|
||||
"ac_fan_auto" to "Auto",
|
||||
"ac_fan_low" to "Low",
|
||||
"ac_fan_medium" to "Med",
|
||||
"ac_fan_high" to "High",
|
||||
|
||||
// Light
|
||||
"light_title" to "Light",
|
||||
"light_brightness" to "Brightness",
|
||||
|
||||
// Curtain
|
||||
"curtain_title" to "Curtain",
|
||||
|
||||
// Weather
|
||||
"weather_humidity" to "Humidity",
|
||||
|
||||
// Common
|
||||
"add_device" to "+ Add Device",
|
||||
"on" to "On",
|
||||
"off" to "Off",
|
||||
"cancel" to "Cancel",
|
||||
"confirm" to "Confirm",
|
||||
"select_language" to "Select Language"
|
||||
),
|
||||
|
||||
Language.KOREAN to mapOf(
|
||||
// 네비게이션
|
||||
"nav_dashboard" to "대시보드",
|
||||
"nav_scene" to "장면",
|
||||
"nav_automation" to "자동화",
|
||||
"nav_statistics" to "통계",
|
||||
"nav_security" to "보안",
|
||||
"nav_settings" to "설정",
|
||||
|
||||
// 설정
|
||||
"settings_title" to "설정",
|
||||
"settings_account" to "계정 설정",
|
||||
"settings_profile" to "프로필",
|
||||
"settings_profile_desc" to "프로필 관리",
|
||||
"settings_security" to "계정 보안",
|
||||
"settings_security_desc" to "비밀번호 및 보안 설정",
|
||||
"settings_device" to "기기 관리",
|
||||
"settings_auto_connect" to "자동 연결",
|
||||
"settings_auto_connect_desc" to "홈 네트워크에 자동 연결",
|
||||
"settings_notifications" to "기기 알림",
|
||||
"settings_notifications_desc" to "기기 상태 알림 받기",
|
||||
"settings_display" to "디스플레이 설정",
|
||||
"settings_background" to "배경화면",
|
||||
"settings_dark_mode" to "다크 모드",
|
||||
"settings_dark_mode_desc" to "다크 테마 사용",
|
||||
"settings_language" to "언어",
|
||||
"settings_about" to "정보",
|
||||
"settings_version" to "버전 정보",
|
||||
"settings_privacy" to "개인정보 처리방침",
|
||||
"settings_privacy_desc" to "개인정보 처리방침 보기",
|
||||
"settings_terms" to "이용약관",
|
||||
"settings_terms_desc" to "이용약관 보기",
|
||||
"settings_logout" to "로그아웃",
|
||||
|
||||
// 대시보드
|
||||
"dashboard_title" to "대시보드",
|
||||
"room_all" to "전체",
|
||||
"room_living" to "거실",
|
||||
"room_bedroom" to "침실",
|
||||
"room_kitchen" to "주방",
|
||||
"room_bathroom" to "욕실",
|
||||
|
||||
// 에어컨
|
||||
"ac_title" to "에어컨",
|
||||
"ac_mode_cool" to "냉방",
|
||||
"ac_mode_heat" to "난방",
|
||||
"ac_mode_dry" to "제습",
|
||||
"ac_mode_fan" to "송풍",
|
||||
"ac_fan_speed" to "풍속",
|
||||
"ac_fan_auto" to "자동",
|
||||
"ac_fan_low" to "약",
|
||||
"ac_fan_medium" to "중",
|
||||
"ac_fan_high" to "강",
|
||||
|
||||
// 조명
|
||||
"light_title" to "조명",
|
||||
"light_brightness" to "밝기",
|
||||
|
||||
// 커튼
|
||||
"curtain_title" to "커튼",
|
||||
|
||||
// 날씨
|
||||
"weather_humidity" to "습도",
|
||||
|
||||
// 공통
|
||||
"add_device" to "+ 기기 추가",
|
||||
"on" to "켜짐",
|
||||
"off" to "꺼짐",
|
||||
"cancel" to "취소",
|
||||
"confirm" to "확인",
|
||||
"select_language" to "언어 선택"
|
||||
),
|
||||
|
||||
Language.JAPANESE to mapOf(
|
||||
// ナビゲーション
|
||||
"nav_dashboard" to "ダッシュボード",
|
||||
"nav_scene" to "シーン",
|
||||
"nav_automation" to "オートメーション",
|
||||
"nav_statistics" to "統計",
|
||||
"nav_security" to "セキュリティ",
|
||||
"nav_settings" to "設定",
|
||||
|
||||
// 設定
|
||||
"settings_title" to "設定",
|
||||
"settings_account" to "アカウント設定",
|
||||
"settings_profile" to "プロフィール",
|
||||
"settings_profile_desc" to "プロフィールを管理",
|
||||
"settings_security" to "アカウントセキュリティ",
|
||||
"settings_security_desc" to "パスワードとセキュリティ設定",
|
||||
"settings_device" to "デバイス管理",
|
||||
"settings_auto_connect" to "自動接続",
|
||||
"settings_auto_connect_desc" to "ホームネットワークに自動接続",
|
||||
"settings_notifications" to "デバイス通知",
|
||||
"settings_notifications_desc" to "デバイス状態の通知を受信",
|
||||
"settings_display" to "表示設定",
|
||||
"settings_background" to "背景",
|
||||
"settings_dark_mode" to "ダークモード",
|
||||
"settings_dark_mode_desc" to "ダークテーマを使用",
|
||||
"settings_language" to "言語",
|
||||
"settings_about" to "情報",
|
||||
"settings_version" to "バージョン情報",
|
||||
"settings_privacy" to "プライバシーポリシー",
|
||||
"settings_privacy_desc" to "プライバシーポリシーを見る",
|
||||
"settings_terms" to "利用規約",
|
||||
"settings_terms_desc" to "利用規約を見る",
|
||||
"settings_logout" to "ログアウト",
|
||||
|
||||
// ダッシュボード
|
||||
"dashboard_title" to "ダッシュボード",
|
||||
"room_all" to "全室",
|
||||
"room_living" to "リビング",
|
||||
"room_bedroom" to "寝室",
|
||||
"room_kitchen" to "キッチン",
|
||||
"room_bathroom" to "バスルーム",
|
||||
|
||||
// エアコン
|
||||
"ac_title" to "エアコン",
|
||||
"ac_mode_cool" to "冷房",
|
||||
"ac_mode_heat" to "暖房",
|
||||
"ac_mode_dry" to "除湿",
|
||||
"ac_mode_fan" to "送風",
|
||||
"ac_fan_speed" to "風速",
|
||||
"ac_fan_auto" to "自動",
|
||||
"ac_fan_low" to "弱",
|
||||
"ac_fan_medium" to "中",
|
||||
"ac_fan_high" to "強",
|
||||
|
||||
// 照明
|
||||
"light_title" to "照明",
|
||||
"light_brightness" to "明るさ",
|
||||
|
||||
// カーテン
|
||||
"curtain_title" to "カーテン",
|
||||
|
||||
// 天気
|
||||
"weather_humidity" to "湿度",
|
||||
|
||||
// 共通
|
||||
"add_device" to "+ デバイス追加",
|
||||
"on" to "オン",
|
||||
"off" to "オフ",
|
||||
"cancel" to "キャンセル",
|
||||
"confirm" to "確認",
|
||||
"select_language" to "言語を選択"
|
||||
),
|
||||
|
||||
Language.RUSSIAN to mapOf(
|
||||
// Навигация
|
||||
"nav_dashboard" to "Панель",
|
||||
"nav_scene" to "Сцены",
|
||||
"nav_automation" to "Автоматизация",
|
||||
"nav_statistics" to "Статистика",
|
||||
"nav_security" to "Безопасность",
|
||||
"nav_settings" to "Настройки",
|
||||
|
||||
// Настройки
|
||||
"settings_title" to "Настройки",
|
||||
"settings_account" to "Настройки аккаунта",
|
||||
"settings_profile" to "Профиль",
|
||||
"settings_profile_desc" to "Управление профилем",
|
||||
"settings_security" to "Безопасность аккаунта",
|
||||
"settings_security_desc" to "Пароль и настройки безопасности",
|
||||
"settings_device" to "Управление устройствами",
|
||||
"settings_auto_connect" to "Автоподключение",
|
||||
"settings_auto_connect_desc" to "Автоподключение к домашней сети",
|
||||
"settings_notifications" to "Уведомления",
|
||||
"settings_notifications_desc" to "Получать уведомления о состоянии",
|
||||
"settings_display" to "Настройки дисплея",
|
||||
"settings_background" to "Фон",
|
||||
"settings_dark_mode" to "Тёмный режим",
|
||||
"settings_dark_mode_desc" to "Использовать тёмную тему",
|
||||
"settings_language" to "Язык",
|
||||
"settings_about" to "О приложении",
|
||||
"settings_version" to "Версия",
|
||||
"settings_privacy" to "Политика конфиденциальности",
|
||||
"settings_privacy_desc" to "Просмотреть политику",
|
||||
"settings_terms" to "Условия использования",
|
||||
"settings_terms_desc" to "Просмотреть условия",
|
||||
"settings_logout" to "Выйти",
|
||||
|
||||
// Панель
|
||||
"dashboard_title" to "Панель",
|
||||
"room_all" to "Все комнаты",
|
||||
"room_living" to "Гостиная",
|
||||
"room_bedroom" to "Спальня",
|
||||
"room_kitchen" to "Кухня",
|
||||
"room_bathroom" to "Ванная",
|
||||
|
||||
// Кондиционер
|
||||
"ac_title" to "Кондиционер",
|
||||
"ac_mode_cool" to "Охлаждение",
|
||||
"ac_mode_heat" to "Обогрев",
|
||||
"ac_mode_dry" to "Осушение",
|
||||
"ac_mode_fan" to "Вентиляция",
|
||||
"ac_fan_speed" to "Скорость",
|
||||
"ac_fan_auto" to "Авто",
|
||||
"ac_fan_low" to "Низ",
|
||||
"ac_fan_medium" to "Сред",
|
||||
"ac_fan_high" to "Выс",
|
||||
|
||||
// Освещение
|
||||
"light_title" to "Свет",
|
||||
"light_brightness" to "Яркость",
|
||||
|
||||
// Шторы
|
||||
"curtain_title" to "Шторы",
|
||||
|
||||
// Погода
|
||||
"weather_humidity" to "Влажность",
|
||||
|
||||
// Общее
|
||||
"add_device" to "+ Добавить",
|
||||
"on" to "Вкл",
|
||||
"off" to "Выкл",
|
||||
"cancel" to "Отмена",
|
||||
"confirm" to "ОК",
|
||||
"select_language" to "Выбрать язык"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// 便捷函数
|
||||
fun tr(key: String): String = LanguageManager.getString(key)
|
||||
486
app/src/main/java/com/example/smarthome/data/MqttService.kt
Normal file
@ -0,0 +1,486 @@
|
||||
package com.example.smarthome.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.json.JSONObject
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
/**
|
||||
* MQTT 服务
|
||||
*
|
||||
* 依赖库(需要在 build.gradle 中添加):
|
||||
* implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
|
||||
* implementation("org.eclipse.paho:org.eclipse.paho.android.service:1.1.1")
|
||||
*
|
||||
* AndroidManifest.xml 需要添加:
|
||||
* <service android:name="org.eclipse.paho.android.service.MqttService" />
|
||||
*/
|
||||
|
||||
// ==================== 常量定义(与原代码一致)====================
|
||||
|
||||
object MqttConstants {
|
||||
const val TAG = "MqttService"
|
||||
|
||||
// 默认服务器配置
|
||||
const val HOST_DOMAIN = "mqtt.cd-iot.cn"
|
||||
const val HOST_DOMAIN_API = "https://www.cd-iot.cn"
|
||||
const val HOST_PORT = ":1884"
|
||||
|
||||
// 主题前缀
|
||||
const val PUBLISH_HEAD = "bcup/0jnqd7slms9vvaf8/"
|
||||
const val PUBLISH_TAIL_EVENT = "/event"
|
||||
const val PUBLISH_TAIL_PROP = "/prop"
|
||||
const val PUBLISH_TAIL_UPDATEREPLY = "/upgrade/reply"
|
||||
const val PUBLISH_TAIL_MESSAGEREPLY = "/message/reply"
|
||||
|
||||
const val SUBSCRIBE_HEAD = "bcdown/0jnqd7slms9vvaf8/"
|
||||
const val SUBSCRIBE_TAIL_UPDATE = "/upgrade/set"
|
||||
const val SUBSCRIBE_TAIL_PROPERTYSET = "/property/set"
|
||||
const val SUBSCRIBE_TAIL_SERVER = "/service/invoke"
|
||||
|
||||
// QoS 等级
|
||||
const val QOS_DEFAULT = 2
|
||||
}
|
||||
|
||||
// ==================== 数据模型 ====================
|
||||
|
||||
/**
|
||||
* MQTT 连接状态
|
||||
*/
|
||||
enum class MqttConnectionState {
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
CONNECTION_LOST
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备信息(与原代码 Door 实体对应)
|
||||
*/
|
||||
data class DoorInfo(
|
||||
val cellName: String = "", // 格口名称
|
||||
val cabinetName: String = "", // 柜子名称
|
||||
val cellState: String = "", // 格口状态
|
||||
val cellVoltageType: String = "", // 电压类型
|
||||
val cellConnectorType: String = "", // 接口类型
|
||||
val cellRepairType: String = "", // 格口维修类型
|
||||
val devType: String = "", // 设备类型
|
||||
val devCheckType: String = "", // 设备检测类型
|
||||
val devSn: String = "", // 设备序列号
|
||||
val devPvd: String = "", // 设备PID/VID
|
||||
val devPro: String = "", // 设备产品名
|
||||
val devMfg: String = "", // 设备制造商
|
||||
val devPwr: String = "", // 设备电量
|
||||
val devState: String = "", // 设备状态
|
||||
val devRepairType: String = "" // 设备维修类型
|
||||
)
|
||||
|
||||
/**
|
||||
* 借还设备事件
|
||||
*/
|
||||
data class DeviceEvent(
|
||||
val opType: String, // 操作类型
|
||||
val deviceCode: String, // 设备编码(柜子SN)
|
||||
val cabinetName: String, // 柜子名称
|
||||
val devSn: String, // 设备序列号
|
||||
val doorName: String, // 格口名称
|
||||
val devType: String, // 设备类型
|
||||
val userId: String, // 用户ID
|
||||
val batchId: String, // 批次ID
|
||||
val opTime: String, // 操作时间
|
||||
val doorInfo: DoorInfo // 格口信息
|
||||
)
|
||||
|
||||
// ==================== MQTT 服务接口 ====================
|
||||
|
||||
interface IMqttService {
|
||||
val connectionState: StateFlow<MqttConnectionState>
|
||||
val isMqttConnect: Boolean
|
||||
|
||||
// 初始化和连接
|
||||
fun mqttInit(hostIp: String, deviceSn: String, devicePassword: String)
|
||||
fun mqttConnect()
|
||||
fun mqttClose()
|
||||
|
||||
// 订阅
|
||||
fun subscribeUpdate()
|
||||
fun subscribeLog()
|
||||
fun subscribeUploadLog()
|
||||
|
||||
// 发布 - 设备状态同步
|
||||
fun publishDb(doorInfo: DoorInfo)
|
||||
fun publishBorrowdevEvent(event: DeviceEvent)
|
||||
fun publishReturndevEvent(event: DeviceEvent)
|
||||
fun publishLosedevEvent(event: DeviceEvent)
|
||||
|
||||
// 发布 - 系统消息
|
||||
fun publishSoftwareVersion(version: String)
|
||||
fun publishDownLoadReply(code: String, messageId: String, taskId: String)
|
||||
fun publishLogupload(messageId: String, filename: String, fileUrl: String)
|
||||
fun publishInitDeviceReply(messageId: String, opType: String)
|
||||
fun publishCellSetReply(messageId: String)
|
||||
|
||||
// 回调设置
|
||||
fun setMessageCallback(callback: (topic: String, message: String) -> Unit)
|
||||
fun setConnectionCallback(callback: (MqttConnectionState) -> Unit)
|
||||
}
|
||||
|
||||
// ==================== MQTT 服务实现(模拟)====================
|
||||
|
||||
class MqttServiceImpl private constructor(
|
||||
private val context: Context
|
||||
) : IMqttService {
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var instance: MqttServiceImpl? = null
|
||||
|
||||
fun getInstance(context: Context): MqttServiceImpl {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: MqttServiceImpl(context.applicationContext).also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 状态
|
||||
private val _connectionState = MutableStateFlow(MqttConnectionState.DISCONNECTED)
|
||||
override val connectionState: StateFlow<MqttConnectionState> = _connectionState.asStateFlow()
|
||||
override val isMqttConnect: Boolean get() = _connectionState.value == MqttConnectionState.CONNECTED
|
||||
|
||||
// 配置
|
||||
private var hostIp: String = ""
|
||||
private var deviceSn: String = ""
|
||||
private var clientId: String = ""
|
||||
private var username: String = ""
|
||||
private var password: String = ""
|
||||
|
||||
// 主题
|
||||
private var topicPublishDb: String = ""
|
||||
private var topicPublishSoftwareVersion: String = ""
|
||||
private var topicPublishUpdateReply: String = ""
|
||||
private var topicPublishLogUpload: String = ""
|
||||
private var topicPublishInitDeviceReply: String = ""
|
||||
private var topicPublishBorrowDev: String = ""
|
||||
private var topicPublishReturnDev: String = ""
|
||||
private var topicPublishLoseDev: String = ""
|
||||
private var topicSubscribeUpdate: String = ""
|
||||
private var topicSubscribePropertySet: String = ""
|
||||
private var topicSubscribeServer: String = ""
|
||||
|
||||
// 回调
|
||||
private var messageCallback: ((String, String) -> Unit)? = null
|
||||
private var connectionCallback: ((MqttConnectionState) -> Unit)? = null
|
||||
|
||||
// SharedPreferences
|
||||
private val sharedPreferences: SharedPreferences by lazy {
|
||||
context.getSharedPreferences("mqtt_prefs", Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
// TODO: 真实 MQTT 客户端
|
||||
// private var mqttAndroidClient: MqttAndroidClient? = null
|
||||
// private var mqttConnectOptions: MqttConnectOptions? = null
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
override fun mqttInit(hostIp: String, deviceSn: String, devicePassword: String) {
|
||||
if (hostIp.isEmpty()) {
|
||||
Log.w(MqttConstants.TAG, "mqttInit: hostIp is empty")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(MqttConstants.TAG, "mqttInit")
|
||||
this.hostIp = hostIp
|
||||
this.deviceSn = deviceSn
|
||||
|
||||
// 初始化主题(与原代码一致)
|
||||
topicPublishDb = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_EVENT}"
|
||||
topicPublishSoftwareVersion = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_PROP}"
|
||||
topicPublishUpdateReply = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_UPDATEREPLY}"
|
||||
topicPublishLogUpload = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_MESSAGEREPLY}"
|
||||
topicPublishInitDeviceReply = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_MESSAGEREPLY}"
|
||||
topicPublishBorrowDev = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_EVENT}"
|
||||
topicPublishReturnDev = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_EVENT}"
|
||||
topicPublishLoseDev = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_EVENT}"
|
||||
topicSubscribeUpdate = "${MqttConstants.SUBSCRIBE_HEAD}$deviceSn${MqttConstants.SUBSCRIBE_TAIL_UPDATE}"
|
||||
topicSubscribePropertySet = "${MqttConstants.SUBSCRIBE_HEAD}$deviceSn${MqttConstants.SUBSCRIBE_TAIL_PROPERTYSET}"
|
||||
topicSubscribeServer = "${MqttConstants.SUBSCRIBE_HEAD}$deviceSn${MqttConstants.SUBSCRIBE_TAIL_SERVER}"
|
||||
|
||||
// 生成认证信息(与原代码一致)
|
||||
clientId = deviceSn
|
||||
username = "$deviceSn|${System.currentTimeMillis()}"
|
||||
password = md5("$username|$devicePassword")
|
||||
|
||||
Log.d(MqttConstants.TAG, "CLIENTID: $clientId")
|
||||
Log.d(MqttConstants.TAG, "USERNAME: $username")
|
||||
|
||||
val serverUri = "tcp://$hostIp${MqttConstants.HOST_PORT}"
|
||||
Log.d(MqttConstants.TAG, "serverURI: $serverUri")
|
||||
|
||||
// TODO: 初始化真实 MQTT 客户端
|
||||
// mqttAndroidClient = MqttAndroidClient(context, serverUri, clientId)
|
||||
// mqttAndroidClient?.setCallback(mqttCallback)
|
||||
// mqttConnectOptions = MqttConnectOptions().apply {
|
||||
// isCleanSession = true
|
||||
// connectionTimeout = 10
|
||||
// keepAliveInterval = 20
|
||||
// userName = this@MqttServiceImpl.username
|
||||
// password = this@MqttServiceImpl.password.toCharArray()
|
||||
// }
|
||||
}
|
||||
|
||||
override fun mqttConnect() {
|
||||
Log.d(MqttConstants.TAG, "mqttConnect")
|
||||
_connectionState.value = MqttConnectionState.CONNECTING
|
||||
connectionCallback?.invoke(MqttConnectionState.CONNECTING)
|
||||
|
||||
// TODO: 真实连接
|
||||
// try {
|
||||
// mqttAndroidClient?.connect(mqttConnectOptions, null, object : IMqttActionListener {
|
||||
// override fun onSuccess(asyncActionToken: IMqttToken?) {
|
||||
// Log.d(TAG, "MQTT连接成功!")
|
||||
// _connectionState.value = MqttConnectionState.CONNECTED
|
||||
// connectionCallback?.invoke(MqttConnectionState.CONNECTED)
|
||||
// publishSoftwareVersion(getSoftwareVersion())
|
||||
// subscribeUpdate()
|
||||
// subscribeLog()
|
||||
// subscribeUploadLog()
|
||||
// }
|
||||
// override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
|
||||
// Log.d(TAG, "MQTT连接失败!")
|
||||
// mqttClose()
|
||||
// }
|
||||
// })
|
||||
// } catch (e: MqttException) {
|
||||
// e.printStackTrace()
|
||||
// }
|
||||
|
||||
// 模拟连接成功
|
||||
_connectionState.value = MqttConnectionState.CONNECTED
|
||||
connectionCallback?.invoke(MqttConnectionState.CONNECTED)
|
||||
sharedPreferences.edit().putBoolean("connected", true).apply()
|
||||
}
|
||||
|
||||
override fun mqttClose() {
|
||||
Log.d(MqttConstants.TAG, "mqttClose")
|
||||
_connectionState.value = MqttConnectionState.DISCONNECTED
|
||||
connectionCallback?.invoke(MqttConnectionState.DISCONNECTED)
|
||||
sharedPreferences.edit().putBoolean("connected", false).apply()
|
||||
|
||||
// TODO: 断开真实连接
|
||||
// try {
|
||||
// mqttAndroidClient?.disconnect()
|
||||
// } catch (e: MqttException) {
|
||||
// e.printStackTrace()
|
||||
// }
|
||||
}
|
||||
|
||||
// ==================== 订阅 ====================
|
||||
|
||||
override fun subscribeUpdate() {
|
||||
if (!isMqttConnect) return
|
||||
Log.d(MqttConstants.TAG, "subscribeUpdate: $topicSubscribeUpdate")
|
||||
// TODO: mqttAndroidClient?.subscribe(topicSubscribeUpdate, MqttConstants.QOS_DEFAULT)
|
||||
}
|
||||
|
||||
override fun subscribeLog() {
|
||||
if (!isMqttConnect) return
|
||||
Log.d(MqttConstants.TAG, "subscribeLog: $topicSubscribePropertySet")
|
||||
// TODO: mqttAndroidClient?.subscribe(topicSubscribePropertySet, MqttConstants.QOS_DEFAULT)
|
||||
}
|
||||
|
||||
override fun subscribeUploadLog() {
|
||||
if (!isMqttConnect) return
|
||||
Log.d(MqttConstants.TAG, "subscribeUploadLog: $topicSubscribeServer")
|
||||
// TODO: mqttAndroidClient?.subscribe(topicSubscribeServer, MqttConstants.QOS_DEFAULT)
|
||||
}
|
||||
|
||||
// ==================== 发布 - 设备状态 ====================
|
||||
|
||||
override fun publishDb(doorInfo: DoorInfo) {
|
||||
val message = buildDoorInfoSyncEvent(doorInfo)
|
||||
publish(topicPublishDb, message)
|
||||
}
|
||||
|
||||
override fun publishBorrowdevEvent(event: DeviceEvent) {
|
||||
val message = buildBorrowDeviceEvent(event)
|
||||
publish(topicPublishBorrowDev, message)
|
||||
}
|
||||
|
||||
override fun publishReturndevEvent(event: DeviceEvent) {
|
||||
val message = buildReturnDeviceEvent(event)
|
||||
publish(topicPublishReturnDev, message)
|
||||
}
|
||||
|
||||
override fun publishLosedevEvent(event: DeviceEvent) {
|
||||
val message = buildLoseDeviceEvent(event)
|
||||
publish(topicPublishLoseDev, message)
|
||||
}
|
||||
|
||||
// ==================== 发布 - 系统消息 ====================
|
||||
|
||||
override fun publishSoftwareVersion(version: String) {
|
||||
val message = """{"identifier":"firmware_version","firmware_version":"$version"}"""
|
||||
publish(topicPublishSoftwareVersion, message)
|
||||
}
|
||||
|
||||
override fun publishDownLoadReply(code: String, messageId: String, taskId: String) {
|
||||
val replyMsg = when (code) {
|
||||
"0" -> "等待升级"
|
||||
"1" -> "已发送设备"
|
||||
"2" -> "设备收到"
|
||||
"3" -> "升级成功"
|
||||
"4" -> "升级失败"
|
||||
"5" -> "设备离线停止推送"
|
||||
else -> "未知错误码"
|
||||
}
|
||||
val message = """{"messageId":"$messageId","result":{"code":"$code","msg":"$replyMsg","taskId":"$taskId"}}"""
|
||||
publish(topicPublishUpdateReply, message)
|
||||
}
|
||||
|
||||
override fun publishLogupload(messageId: String, filename: String, fileUrl: String) {
|
||||
val message = """{"messageId":"$messageId","result":{"code":"200","msg":{"type":"upload_log_reply","fileName":"$filename","fileUrl":"$fileUrl"}}}"""
|
||||
publish(topicPublishLogUpload, message)
|
||||
}
|
||||
|
||||
override fun publishInitDeviceReply(messageId: String, opType: String) {
|
||||
val dstr = getDataString()
|
||||
val message = """{"messageId":"$messageId","result":{"code":"200","msg":{"type":"init_device_reply","status":"ok","optype":"$opType","time":"$dstr"}}}"""
|
||||
publish(topicPublishInitDeviceReply, message)
|
||||
}
|
||||
|
||||
override fun publishCellSetReply(messageId: String) {
|
||||
val dstr = getDataString()
|
||||
val message = """{"messageId":"$messageId","result":{"code":"200","msg":{"type":"door_set_reply","status":"ok","time":"$dstr"}}}"""
|
||||
publish(topicPublishInitDeviceReply, message)
|
||||
}
|
||||
|
||||
// ==================== 回调 ====================
|
||||
|
||||
override fun setMessageCallback(callback: (topic: String, message: String) -> Unit) {
|
||||
messageCallback = callback
|
||||
}
|
||||
|
||||
override fun setConnectionCallback(callback: (MqttConnectionState) -> Unit) {
|
||||
connectionCallback = callback
|
||||
}
|
||||
|
||||
// ==================== 内部方法 ====================
|
||||
|
||||
private fun publish(topic: String, message: String) {
|
||||
if (!isMqttConnect) {
|
||||
Log.w(MqttConstants.TAG, "publish failed: not connected")
|
||||
return
|
||||
}
|
||||
Log.d(MqttConstants.TAG, "publish to $topic: $message")
|
||||
|
||||
// TODO: 真实发布
|
||||
// try {
|
||||
// mqttAndroidClient?.publish(topic, message.toByteArray(), MqttConstants.QOS_DEFAULT, false)
|
||||
// } catch (e: MqttException) {
|
||||
// e.printStackTrace()
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理收到的消息(由 MqttCallback 调用)
|
||||
*/
|
||||
internal fun handleMessage(topic: String, payload: String) {
|
||||
Log.d(MqttConstants.TAG, "MQTT收到消息:topic:$topic msg:$payload")
|
||||
messageCallback?.invoke(topic, payload)
|
||||
|
||||
try {
|
||||
when (topic) {
|
||||
topicSubscribeUpdate -> handleUpdateMessage(payload)
|
||||
topicSubscribePropertySet -> handlePropertySetMessage(payload)
|
||||
topicSubscribeServer -> handleServerMessage(payload)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(MqttConstants.TAG, "handleMessage error", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpdateMessage(payload: String) {
|
||||
val jsonObject = JSONObject(payload)
|
||||
val body = jsonObject.getString("body")
|
||||
val bodyJson = JSONObject(body)
|
||||
|
||||
val messageId = bodyJson.getString("messageId")
|
||||
val taskId = bodyJson.getString("taskId")
|
||||
val firmwareVersion = bodyJson.getString("firmwareVersion")
|
||||
val url = bodyJson.getString("url")
|
||||
|
||||
sharedPreferences.edit().apply {
|
||||
putString("fw", firmwareVersion)
|
||||
putString("url", url)
|
||||
putString("mid", messageId)
|
||||
putString("tid", taskId)
|
||||
putString("vcheck", "1")
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePropertySetMessage(payload: String) {
|
||||
val jsonObject = JSONObject(payload)
|
||||
val identifier = jsonObject.getString("identifier")
|
||||
val body = jsonObject.getString("body")
|
||||
Log.d(MqttConstants.TAG, "PropertySet: $identifier = $body")
|
||||
}
|
||||
|
||||
private fun handleServerMessage(payload: String) {
|
||||
val jsonObject = JSONObject(payload)
|
||||
val identifier = jsonObject.getString("identifier")
|
||||
Log.d(MqttConstants.TAG, "Server identifier: $identifier")
|
||||
// 根据 identifier 处理不同的服务调用
|
||||
}
|
||||
|
||||
// ==================== 消息构建 ====================
|
||||
|
||||
private fun buildDoorInfoSyncEvent(info: DoorInfo): String {
|
||||
return """{"door_info_sync_event":{"devType":"${info.devType}","devSn":"${info.devSn}","devCheckType":"${info.devCheckType}","cabinetName":"${info.cabinetName}","doorState":"${info.cellState}","connectorType":"${info.cellConnectorType}","devPvd":"${info.devPvd}","doorName":"${info.cellName}","doorRepairType":"${info.cellRepairType}","devRepairType":"${info.devRepairType}","devPro":"${info.devPro}","devState":"${info.devState}","devMfg":"${info.devMfg}","devPwr":"${info.devPwr}","doorVoltageType":"${info.cellVoltageType}"}}"""
|
||||
}
|
||||
|
||||
private fun buildBorrowDeviceEvent(event: DeviceEvent): String {
|
||||
val doorInfo = event.doorInfo
|
||||
return """{"borrow_device_event":{"opType":"${event.opType}","deviceCode":"${event.deviceCode}","cabinetName":"${event.cabinetName}","devSn":"${event.devSn}","doorName":"${event.doorName}","devType":"${event.devType}","userId":"${event.userId}","batchId":"${event.batchId}","borrowTime":"${event.opTime}","doorInfo":${buildDoorInfoJson(doorInfo)}}}"""
|
||||
}
|
||||
|
||||
private fun buildReturnDeviceEvent(event: DeviceEvent): String {
|
||||
val doorInfo = event.doorInfo
|
||||
return """{"return_device_event":{"opType":"${event.opType}","deviceCode":"${event.deviceCode}","cabinetName":"${event.cabinetName}","devSn":"${event.devSn}","doorName":"${event.doorName}","devType":"${event.devType}","userId":"${event.userId}","batchId":"${event.batchId}","doorRepairType":"${doorInfo.cellRepairType}","devRepairType":"${doorInfo.devRepairType}","returnTime":"${event.opTime}","doorInfo":${buildDoorInfoJson(doorInfo)}}}"""
|
||||
}
|
||||
|
||||
private fun buildLoseDeviceEvent(event: DeviceEvent): String {
|
||||
val doorInfo = event.doorInfo
|
||||
return """{"lose_device_event":{"opType":"${event.opType}","deviceCode":"${event.deviceCode}","cabinetName":"${event.cabinetName}","devSn":"${event.devSn}","doorName":"${event.doorName}","devType":"${event.devType}","userId":"${event.userId}","batchId":"${event.batchId}","doorRepairType":"${doorInfo.cellRepairType}","devRepairType":"${doorInfo.devRepairType}","loseTime":"${event.opTime}","doorInfo":${buildDoorInfoJson(doorInfo)}}}"""
|
||||
}
|
||||
|
||||
private fun buildDoorInfoJson(info: DoorInfo): String {
|
||||
return """{"devType":"${info.devType}","devSn":"${info.devSn}","devCheckType":"${info.devCheckType}","cabinetName":"${info.cabinetName}","doorState":"${info.cellState}","connectorType":"${info.cellConnectorType}","devPvd":"${info.devPvd}","doorName":"${info.cellName}","doorRepairType":"${info.cellRepairType}","devRepairType":"${info.devRepairType}","devPro":"${info.devPro}","devState":"${info.devState}","devMfg":"${info.devMfg}","devPwr":"${info.devPwr}","doorVoltageType":"${info.cellVoltageType}"}"""
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
private fun md5(input: String): String {
|
||||
return try {
|
||||
val md = MessageDigest.getInstance("MD5")
|
||||
val digest = md.digest(input.toByteArray())
|
||||
digest.joinToString("") { "%02x".format(it) }
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDataString(): String {
|
||||
val sdf = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||
return sdf.format(java.util.Date())
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 便捷扩展 ====================
|
||||
|
||||
fun Context.getMqttService(): IMqttService = MqttServiceImpl.getInstance(this)
|
||||
@ -32,6 +32,8 @@ import androidx.compose.foundation.border
|
||||
import com.example.smarthome.R
|
||||
import com.example.smarthome.data.WeatherInfo
|
||||
import com.example.smarthome.data.WeatherService
|
||||
import com.example.smarthome.data.LanguageManager
|
||||
import com.example.smarthome.data.tr
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.delay
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@ -139,10 +141,10 @@ fun RoomContent(selectedRoom: Int, roomName: String) {
|
||||
fun OverviewRoomContent() {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
AirConditionerCard(modifier = Modifier.weight(1f), roomName = "全屋")
|
||||
UsageStatusChart(modifier = Modifier.weight(1f), roomName = "全屋")
|
||||
AirConditionerCard(modifier = Modifier.weight(1f), roomName = tr("room_all"))
|
||||
UsageStatusChart(modifier = Modifier.weight(1f), roomName = tr("room_all"))
|
||||
}
|
||||
LightRow(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), roomName = "全屋")
|
||||
LightRow(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), roomName = tr("room_all"))
|
||||
ModeButtonsRow(onModeSelected = { })
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
@ -502,14 +504,17 @@ fun SideNavRail(selectedNavItem: Int = 0, onNavItemSelect: (Int) -> Unit = {}) {
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// 上半部分:导航项
|
||||
// 监听语言变化以触发重组
|
||||
val currentLang by LanguageManager.currentLanguage.collectAsState()
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
NavRailItem("控制台", R.drawable.ic_dashboard, selectedNavItem == 0, onClick = { onNavItemSelect(0) })
|
||||
NavRailItem("场景", R.drawable.ic_scene, selectedNavItem == 1, onClick = { onNavItemSelect(1) })
|
||||
NavRailItem("自动化", R.drawable.ic_automation, selectedNavItem == 2, onClick = { onNavItemSelect(2) })
|
||||
NavRailItem("统计", R.drawable.ic_statistics, selectedNavItem == 3, onClick = { onNavItemSelect(3) })
|
||||
NavRailItem("安全", R.drawable.ic_security, selectedNavItem == 4, onClick = { onNavItemSelect(4) })
|
||||
NavRailItem("设置", R.drawable.ic_settings, selectedNavItem == 5, onClick = { onNavItemSelect(5) })
|
||||
NavRailItem(tr("nav_dashboard"), R.drawable.ic_dashboard, selectedNavItem == 0, onClick = { onNavItemSelect(0) })
|
||||
NavRailItem(tr("nav_scene"), R.drawable.ic_scene, selectedNavItem == 1, onClick = { onNavItemSelect(1) })
|
||||
NavRailItem(tr("nav_automation"), R.drawable.ic_automation, selectedNavItem == 2, onClick = { onNavItemSelect(2) })
|
||||
NavRailItem(tr("nav_statistics"), R.drawable.ic_statistics, selectedNavItem == 3, onClick = { onNavItemSelect(3) })
|
||||
NavRailItem(tr("nav_security"), R.drawable.ic_security, selectedNavItem == 4, onClick = { onNavItemSelect(4) })
|
||||
NavRailItem(tr("nav_settings"), R.drawable.ic_settings, selectedNavItem == 5, onClick = { onNavItemSelect(5) })
|
||||
}
|
||||
|
||||
// 底部:模式切换按钮(暂时注释)
|
||||
@ -578,6 +583,7 @@ fun NavRailItem(text: String, iconRes: Int, selected: Boolean, onClick: () -> Un
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier
|
||||
.width(80.dp) // 固定宽度,防止文字长度不一致导致图标不对齐
|
||||
.scale(scale)
|
||||
.clickable(
|
||||
indication = null,
|
||||
@ -615,9 +621,10 @@ fun NavRailItem(text: String, iconRes: Int, selected: Boolean, onClick: () -> Un
|
||||
Text(
|
||||
text = text,
|
||||
color = if (selected) Color.White else Color(0xFFB0B0B0),
|
||||
fontSize = 13.sp,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
|
||||
maxLines = 1
|
||||
maxLines = 1,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -638,7 +645,7 @@ fun TopBar() {
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = "控制台", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.White)
|
||||
Text(text = tr("dashboard_title"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.White)
|
||||
WeatherCard(weatherInfo = weatherInfo, isLoading = isLoading)
|
||||
}
|
||||
}
|
||||
@ -1123,12 +1130,12 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间
|
||||
|
||||
// 模式emoji图标
|
||||
val modeIcons = listOf(
|
||||
"❄️" to "制冷",
|
||||
"☀️" to "制热",
|
||||
"💧" to "除湿",
|
||||
"🌀" to "送风"
|
||||
"❄️" to tr("ac_mode_cool"),
|
||||
"☀️" to tr("ac_mode_heat"),
|
||||
"💧" to tr("ac_mode_dry"),
|
||||
"🌀" to tr("ac_mode_fan")
|
||||
)
|
||||
val fanSpeeds = listOf("自动", "低", "中", "高")
|
||||
val fanSpeeds = listOf(tr("ac_fan_auto"), tr("ac_fan_low"), tr("ac_fan_medium"), tr("ac_fan_high"))
|
||||
|
||||
// 图标动画
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "ac_icon")
|
||||
@ -1212,7 +1219,7 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(text = "空调", fontWeight = FontWeight.SemiBold, color = Color.White, fontSize = 16.sp)
|
||||
Text(text = tr("ac_title"), fontWeight = FontWeight.SemiBold, color = Color.White, fontSize = 16.sp)
|
||||
Text(text = roomName, fontSize = 11.sp, color = Color(0xFF9AA0A6))
|
||||
}
|
||||
}
|
||||
@ -1320,7 +1327,7 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = "风速", fontSize = 12.sp, color = Color(0xFF9AA0A6))
|
||||
Text(text = tr("ac_fan_speed"), fontSize = 12.sp, color = Color(0xFF9AA0A6))
|
||||
fanSpeeds.forEachIndexed { index, speed ->
|
||||
val isSelected = fanSpeed == index
|
||||
Box(
|
||||
|
||||
@ -22,7 +22,10 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.example.smarthome.data.BackgroundManager
|
||||
import com.example.smarthome.data.LanguageManager
|
||||
import com.example.smarthome.data.tr
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(onBack: () -> Unit) {
|
||||
@ -44,52 +47,67 @@ fun SettingsScreen(onBack: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
fun SettingsContentList() {
|
||||
val context = LocalContext.current
|
||||
val currentLang by LanguageManager.currentLanguage.collectAsState()
|
||||
var showLanguageDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// 语言选择对话框
|
||||
if (showLanguageDialog) {
|
||||
LanguageSelectDialog(
|
||||
currentLanguage = currentLang,
|
||||
onLanguageSelect = { lang ->
|
||||
LanguageManager.setLanguage(context, lang)
|
||||
showLanguageDialog = false
|
||||
},
|
||||
onDismiss = { showLanguageDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item { SettingsSection(title = "账户设置") {
|
||||
item { SettingsSection(title = tr("settings_account")) {
|
||||
SettingsItem(
|
||||
title = "个人信息",
|
||||
subtitle = "管理您的个人资料",
|
||||
title = tr("settings_profile"),
|
||||
subtitle = tr("settings_profile_desc"),
|
||||
onClick = { }
|
||||
)
|
||||
SettingsItem(
|
||||
title = "账户安全",
|
||||
subtitle = "密码和安全设置",
|
||||
title = tr("settings_security"),
|
||||
subtitle = tr("settings_security_desc"),
|
||||
onClick = { }
|
||||
)
|
||||
}}
|
||||
|
||||
item { SettingsSection(title = "设备管理") {
|
||||
item { SettingsSection(title = tr("settings_device")) {
|
||||
var autoConnect by remember { mutableStateOf(true) }
|
||||
SettingsSwitchItem(
|
||||
title = "自动连接",
|
||||
subtitle = "自动连接到家庭网络",
|
||||
title = tr("settings_auto_connect"),
|
||||
subtitle = tr("settings_auto_connect_desc"),
|
||||
checked = autoConnect,
|
||||
onCheckedChange = { autoConnect = it }
|
||||
)
|
||||
|
||||
var notifications by remember { mutableStateOf(true) }
|
||||
SettingsSwitchItem(
|
||||
title = "设备通知",
|
||||
subtitle = "接收设备状态通知",
|
||||
title = tr("settings_notifications"),
|
||||
subtitle = tr("settings_notifications_desc"),
|
||||
checked = notifications,
|
||||
onCheckedChange = { notifications = it }
|
||||
)
|
||||
}}
|
||||
|
||||
item { SettingsSection(title = "显示设置") {
|
||||
item { SettingsSection(title = tr("settings_display")) {
|
||||
BackgroundSelector()
|
||||
|
||||
val context = LocalContext.current
|
||||
val selectedBg by BackgroundManager.selectedBackground.collectAsState()
|
||||
// 深色模式根据当前背景是否为暗色来判断
|
||||
val isDarkMode = BackgroundManager.backgrounds.getOrNull(selectedBg)?.isDark ?: false
|
||||
|
||||
SettingsSwitchItem(
|
||||
title = "深色模式",
|
||||
subtitle = "使用深色主题",
|
||||
title = tr("settings_dark_mode"),
|
||||
subtitle = tr("settings_dark_mode_desc"),
|
||||
checked = isDarkMode,
|
||||
onCheckedChange = { dark ->
|
||||
// 切换深色/浅色模式时自动更换背景
|
||||
@ -102,26 +120,26 @@ fun SettingsContentList() {
|
||||
)
|
||||
|
||||
SettingsItem(
|
||||
title = "语言",
|
||||
subtitle = "简体中文",
|
||||
onClick = { }
|
||||
title = tr("settings_language"),
|
||||
subtitle = currentLang.nativeName,
|
||||
onClick = { showLanguageDialog = true }
|
||||
)
|
||||
}}
|
||||
|
||||
item { SettingsSection(title = "关于") {
|
||||
item { SettingsSection(title = tr("settings_about")) {
|
||||
SettingsItem(
|
||||
title = "版本信息",
|
||||
title = tr("settings_version"),
|
||||
subtitle = "v1.0.0",
|
||||
onClick = { }
|
||||
)
|
||||
SettingsItem(
|
||||
title = "隐私政策",
|
||||
subtitle = "查看隐私政策",
|
||||
title = tr("settings_privacy"),
|
||||
subtitle = tr("settings_privacy_desc"),
|
||||
onClick = { }
|
||||
)
|
||||
SettingsItem(
|
||||
title = "用户协议",
|
||||
subtitle = "查看用户协议",
|
||||
title = tr("settings_terms"),
|
||||
subtitle = tr("settings_terms_desc"),
|
||||
onClick = { }
|
||||
)
|
||||
}}
|
||||
@ -133,6 +151,98 @@ fun SettingsContentList() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LanguageSelectDialog(
|
||||
currentLanguage: LanguageManager.Language,
|
||||
onLanguageSelect: (LanguageManager.Language) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(Color(0xFF1E1E2E))
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
text = tr("select_language"),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LanguageManager.Language.entries.forEach { lang ->
|
||||
val isSelected = lang == currentLanguage
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
if (isSelected) Color(0xFF6C63FF).copy(alpha = 0.3f)
|
||||
else Color(0xFF2A2A3E)
|
||||
)
|
||||
.border(
|
||||
width = if (isSelected) 2.dp else 0.dp,
|
||||
color = if (isSelected) Color(0xFF6C63FF) else Color.Transparent,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.clickable { onLanguageSelect(lang) }
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = lang.nativeName,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.White
|
||||
)
|
||||
if (lang.displayName != lang.nativeName) {
|
||||
Text(
|
||||
text = lang.displayName,
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFF9AA0A6)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isSelected) {
|
||||
Text(text = "✓", color = Color(0xFF6C63FF), fontSize = 18.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// 取消按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color(0xFF3A3A4E))
|
||||
.clickable { onDismiss() }
|
||||
.padding(14.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = tr("cancel"),
|
||||
color = Color.White,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsTopBar(onBack: () -> Unit) {
|
||||
Row(
|
||||
@ -158,7 +268,7 @@ fun SettingsTopBar(onBack: () -> Unit) {
|
||||
Text(text = "←", color = Color.White, fontSize = 20.sp)
|
||||
}
|
||||
Text(
|
||||
text = "设置",
|
||||
text = tr("settings_title"),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
@ -344,7 +454,7 @@ fun LogoutButton(onClick: () -> Unit) {
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "退出登录",
|
||||
text = tr("settings_logout"),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
|
||||
BIN
app/src/main/res/drawable/background1.jpg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
app/src/main/res/drawable/background1_thumb.jpg
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
app/src/main/res/drawable/background2.jpg
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
app/src/main/res/drawable/background2_thumb.jpg
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
app/src/main/res/drawable/background3.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
app/src/main/res/drawable/background3_thumb.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
63
app/src/main/res/drawable/ic_ac_cool.xml
Normal file
@ -0,0 +1,63 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<!-- 制冷图标 - 雪花 -->
|
||||
<!-- 垂直线 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,2 L12,22"/>
|
||||
|
||||
<!-- 左上到右下斜线 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M4.93,4.93 L19.07,19.07"/>
|
||||
|
||||
<!-- 右上到左下斜线 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19.07,4.93 L4.93,19.07"/>
|
||||
|
||||
<!-- 顶部分叉 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,5 L10,7 M12,5 L14,7"/>
|
||||
|
||||
<!-- 底部分叉 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,19 L10,17 M12,19 L14,17"/>
|
||||
|
||||
<!-- 左上分叉 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M7.05,7.05 L7.05,9.5 M7.05,7.05 L9.5,7.05"/>
|
||||
|
||||
<!-- 右下分叉 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M16.95,16.95 L16.95,14.5 M16.95,16.95 L14.5,16.95"/>
|
||||
</vector>
|
||||
19
app/src/main/res/drawable/ic_ac_dry.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<!-- 除湿图标 - 水滴 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,2 C12,2 6,10 6,14 C6,17.31 8.69,20 12,20 C15.31,20 18,17.31 18,14 C18,10 12,2 12,2 Z"/>
|
||||
|
||||
<!-- 水滴内的波纹 -->
|
||||
<path
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="0"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M12,18 C9.79,18 8,16.21 8,14 C8,12.5 9.5,10 12,7 L12,18 Z"/>
|
||||
</vector>
|
||||
25
app/src/main/res/drawable/ic_ac_fan.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<!-- 送风图标 - 风扇叶片 -->
|
||||
<!-- 中心圆 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,10 C10.9,10 10,10.9 10,12 C10,13.1 10.9,14 12,14 C13.1,14 14,13.1 14,12 C14,10.9 13.1,10 12,10 Z"/>
|
||||
|
||||
<!-- 三片扇叶 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,2 C12,2 14,6 14,8 C14,9.1 13.1,10 12,10 C10.9,10 10,9.1 10,8 C10,6 12,2 12,2 Z"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M20.66,17 C20.66,17 16.66,17 14.93,16 C13.93,15.42 13.42,14.07 14,13.07 C14.58,12.07 15.93,11.58 16.93,12.15 C18.66,13.15 20.66,17 20.66,17 Z"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M3.34,17 C3.34,17 7.34,17 9.07,16 C10.07,15.42 10.58,14.07 10,13.07 C9.42,12.07 8.07,11.58 7.07,12.15 C5.34,13.15 3.34,17 3.34,17 Z"/>
|
||||
</vector>
|
||||
69
app/src/main/res/drawable/ic_ac_heat.xml
Normal file
@ -0,0 +1,69 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<!-- 制热图标 - 太阳/火焰 -->
|
||||
<!-- 中心圆 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,8 C9.79,8 8,9.79 8,12 C8,14.21 9.79,16 12,16 C14.21,16 16,14.21 16,12 C16,9.79 14.21,8 12,8 Z"/>
|
||||
|
||||
<!-- 光芒 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,2 L12,5"/>
|
||||
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,19 L12,22"/>
|
||||
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M2,12 L5,12"/>
|
||||
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,12 L22,12"/>
|
||||
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M4.93,4.93 L7.05,7.05"/>
|
||||
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M16.95,16.95 L19.07,19.07"/>
|
||||
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M4.93,19.07 L7.05,16.95"/>
|
||||
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M16.95,7.05 L19.07,4.93"/>
|
||||
</vector>
|
||||
BIN
background2.jpg
Normal file
|
After Width: | Height: | Size: 2.9 MiB |