// // MQTTService.swift // QuickLocation // // Created by 八条 on 2026/6/12. // import Foundation import CocoaMQTT import UIKit import _LocationEssentials // MARK: - MQTT 模型 /// 位置上报类型 enum MqttType: String, Codable { case track = "track" // 追踪(获取)和更新用户状态 case disconnect = "disconnect" case message = "message" case join = "join" // 加入群 case leave = "leave" // 离开群 case dismiss = "dismiss" // 解散群 case kick = "kick" // 踢人 case signIn = "signin" // 签到 case sos = "sos" // 求助 case needTrack = "needtrack" // 追踪上报 case emote = "emote" // 接收表情 } /// 单点位置 struct Points: Codable { let lat: Double let lon: Double let addr: String let time: Int64 // 毫秒时间戳 let speed: Double let bearing: Double let altitude: Double let accuracy: Double } /// 位置上报数据(发送) struct MqttLocation: Codable { let battery: String let points: [Points] } /// MQTT 收到的位置消息(接收解析用) struct MqttIncomingMessage: Decodable { let type: String? let data: MqttIncomingData? let extra: String? } struct MqttIncomingData: Decodable { let points: [Points]? let battery: String? } // MARK: - MQTTService /// MQTT 5.0 服务,管理连接、订阅和消息收发 final class MQTTService: NSObject { static let shared = MQTTService() private var mqtt: CocoaMQTT5? private var isConnected = false // MARK: - 连接状态回调 var onConnected: (() -> Void)? var onDisconnected: (() -> Void)? /// 全局接收回调(所有未匹配 topicCallback 的消息) var onMessageReceived: ((CocoaMQTT5Message, UInt16, MqttPublishProperties) -> Void)? /// 按 topic 订阅的回调(优先匹配) private var topicCallbacks: [String: (CocoaMQTT5Message) -> Void] = [:] // MARK: - 配置 private var host: String { "emqx.batiao8.com" } private var port: UInt16 { 1883 } /// 当前 clientID,切换用户时可更新后重连 private(set) var clientID: String = "" private var userName = "batiao" private var password = "Batiao12B" private var topic = "smartdrive/" override private init() {} // MARK: - 连接 func connect() { guard !isConnected else { return } if clientID.isEmpty { clientID = "smartdrive_\(AppContextManager.shared.userId)" } let mqtt = CocoaMQTT5(clientID: clientID, host: host, port: port) mqtt.username = userName mqtt.password = password mqtt.keepAlive = 60 mqtt.autoReconnect = true mqtt.autoReconnectTimeInterval = 5 mqtt.delegate = self mqtt.logLevel = .warning self.mqtt = mqtt _ = mqtt.connect() } // MARK: - 断开 func disconnect() { mqtt?.disconnect() isConnected = false } // MARK: - 切换用户 /// 更新 clientID 并重连(切换用户后调用) func updateClientID(_ newID: String) { clientID = newID disconnect() connect() } // MARK: - 订阅主题 /// - Parameters: /// - topic: 主题 /// - qos: 服务质量 /// - callback: 可选,该 topic 的专用回调,收到消息时优先于此回调 func subscribe(topic: String, qos: CocoaMQTTQoS = .qos1, callback: ((CocoaMQTT5Message) -> Void)? = nil) { let subscription = MqttSubscription(topic: topic, qos: qos) mqtt?.subscribe([subscription]) if let cb = callback { topicCallbacks[topic] = cb } } // MARK: - 取消订阅 func unsubscribe(topic: String) { mqtt?.unsubscribe(topic) topicCallbacks.removeValue(forKey: topic) } /// 订阅指定圈子的所有成员位置 topic: smartdrive/ func subscribeGroupMembers(_ memberIds: [String]) { for id in memberIds { subscribe(topic: "\(topic)\(id)") } } /// 取消订阅上一批成员 func unsubscribeGroupMembers(_ memberIds: [String]) { for id in memberIds { mqtt?.unsubscribe("\(topic)\(id)") } } // MARK: - 发布消息 @discardableResult func publish(topic: String, message: String, qos: CocoaMQTTQoS = .qos1) -> Int { let properties = MqttPublishProperties() return mqtt?.publish(topic, withString: message, qos: qos, DUP: false, retained: false, properties: properties) ?? -1 } @discardableResult func publish(topic: String, data: Data, qos: CocoaMQTTQoS = .qos1) -> Int { let properties = MqttPublishProperties() let message = CocoaMQTT5Message(topic: topic, payload: [UInt8](data)) return mqtt?.publish(message, DUP: false, retained: false, properties: properties) ?? -1 } // MARK: - 位置上报 /// 构建并上报位置数据(格式与 Android 一致) func reportLocation(lat: Double, lon: Double, addr: String, speed: CLLocationSpeed, bearing: CLLocationDirection, altitude: CLLocationDistance, accuracy: CLLocationAccuracy) { let battery = UIDevice.current.batteryLevel > 0 ? Int(UIDevice.current.batteryLevel * 100) : 0 let point = Points( lat: lat, lon: lon, addr: addr, time: Int64(Date().timeIntervalSince1970 * 1000), speed: speed, bearing: bearing, altitude: altitude, accuracy: accuracy ) let location = MqttLocation(battery: battery.string, points: [point]) guard let jsonData = try? JSONEncoder().encode(location), let dataDict = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else { return } // 外层包装,data 是 JSON 对象而非字符串 let payload: [String: Any] = [ "type": MqttType.track.rawValue, "extra": "", "data": dataDict ] publish(topic: "\(topic)\(AppContextManager.shared.userId)", message: payload.toJsonString()) } } // MARK: - CocoaMQTT5Delegate extension MQTTService: CocoaMQTT5Delegate { func mqtt5(_ mqtt5: CocoaMQTT5, didConnectAck ack: CocoaMQTTCONNACKReasonCode, connAckData: MqttDecodeConnAck?) { isConnected = true print("MQTT5 connected: \(ack)") onConnected?() } func mqtt5(_ mqtt5: CocoaMQTT5, didPublishMessage message: CocoaMQTT5Message, id: UInt16) { print("MQTT5 published: \(message.topic)") } func mqtt5(_ mqtt5: CocoaMQTT5, didPublishAck id: UInt16, pubAckData: MqttDecodePubAck?) { print("MQTT5 publish ack: \(id)") } func mqtt5(_ mqtt5: CocoaMQTT5, didPublishRec id: UInt16, pubRecData: MqttDecodePubRec?) {} func mqtt5(_ mqtt5: CocoaMQTT5, didReceiveMessage message: CocoaMQTT5Message, id: UInt16, publishData: MqttDecodePublish?) { // 优先 topic 专用回调 if let cb = topicCallbacks[message.topic] { cb(message) return } // 没有专用回调时走全局回调 if let payload = message.string { print("MQTT5 received on \(message.topic): \(payload)") } onMessageReceived?(message, id, MqttPublishProperties()) } func mqtt5(_ mqtt5: CocoaMQTT5, didSubscribeTopics success: NSDictionary, failed: [String], subAckData: MqttDecodeSubAck?) { print("MQTT5 subscribe success: \(success), failed: \(failed)") } func mqtt5(_ mqtt5: CocoaMQTT5, didUnsubscribeTopics topics: [String], unsubAckData: MqttDecodeUnsubAck?) { print("MQTT5 unsubscribe: \(topics)") } func mqtt5(_ mqtt5: CocoaMQTT5, didReceiveDisconnectReasonCode reasonCode: CocoaMQTTDISCONNECTReasonCode) {} func mqtt5(_ mqtt5: CocoaMQTT5, didReceiveAuthReasonCode reasonCode: CocoaMQTTAUTHReasonCode) {} func mqtt5DidPing(_ mqtt5: CocoaMQTT5) {} func mqtt5DidReceivePong(_ mqtt5: CocoaMQTT5) {} func mqtt5DidDisconnect(_ mqtt5: CocoaMQTT5, withError err: Error?) { isConnected = false print("MQTT5 disconnected: \(err?.localizedDescription ?? "")") onDisconnected?() } }