App/Sources/DSNSection.swift
73 lines · 3541 bytes
1import Feeds
2import SwiftUI
3
4/// Which Deep Space Network dishes are talking to which spacecraft right now.
5struct DSNSection: View {
6 var ink: Color
7 var accent: Color
8 @State private var status: DSNStatus?
9 @State private var failed = false
10
11 private var activeDishes: [DSNStatus.Dish] {
12 status?.dishes.filter { !$0.links.isEmpty } ?? []
13 }
14
15 var body: some View {
16 VStack(alignment: .leading, spacing: 10) {
17 Text("Deep Space Network now").font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).dimmed(0.6)
18 Text("NASA talks to its deep-space probes through giant dishes at three sites spaced around the world, so one always faces any spacecraft. This is who each site is talking to now.")
19 .font(.footnote).dimmed(0.75)
20 if let status {
21 ForEach(["Goldstone", "Madrid", "Canberra"], id: \.self) { station in
22 let dishes = activeDishes.filter { $0.station == station }
23 VStack(alignment: .leading, spacing: 4) {
24 Text(station).font(.subheadline.weight(.medium))
25 if dishes.isEmpty {
26 Text("No spacecraft in contact").font(.footnote).dimmed(0.6)
27 }
28 ForEach(dishes) { dish in
29 ForEach(dish.links, id: \.self) { link in
30 HStack(alignment: .firstTextBaseline) {
31 Text(link.spacecraft).font(.footnote)
32 Spacer()
33 Text(describe(link, dish: dish)).font(.footnote.monospacedDigit()).dimmed(0.7)
34 }
35 .accessibilityElement(children: .combine)
36 }
37 }
38 }
39 .padding(.vertical, 4)
40 }
41 Text("Updated \(status.updated.formatted(date: .omitted, time: .shortened)). Data: NASA/JPL Deep Space Network.")
42 .font(.caption2).dimmed(0.5)
43 } else if failed {
44 Text("Couldn't load Deep Space Network status.").font(.footnote).foregroundStyle(accent)
45 } else {
46 ProgressView().tint(ink)
47 }
48 }
49 .task {
50 while !Task.isCancelled {
51 do { status = try await DSNStatus.fetch(); failed = false } catch { failed = status == nil }
52 try? await Task.sleep(for: .seconds(30))
53 }
54 }
55 }
56
57 private func describe(_ l: DSNStatus.Link, dish: DSNStatus.Dish) -> String {
58 var parts = [dish.name.replacingOccurrences(of: "DSS", with: "DSS ")]
59 if l.receiving && l.sending { parts.append("two-way") } else if l.receiving { parts.append("receiving") } else { parts.append("sending") }
60 if let r = l.downlinkRate { parts.append(rate(r)) }
61 if let rt = l.roundTrip, rt > 60 { parts.append("round trip \(duration(rt))") }
62 return parts.joined(separator: " · ")
63 }
64
65 private func rate(_ bps: Double) -> String {
66 bps >= 1_000_000 ? String(format: "%.1f Mb/s", bps / 1_000_000) : bps >= 1000 ? String(format: "%.0f kb/s", bps / 1000) : String(format: "%.0f b/s", bps)
67 }
68
69 private func duration(_ s: Double) -> String {
70 let h = Int(s / 3600), m = Int(s.truncatingRemainder(dividingBy: 3600) / 60)
71 return h > 0 ? "\(h) h \(m) m" : "\(m) m"
72 }
73}