111/app/src/main/java/com/example/smarthome/data/MqttService.kt
2025-11-27 16:16:34 +08:00

487 lines
21 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)