258 lines
8.1 KiB
Swift
258 lines
8.1 KiB
Swift
//
|
||
// 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/<memberId>
|
||
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?()
|
||
}
|
||
}
|