111/app/src/main/java/com/example/smarthome/data/MqttService.kt

487 lines
21 KiB
Kotlin
Raw Normal View History

2025-11-27 08:16:34 +00:00
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)