krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

Packages/MoonriseKit/Sources/Feeds/DSN.swift

04b2b1d914788057bcd20945b876847386eb8f65
moonrise/Packages/MoonriseKit/Sources/Feeds/DSN.swift history · blame · raw

95 lines · 3754 bytes

9 symbols in this file
 1import Foundation
 2#if canImport(FoundationXML)
 3import FoundationXML
 4#endif
 5
 6/// NASA's Deep Space Network Now feed: what each antenna is talking to.
 7public struct DSNStatus: Sendable {
 8    public struct Link: Sendable, Hashable {
 9        /// Display name, e.g. "Voyager 1".
10        public var spacecraft: String
11        public var receiving: Bool
12        public var sending: Bool
13        /// Bits per second of the active downlink, if any.
14        public var downlinkRate: Double?
15        /// Round-trip light time in seconds, if reported.
16        public var roundTrip: Double?
17    }
18
19    public struct Dish: Sendable, Identifiable {
20        public var name: String
21        public var station: String
22        public var activity: String
23        public var links: [Link]
24        public var id: String { name }
25    }
26
27    public var updated: Date
28    public var dishes: [Dish]
29
30    public static let url = URL(string: "https://eyes.nasa.gov/dsn/data/dsn.xml")!
31
32    public static func fetch(session: URLSession = .shared) async throws -> DSNStatus {
33        try decode(await session.data(from: url).0)
34    }
35
36    public static func decode(_ data: Data) throws -> DSNStatus {
37        let p = Parser()
38        let xml = XMLParser(data: data)
39        xml.delegate = p
40        guard xml.parse() else { throw xml.parserError ?? CocoaError(.coderReadCorrupt) }
41        return DSNStatus(updated: p.updated, dishes: p.dishes)
42    }
43
44    public static func displayName(_ code: String) -> String {
45        dsnSpacecraftNames[code.lowercased()] ?? code
46    }
47
48    final class Parser: NSObject, XMLParserDelegate {
49        var updated = Date.distantPast
50        var dishes: [Dish] = []
51        var station = ""
52        var current: Dish?
53        var signals: [String: (down: Bool, up: Bool, rate: Double?)] = [:]
54        var rtlt: [String: Double] = [:]
55
56        func parser(_ parser: XMLParser, didStartElement name: String, namespaceURI: String?, qualifiedName: String?, attributes a: [String: String]) {
57            switch name {
58            case "station":
59                station = a["friendlyName"] ?? a["name"] ?? ""
60                if let ms = a["timeUTC"].flatMap(Double.init) { updated = Date(timeIntervalSince1970: ms / 1000) }
61            case "dish":
62                current = Dish(name: a["name"] ?? "", station: station, activity: a["activity"] ?? "", links: [])
63                signals = [:]
64                rtlt = [:]
65            case "downSignal", "upSignal":
66                guard let sc = a["spacecraft"], !sc.isEmpty else { return }
67                let active = a["active"] == "true"
68                var s = signals[sc] ?? (false, false, nil)
69                if name == "downSignal" {
70                    s.down = s.down || active
71                    if active, let r = a["dataRate"].flatMap(Double.init), r > 0 { s.rate = r }
72                } else {
73                    s.up = s.up || active
74                }
75                signals[sc] = s
76            case "target":
77                if let n = a["name"], let t = a["rtlt"].flatMap(Double.init), t > 0 { rtlt[n] = t }
78            default:
79                break
80            }
81        }
82
83        func parser(_ parser: XMLParser, didEndElement name: String, namespaceURI: String?, qualifiedName: String?) {
84            guard name == "dish", var dish = current else { return }
85            dish.links = signals
86                .filter { $0.value.down || $0.value.up }
87                .map { code, s in
88                    Link(spacecraft: DSNStatus.displayName(code), receiving: s.down, sending: s.up, downlinkRate: s.rate, roundTrip: rtlt[code])
89                }
90                .sorted { $0.spacecraft < $1.spacecraft }
91            dishes.append(dish)
92            current = nil
93        }
94    }
95}