Browse Source
feat: real-time 1s ping monitor to Soniox with dynamic orb color (green/yellow/orange/red) and millisecond HUD readout
main
feat: real-time 1s ping monitor to Soniox with dynamic orb color (green/yellow/orange/red) and millisecond HUD readout
main
3 changed files with 153 additions and 14 deletions
@ -0,0 +1,93 @@ |
|||||
|
import Foundation |
||||
|
|
||||
|
public enum PingQuality { |
||||
|
case good(ms: Int) // < 450 ms -> Emerald Green |
||||
|
case medium(ms: Int) // 450 - 750 ms -> Warm Yellow/Lime |
||||
|
case high(ms: Int) // 750 - 1500 ms -> Amber Orange |
||||
|
case disconnected(ms: Int?) // > 1500 ms or Timeout/Error -> Crimson Red |
||||
|
|
||||
|
public var labelText: String { |
||||
|
switch self { |
||||
|
case .good(let ms): return "\(ms)ms" |
||||
|
case .medium(let ms): return "\(ms)ms" |
||||
|
case .high(let ms): return "\(ms)ms" |
||||
|
case .disconnected(let ms): |
||||
|
if let ms = ms { |
||||
|
return "\(ms)ms" |
||||
|
} else { |
||||
|
return "OFF" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public final class PingMonitor { |
||||
|
public static let shared = PingMonitor() |
||||
|
|
||||
|
private let targetURL = URL(string: "https://translate.compare.soniox.com/")! |
||||
|
private var session: URLSession! |
||||
|
private var timer: Timer? |
||||
|
private var isRunning = false |
||||
|
|
||||
|
public var onPingUpdate: ((PingQuality) -> Void)? |
||||
|
public private(set) var currentQuality: PingQuality = .medium(ms: 380) |
||||
|
|
||||
|
private init() { |
||||
|
let config = URLSessionConfiguration.default |
||||
|
config.timeoutIntervalForRequest = 2.5 |
||||
|
config.timeoutIntervalForResource = 2.5 |
||||
|
config.waitsForConnectivity = false |
||||
|
config.requestCachePolicy = .reloadIgnoringLocalCacheData |
||||
|
self.session = URLSession(configuration: config) |
||||
|
} |
||||
|
|
||||
|
public func start() { |
||||
|
guard !isRunning else { return } |
||||
|
isRunning = true |
||||
|
measurePing() |
||||
|
DispatchQueue.main.async { [weak self] in |
||||
|
self?.timer?.invalidate() |
||||
|
self?.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in |
||||
|
self?.measurePing() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public func stop() { |
||||
|
isRunning = false |
||||
|
timer?.invalidate() |
||||
|
timer = nil |
||||
|
} |
||||
|
|
||||
|
private func measurePing() { |
||||
|
var req = URLRequest(url: targetURL) |
||||
|
req.httpMethod = "HEAD" |
||||
|
let t0 = Date() |
||||
|
|
||||
|
let task = session.dataTask(with: req) { [weak self] _, resp, _ in |
||||
|
let elapsed = Date().timeIntervalSince(t0) * 1000.0 |
||||
|
let ms = Int(elapsed) |
||||
|
|
||||
|
let quality: PingQuality |
||||
|
if let http = resp as? HTTPURLResponse, (200...399).contains(http.statusCode) { |
||||
|
if ms < 450 { |
||||
|
quality = .good(ms: ms) |
||||
|
} else if ms <= 750 { |
||||
|
quality = .medium(ms: ms) |
||||
|
} else if ms <= 1500 { |
||||
|
quality = .high(ms: ms) |
||||
|
} else { |
||||
|
quality = .disconnected(ms: ms) |
||||
|
} |
||||
|
} else { |
||||
|
quality = .disconnected(ms: nil) |
||||
|
} |
||||
|
|
||||
|
DispatchQueue.main.async { |
||||
|
self?.currentQuality = quality |
||||
|
self?.onPingUpdate?(quality) |
||||
|
} |
||||
|
} |
||||
|
task.resume() |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue