Initial commit — RoadCode import
This commit is contained in:
222
BlackRoadWatch/Sources/iOS/BlackRoadUDP.swift
Normal file
222
BlackRoadWatch/Sources/iOS/BlackRoadUDP.swift
Normal file
@@ -0,0 +1,222 @@
|
||||
import Foundation
|
||||
import Network
|
||||
import WatchConnectivity
|
||||
import Combine
|
||||
|
||||
// MARK: - UDP Listener for M1s Firmware Broadcasts
|
||||
|
||||
class BlackRoadUDPManager: NSObject, ObservableObject {
|
||||
static let shared = BlackRoadUDPManager()
|
||||
|
||||
@Published var isListening = false
|
||||
@Published var isConnected = false
|
||||
@Published var watchReachable = false
|
||||
@Published var sensorData: SensorData?
|
||||
@Published var aiStatus: AIStatus?
|
||||
@Published var systemHealth: SystemHealth?
|
||||
@Published var lastUpdate: Date?
|
||||
@Published var packetsReceived: UInt64 = 0
|
||||
@Published var sourceAddress: String = ""
|
||||
|
||||
private var listener: NWListener?
|
||||
private var wcSession: WCSession?
|
||||
private let udpPort: UInt16 = 8420
|
||||
private let queue = DispatchQueue(label: "io.blackroad.udp", qos: .userInteractive)
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
setupWatchConnectivity()
|
||||
}
|
||||
|
||||
// MARK: - UDP Listener
|
||||
|
||||
func startListening() {
|
||||
guard listener == nil else { return }
|
||||
|
||||
do {
|
||||
let params = NWParameters.udp
|
||||
params.allowLocalEndpointReuse = true
|
||||
params.requiredInterfaceType = .wifi
|
||||
|
||||
listener = try NWListener(using: params, on: NWEndpoint.Port(rawValue: udpPort)!)
|
||||
|
||||
listener?.stateUpdateHandler = { [weak self] state in
|
||||
DispatchQueue.main.async {
|
||||
switch state {
|
||||
case .ready:
|
||||
self?.isListening = true
|
||||
print("[BR] UDP listener ready on port \(self?.udpPort ?? 0)")
|
||||
case .failed(let error):
|
||||
self?.isListening = false
|
||||
print("[BR] UDP listener failed: \(error)")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
self?.stopListening()
|
||||
self?.startListening()
|
||||
}
|
||||
case .cancelled:
|
||||
self?.isListening = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listener?.newConnectionHandler = { [weak self] connection in
|
||||
self?.handleConnection(connection)
|
||||
}
|
||||
|
||||
listener?.start(queue: queue)
|
||||
} catch {
|
||||
print("[BR] Failed to create UDP listener: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func stopListening() {
|
||||
listener?.cancel()
|
||||
listener = nil
|
||||
DispatchQueue.main.async {
|
||||
self.isListening = false
|
||||
self.isConnected = false
|
||||
}
|
||||
}
|
||||
|
||||
private func handleConnection(_ connection: NWConnection) {
|
||||
connection.start(queue: queue)
|
||||
receiveData(on: connection)
|
||||
}
|
||||
|
||||
private func receiveData(on connection: NWConnection) {
|
||||
connection.receiveMessage { [weak self] data, context, isComplete, error in
|
||||
if let data = data, !data.isEmpty {
|
||||
self?.processPacket(data, from: connection)
|
||||
}
|
||||
if error == nil {
|
||||
self?.receiveData(on: connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - JSON Parsing
|
||||
|
||||
private func processPacket(_ data: Data, from connection: NWConnection) {
|
||||
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let type = json["type"] as? String, type == "br_watch" else {
|
||||
return
|
||||
}
|
||||
|
||||
if let endpoint = connection.currentPath?.remoteEndpoint,
|
||||
case let .hostPort(host, _) = endpoint {
|
||||
let addr = "\(host)"
|
||||
DispatchQueue.main.async { self.sourceAddress = addr }
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.packetsReceived += 1
|
||||
self.isConnected = true
|
||||
self.lastUpdate = Date()
|
||||
|
||||
if let sensorDict = json["sensor"] as? [String: Any] {
|
||||
self.sensorData = SensorData(
|
||||
temperature: sensorDict["temp"] as? Double ?? 0,
|
||||
humidity: sensorDict["hum"] as? Double ?? 0,
|
||||
light: 0,
|
||||
accelX: 0, accelY: 0, accelZ: 0,
|
||||
batteryMV: sensorDict["bat"] as? Int ?? 0,
|
||||
uptimeSec: UInt32(sensorDict["up"] as? Int ?? 0)
|
||||
)
|
||||
}
|
||||
|
||||
if let aiDict = json["ai"] as? [String: Any] {
|
||||
self.aiStatus = AIStatus(
|
||||
modelID: 1,
|
||||
confidence: aiDict["conf"] as? Int ?? 0,
|
||||
inferenceMS: 0,
|
||||
totalInferences: UInt32(aiDict["infers"] as? Int ?? 0),
|
||||
npuLoad: aiDict["load"] as? Int ?? 0,
|
||||
npuTemp: aiDict["temp"] as? Int ?? 0,
|
||||
classID: 0
|
||||
)
|
||||
}
|
||||
|
||||
if let fleetDict = json["fleet"] as? [String: Any] {
|
||||
self.systemHealth = SystemHealth(
|
||||
fleetOnline: fleetDict["on"] as? Int ?? 0,
|
||||
fleetTotal: fleetDict["total"] as? Int ?? 0,
|
||||
agentsActive: fleetDict["agents"] as? Int ?? 0,
|
||||
trafficGreen: fleetDict["green"] as? Int ?? 0,
|
||||
trafficYellow: 0,
|
||||
trafficRed: 0,
|
||||
tasksPending: 0,
|
||||
tasksDone: fleetDict["tasks"] as? Int ?? 0,
|
||||
memoryEntries: UInt32(fleetDict["mem"] as? Int ?? 0),
|
||||
reposCount: fleetDict["repos"] as? Int ?? 0,
|
||||
cfProjects: fleetDict["cf"] as? Int ?? 0,
|
||||
cpuLoad: 0
|
||||
)
|
||||
}
|
||||
|
||||
self.sendToWatch()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Watch Connectivity
|
||||
|
||||
private func setupWatchConnectivity() {
|
||||
guard WCSession.isSupported() else { return }
|
||||
wcSession = WCSession.default
|
||||
wcSession?.delegate = self
|
||||
wcSession?.activate()
|
||||
}
|
||||
|
||||
private func sendToWatch() {
|
||||
guard let session = wcSession, session.activationState == .activated else { return }
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
var context: [String: Any] = [:]
|
||||
|
||||
if let sensor = sensorData,
|
||||
let sensorJSON = try? encoder.encode(sensor),
|
||||
let sensorDict = try? JSONSerialization.jsonObject(with: sensorJSON) {
|
||||
context["sensor"] = sensorDict
|
||||
}
|
||||
if let ai = aiStatus,
|
||||
let aiJSON = try? encoder.encode(ai),
|
||||
let aiDict = try? JSONSerialization.jsonObject(with: aiJSON) {
|
||||
context["ai"] = aiDict
|
||||
}
|
||||
if let health = systemHealth,
|
||||
let healthJSON = try? encoder.encode(health),
|
||||
let healthDict = try? JSONSerialization.jsonObject(with: healthJSON) {
|
||||
context["health"] = healthDict
|
||||
}
|
||||
context["timestamp"] = Date().timeIntervalSince1970
|
||||
|
||||
try? session.updateApplicationContext(context)
|
||||
|
||||
if session.isReachable {
|
||||
session.sendMessage(context, replyHandler: nil)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.watchReachable = session.isReachable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - WCSessionDelegate
|
||||
|
||||
extension BlackRoadUDPManager: WCSessionDelegate {
|
||||
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||
print("[BR] Watch session activated: \(activationState.rawValue)")
|
||||
DispatchQueue.main.async { self.watchReachable = session.isReachable }
|
||||
}
|
||||
|
||||
func sessionDidBecomeInactive(_ session: WCSession) {}
|
||||
func sessionDidDeactivate(_ session: WCSession) { session.activate() }
|
||||
|
||||
func sessionReachabilityDidChange(_ session: WCSession) {
|
||||
DispatchQueue.main.async { self.watchReachable = session.isReachable }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user