724 lines
24 KiB
Swift
724 lines
24 KiB
Swift
//
|
||
// MQTTService.swift
|
||
// QuickLocation
|
||
//
|
||
// Created by 八条 on 2026/6/12.
|
||
//
|
||
|
||
import Foundation
|
||
import CocoaMQTT
|
||
import UIKit
|
||
import CoreLocation
|
||
import Network
|
||
import CoreTelephony
|
||
import AVFoundation
|
||
|
||
// 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" // 接收表情
|
||
case phone = "phone" // 手机信息上报
|
||
case phoneUsage = "phoneUsage" // 当前 App 使用次数上报
|
||
}
|
||
|
||
/// 单点位置
|
||
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]
|
||
}
|
||
|
||
/// 签到上报数据
|
||
struct MqttSignIn: Codable {
|
||
let topic: String?
|
||
let latitude: Double
|
||
let longitude: Double
|
||
let address: String
|
||
}
|
||
|
||
/// MQTT 收到的位置消息(接收解析用)
|
||
struct MqttIncomingMessage: Decodable {
|
||
let type: String?
|
||
let data: MqttIncomingData?
|
||
let extra: String?
|
||
}
|
||
|
||
struct MqttIncomingData: Decodable {
|
||
let points: [Points]?
|
||
let battery: String?
|
||
let index: Int?
|
||
let group_key: String?
|
||
let user_id: String?
|
||
}
|
||
|
||
/// 手机信息上报数据
|
||
private struct MqttPhoneInfo: Codable {
|
||
let brand: String
|
||
let model: String
|
||
let memory_total: String
|
||
let memory_used: String
|
||
let battery: String
|
||
let network: String
|
||
let brightness: String
|
||
let volume: String
|
||
let weather: String
|
||
let use_time: String
|
||
let unlock_count: String
|
||
}
|
||
|
||
private struct MqttPhoneReportData: Codable {
|
||
let user_id: String
|
||
let group_key: String
|
||
let phone: MqttPhoneInfo
|
||
}
|
||
|
||
private struct MqttPhoneReportPayload: Codable {
|
||
let type: String
|
||
let data: MqttPhoneReportData
|
||
let extra: String
|
||
}
|
||
|
||
private struct MqttPhoneUsageExtra: Codable {
|
||
let count: Int
|
||
}
|
||
|
||
private struct MqttPhoneUsageItem: Codable {
|
||
let day: String
|
||
let appName: String
|
||
let packageName: String
|
||
let usageTime: Int
|
||
let extra: MqttPhoneUsageExtra
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case day
|
||
case appName = "app_name"
|
||
case packageName = "package"
|
||
case usageTime = "usage_time"
|
||
case extra
|
||
}
|
||
}
|
||
|
||
private struct MqttPhoneUsageData: Codable {
|
||
let userId: String
|
||
let groupKey: String
|
||
let phoneUsage: [MqttPhoneUsageItem]
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case userId = "user_id"
|
||
case groupKey = "group_key"
|
||
case phoneUsage = "phone_usage"
|
||
}
|
||
}
|
||
|
||
private struct MqttPhoneUsagePayload: Codable {
|
||
let type: String
|
||
let data: MqttPhoneUsageData
|
||
let extra: String
|
||
}
|
||
|
||
private struct MqttPhoneUsageSnapshot: Equatable {
|
||
let day: String
|
||
let count: Int
|
||
}
|
||
|
||
// 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/"
|
||
|
||
// MARK: - 手机信息上报
|
||
private let phoneReportInterval: TimeInterval = 30 * 60
|
||
private let phoneReportRetryInterval: TimeInterval = 60
|
||
private let phoneNetworkMonitor = PhoneNetworkStatusMonitor()
|
||
private var phoneReportGroupKey = ""
|
||
private var phoneReportLocation: CLLocation?
|
||
private var phoneReportTimer: Timer?
|
||
private var lastPhoneReportDate: Date?
|
||
private var isPhoneReportInFlight = false
|
||
private var phoneReportGeneration = 0
|
||
private var lastObservedPhoneUsage: MqttPhoneUsageSnapshot?
|
||
private var pendingPhoneUsage: MqttPhoneUsageSnapshot?
|
||
|
||
override private init() {
|
||
super.init()
|
||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||
phoneNetworkMonitor.start()
|
||
NotificationCenter.default.addObserver(
|
||
self,
|
||
selector: #selector(applicationDidBecomeActive),
|
||
name: UIApplication.didBecomeActiveNotification,
|
||
object: nil
|
||
)
|
||
NotificationCenter.default.addObserver(
|
||
self,
|
||
selector: #selector(appUsageCountDidChange),
|
||
name: .unlockCountDidChange,
|
||
object: nil
|
||
)
|
||
let usage = Self.currentPhoneUsageSnapshot()
|
||
lastObservedPhoneUsage = usage
|
||
if usage.count > 0 {
|
||
pendingPhoneUsage = usage
|
||
}
|
||
}
|
||
|
||
// 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.cleanSession = true
|
||
mqtt.keepAlive = 30
|
||
mqtt.autoReconnect = true
|
||
mqtt.autoReconnectTimeInterval = 5
|
||
mqtt.delegate = self
|
||
mqtt.logLevel = .warning
|
||
// MQTT 5 连接属性(对应 Android 端配置)
|
||
let connProps = MqttConnectProperties()
|
||
connProps.sessionExpiryInterval = 30
|
||
connProps.receiveMaximum = 10
|
||
connProps.maximumPacketSize = 10240
|
||
connProps.topicAliasMaximum = 0
|
||
mqtt.connectProperties = connProps
|
||
// Will 消息
|
||
let will = CocoaMQTT5Message(topic: "willtopic", payload: [])
|
||
will.qos = .qos1
|
||
will.retained = false
|
||
mqtt.willMessage = will
|
||
|
||
self.mqtt = mqtt
|
||
_ = mqtt.connect()
|
||
}
|
||
|
||
// MARK: - 断开
|
||
func disconnect() {
|
||
mqtt?.disconnect()
|
||
isConnected = false
|
||
invalidatePhoneReportTimer()
|
||
}
|
||
|
||
// MARK: - 切换用户
|
||
/// 更新 clientID 并重连(切换用户后调用)
|
||
func updateClientID(_ newID: String) {
|
||
resetPhoneReportState()
|
||
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)
|
||
subscription.noLocal = false
|
||
subscription.retainAsPublished = true
|
||
subscription.retainHandling = .none
|
||
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: - 签到
|
||
func reportSignIn(lat: Double, lon: Double, addr: String) {
|
||
let signIn = MqttSignIn(topic: nil, latitude: lat, longitude: lon, address: addr)
|
||
guard let jsonData = try? JSONEncoder().encode(signIn),
|
||
let dataDict = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
|
||
else { return }
|
||
|
||
let payload: [String: Any] = [
|
||
"type": MqttType.signIn.rawValue,
|
||
"data": dataDict,
|
||
"extra": "",
|
||
"sort": ""
|
||
]
|
||
publish(topic: "\(topic)\(AppContextManager.shared.userId)", message: payload.toJsonString())
|
||
}
|
||
|
||
// MARK: - 手机信息上报
|
||
|
||
/// 首页提供当前默认圈子和最新定位。切换圈子只更新上下文,不额外触发重复上报。
|
||
func updatePhoneReportContext(groupKey: String?, location: CLLocation?) {
|
||
performOnMain { [weak self] in
|
||
guard let self else { return }
|
||
if let groupKey {
|
||
self.phoneReportGroupKey = groupKey
|
||
}
|
||
if let location, CLLocationCoordinate2DIsValid(location.coordinate) {
|
||
self.phoneReportLocation = location
|
||
}
|
||
self.reportPhoneUsageIfNeeded()
|
||
self.reportPhoneIfNeeded()
|
||
}
|
||
}
|
||
|
||
/// MQTT 重连、Timer 和 App 回到前台都通过此入口检查是否到期。
|
||
func reportPhoneIfNeeded(force: Bool = false) {
|
||
guard Thread.isMainThread else {
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.reportPhoneIfNeeded(force: force)
|
||
}
|
||
return
|
||
}
|
||
|
||
guard isConnected,
|
||
!isPhoneReportInFlight,
|
||
!phoneReportGroupKey.isEmpty,
|
||
!AppContextManager.shared.userId.isEmpty else { return }
|
||
|
||
if !force,
|
||
let lastPhoneReportDate,
|
||
Date().timeIntervalSince(lastPhoneReportDate) < phoneReportInterval {
|
||
schedulePhoneReportTimer(after: phoneReportInterval - Date().timeIntervalSince(lastPhoneReportDate))
|
||
return
|
||
}
|
||
|
||
isPhoneReportInFlight = true
|
||
phoneReportGeneration += 1
|
||
let generation = phoneReportGeneration
|
||
|
||
guard let location = phoneReportLocation else {
|
||
finishPhoneReport(weather: "未知", generation: generation)
|
||
return
|
||
}
|
||
|
||
MemberWeatherService.shared.weather(for: location) { [weak self] result in
|
||
let weather = (try? result.get().text) ?? "未知"
|
||
self?.finishPhoneReport(weather: weather, generation: generation)
|
||
}
|
||
}
|
||
|
||
@objc func applicationDidBecomeActive() {
|
||
capturePhoneUsageChange()
|
||
reportPhoneUsageIfNeeded()
|
||
reportPhoneIfNeeded()
|
||
}
|
||
|
||
@objc private func appUsageCountDidChange() {
|
||
performOnMain { [weak self] in
|
||
self?.capturePhoneUsageChange()
|
||
}
|
||
}
|
||
|
||
private func capturePhoneUsageChange() {
|
||
let usage = Self.currentPhoneUsageSnapshot()
|
||
guard usage != lastObservedPhoneUsage else { return }
|
||
lastObservedPhoneUsage = usage
|
||
pendingPhoneUsage = usage
|
||
reportPhoneUsageIfNeeded()
|
||
}
|
||
|
||
private func reportPhoneUsageIfNeeded() {
|
||
guard Thread.isMainThread else {
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.reportPhoneUsageIfNeeded()
|
||
}
|
||
return
|
||
}
|
||
|
||
let userId = AppContextManager.shared.userId
|
||
let groupKey = phoneReportGroupKey
|
||
guard isConnected,
|
||
let usage = pendingPhoneUsage,
|
||
!userId.isEmpty,
|
||
!groupKey.isEmpty else { return }
|
||
|
||
let item = MqttPhoneUsageItem(
|
||
day: usage.day,
|
||
appName: Self.appDisplayName,
|
||
packageName: "cn.zuom8.jisuloca",
|
||
usageTime: 0,
|
||
extra: MqttPhoneUsageExtra(count: usage.count)
|
||
)
|
||
let payload = MqttPhoneUsagePayload(
|
||
type: MqttType.phoneUsage.rawValue,
|
||
data: MqttPhoneUsageData(
|
||
userId: userId,
|
||
groupKey: groupKey,
|
||
phoneUsage: [item]
|
||
),
|
||
extra: ""
|
||
)
|
||
guard let data = try? JSONEncoder().encode(payload),
|
||
let message = String(data: data, encoding: .utf8) else { return }
|
||
|
||
let messageId = publish(
|
||
topic: "\(topic)\(userId)",
|
||
message: message
|
||
)
|
||
if messageId >= 0, pendingPhoneUsage == usage {
|
||
pendingPhoneUsage = nil
|
||
}
|
||
}
|
||
|
||
private func finishPhoneReport(weather: String, generation: Int) {
|
||
performOnMain { [weak self] in
|
||
guard let self,
|
||
generation == self.phoneReportGeneration else { return }
|
||
self.isPhoneReportInFlight = false
|
||
|
||
let userId = AppContextManager.shared.userId
|
||
let groupKey = self.phoneReportGroupKey
|
||
guard self.isConnected, !userId.isEmpty, !groupKey.isEmpty else { return }
|
||
|
||
let payload = MqttPhoneReportPayload(
|
||
type: MqttType.phone.rawValue,
|
||
data: MqttPhoneReportData(
|
||
user_id: userId,
|
||
group_key: groupKey,
|
||
phone: self.collectPhoneInfo(weather: weather)
|
||
),
|
||
extra: ""
|
||
)
|
||
guard let data = try? JSONEncoder().encode(payload),
|
||
let message = String(data: data, encoding: .utf8) else {
|
||
self.schedulePhoneReportTimer(after: self.phoneReportRetryInterval)
|
||
return
|
||
}
|
||
|
||
let messageId = self.publish(
|
||
topic: "\(self.topic)\(userId)",
|
||
message: message
|
||
)
|
||
if messageId >= 0 {
|
||
self.lastPhoneReportDate = Date()
|
||
self.schedulePhoneReportTimer(after: self.phoneReportInterval)
|
||
} else {
|
||
self.schedulePhoneReportTimer(after: self.phoneReportRetryInterval)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func collectPhoneInfo(weather: String) -> MqttPhoneInfo {
|
||
let storage = Self.storageInfo()
|
||
return MqttPhoneInfo(
|
||
brand: "iphone",
|
||
model: UIDevice.modelName,
|
||
memory_total: storage?.total ?? "未知",
|
||
memory_used: storage?.used ?? "未知",
|
||
battery: UIDevice.batteryPercent.map { "\($0)%" } ?? "未知",
|
||
network: phoneNetworkMonitor.statusText,
|
||
brightness: "\(Int((UIScreen.main.brightness * 100).rounded()))%",
|
||
volume: "\(Int((AVAudioSession.sharedInstance().outputVolume * 100).rounded()))%",
|
||
weather: weather,
|
||
use_time: "\(max(0, UnlockCountManager.shared.todayScreenTimeSeconds))",
|
||
unlock_count: "\(UnlockCountManager.shared.todayCount)"
|
||
)
|
||
}
|
||
|
||
private func schedulePhoneReportTimer(after interval: TimeInterval) {
|
||
invalidatePhoneReportTimer()
|
||
let timer = Timer(timeInterval: max(1, interval), repeats: false) { [weak self] _ in
|
||
self?.phoneReportTimer = nil
|
||
self?.reportPhoneIfNeeded()
|
||
}
|
||
RunLoop.main.add(timer, forMode: .common)
|
||
phoneReportTimer = timer
|
||
}
|
||
|
||
private func invalidatePhoneReportTimer() {
|
||
performOnMain { [weak self] in
|
||
self?.phoneReportTimer?.invalidate()
|
||
self?.phoneReportTimer = nil
|
||
}
|
||
}
|
||
|
||
private func resetPhoneReportState() {
|
||
performOnMain { [weak self] in
|
||
guard let self else { return }
|
||
self.phoneReportGeneration += 1
|
||
self.phoneReportGroupKey = ""
|
||
self.phoneReportLocation = nil
|
||
self.lastPhoneReportDate = nil
|
||
self.isPhoneReportInFlight = false
|
||
self.invalidatePhoneReportTimer()
|
||
}
|
||
}
|
||
|
||
private func performOnMain(_ work: @escaping () -> Void) {
|
||
if Thread.isMainThread {
|
||
work()
|
||
} else {
|
||
DispatchQueue.main.async(execute: work)
|
||
}
|
||
}
|
||
|
||
private static func storageInfo() -> (used: String, total: String)? {
|
||
let url = URL(fileURLWithPath: NSHomeDirectory())
|
||
let keys: Set<URLResourceKey> = [
|
||
.volumeTotalCapacityKey,
|
||
.volumeAvailableCapacityForImportantUsageKey
|
||
]
|
||
guard let values = try? url.resourceValues(forKeys: keys),
|
||
let total = values.volumeTotalCapacity,
|
||
let available = values.volumeAvailableCapacityForImportantUsage,
|
||
total > 0 else { return nil }
|
||
|
||
let totalBytes = Int64(total)
|
||
let usedBytes = max(0, totalBytes - available)
|
||
return (
|
||
used: String(format: "%.1fGB", Double(usedBytes) / 1_000_000_000),
|
||
total: String(format: "%.0fGB", Double(totalBytes) / 1_000_000_000)
|
||
)
|
||
}
|
||
|
||
private static func currentPhoneUsageSnapshot() -> MqttPhoneUsageSnapshot {
|
||
let formatter = DateFormatter()
|
||
formatter.calendar = Calendar.current
|
||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||
formatter.timeZone = TimeZone.current
|
||
formatter.dateFormat = "yyyy-MM-dd"
|
||
return MqttPhoneUsageSnapshot(
|
||
day: formatter.string(from: Date()),
|
||
count: max(0, UnlockCountManager.shared.todayAppUsageCount)
|
||
)
|
||
}
|
||
|
||
private static var appDisplayName: String {
|
||
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
|
||
?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
|
||
?? "极速定位"
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: - CocoaMQTT5Delegate
|
||
extension MQTTService: CocoaMQTT5Delegate {
|
||
|
||
func mqtt5(_ mqtt5: CocoaMQTT5, didConnectAck ack: CocoaMQTTCONNACKReasonCode, connAckData: MqttDecodeConnAck?) {
|
||
isConnected = true
|
||
print("MQTT5 connected: \(ack)")
|
||
// 订阅基础 topic,接收 signIn/join/leave 等非位置消息
|
||
subscribe(topic: topic)
|
||
onConnected?()
|
||
reportPhoneUsageIfNeeded()
|
||
reportPhoneIfNeeded()
|
||
}
|
||
|
||
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
|
||
invalidatePhoneReportTimer()
|
||
print("MQTT5 disconnected: \(err?.localizedDescription ?? "")")
|
||
onDisconnected?()
|
||
}
|
||
}
|
||
|
||
private final class PhoneNetworkStatusMonitor {
|
||
|
||
private let monitor = NWPathMonitor()
|
||
private let queue = DispatchQueue(label: "com.quicklocation.mqtt.network")
|
||
private let lock = NSLock()
|
||
private let telephonyInfo = CTTelephonyNetworkInfo()
|
||
private var path: NWPath?
|
||
|
||
func start() {
|
||
monitor.pathUpdateHandler = { [weak self] path in
|
||
self?.lock.lock()
|
||
self?.path = path
|
||
self?.lock.unlock()
|
||
}
|
||
monitor.start(queue: queue)
|
||
}
|
||
|
||
var statusText: String {
|
||
lock.lock()
|
||
let currentPath = path
|
||
lock.unlock()
|
||
|
||
guard let currentPath, currentPath.status == .satisfied else {
|
||
return "无网络"
|
||
}
|
||
if currentPath.usesInterfaceType(.wifi) {
|
||
return "Wi-Fi"
|
||
}
|
||
guard currentPath.usesInterfaceType(.cellular) else {
|
||
return "无网络"
|
||
}
|
||
return cellularGeneration
|
||
}
|
||
|
||
private var cellularGeneration: String {
|
||
let technologies = telephonyInfo.serviceCurrentRadioAccessTechnology
|
||
.map { Array($0.values) } ?? []
|
||
guard let technology = technologies.first else { return "蜂窝" }
|
||
switch technology {
|
||
case CTRadioAccessTechnologyNR, CTRadioAccessTechnologyNRNSA:
|
||
return "5G"
|
||
case CTRadioAccessTechnologyLTE:
|
||
return "4G"
|
||
case CTRadioAccessTechnologyWCDMA,
|
||
CTRadioAccessTechnologyHSDPA,
|
||
CTRadioAccessTechnologyHSUPA,
|
||
CTRadioAccessTechnologyCDMAEVDORev0,
|
||
CTRadioAccessTechnologyCDMAEVDORevA,
|
||
CTRadioAccessTechnologyCDMAEVDORevB,
|
||
CTRadioAccessTechnologyeHRPD:
|
||
return "3G"
|
||
case CTRadioAccessTechnologyGPRS,
|
||
CTRadioAccessTechnologyEdge,
|
||
CTRadioAccessTechnologyCDMA1x:
|
||
return "2G"
|
||
default:
|
||
return "蜂窝"
|
||
}
|
||
}
|
||
}
|