A deep technical reference for the OrbitDeck for iOS codebase — subsystem by subsystem — so you can read it, extend it, or verify the math. It documents the actual types, algorithms and data flow, not just the shape of the app.
OrbitDeck is a native SwiftUI application for iPhone and iPad (iOS/iPadOS 17+) that computes satellite passes, Doppler, look angles, footprints, link budgets, and a large set of orbital-analysis and operating tools entirely on-device from live orbital elements. It is a companion to the OrbitDeck desktop app and CardSat by Paul Stoetzer, N8HM.
The architecture is deliberately layered and framework-light: one observable store holds all state, pure engine types do the math, thin service types fetch data and talk to the system, and SwiftUI views render it. There is no external app framework and no database. Roughly 33,000 lines of app code — Views ~12.5k, Engine ~9.3k, Services ~3.5k, rig/rotator/audio/logging/digital-mode subsystems ~6k, Models ~1k — plus two vendored C/Swift libraries: the SGP4/SDP4 propagator (SatelliteKit) and the MIT-licensed ft8_lib (FT4/FT8).
| Concern | Choice |
|---|---|
| UI | SwiftUI (declarative); MapKit for maps; Canvas for polar/sky plots; Core Graphics for PDF export |
| Concurrency | Swift async/await and Task/Task.detached. Combine is intentionally avoided in favor of async APIs and @Published. |
| Propagation | SatelliteKit (SGP4/SDP4) — vendored in the repo as a local Swift package |
| Persistence | UserDefaults (preferences as JSON), the Keychain (secrets), and on-disk JSON caches under Application Support (GP catalog, SatNOGS DB, space weather) |
| Minimum OS | iOS/iPadOS 17 |
| Dependencies | One: SatelliteKit. No analytics, ads, or tracking SDKs. |
The app target lives under OrbitDeckIOS/, split by role. The propagation library is vendored under SatelliteKit/.
| Path | Contents |
|---|---|
Models/OrbitStore.swift | The single source of truth (@MainActor observable store) plus OrbitSecretStore (Keychain). |
Models/SatelliteModels.swift | The value types: ObserverSite, SatelliteRecord, TransponderRecord, PredictedPass, LiveLook, SkyPoint, RadioCalibration, StorePreferences, ManualSatelliteDefinition, enums. |
Engine/OrbitPredictor.swift | Look angles, pass prediction, ground track, Doppler, footprint, beta angle. |
Engine/FeatureEngine.swift | Sun/Moon/planet positions, Maidenhead/VUCC, footprint sets, space-weather fetch, EME, transits, zones, orbital history, MUF. |
Engine/FeatureCompletionEngine.swift | Link budgets, radio playbook, EME band analysis, satellite eclipse timelines, conjunction detail. |
Engine/ParityPlanningEngine.swift | Workable grids/states/DXCC, mutual and rove windows, satellite↔satellite LOS, visible passes, horizon masks. |
Engine/AstronomyParityEngine.swift | Aurora outlook, occultations, appulses, solar/lunar eclipses and shadow tracks. |
Engine/DeepParityEngine.swift | The Tiny BASIC interpreter, the bench-tool math, and OrbitDecayModel. |
Engine/UtilityEngine.swift | The safe math evaluator, the graphing engine, and shared calculator plumbing. |
Engine/DXCCData.swift | The DXCC entity roster and GeoEntityLookup (reverse geocode → DXCC + ADIF/LoTW subdivisions). |
Engine/DXCCNumericData.swift | ARRL numeric entity codes and representative coordinates (used by Tiny BASIC). |
Engine/WorldMapData.swift | Public-domain Natural Earth coastline polygons for the flat/globe maps. |
Services/ | GPService, TransponderService, AO7Service, LocationProvider, ExportService, XLSXExportService, PassAlarmService. |
Views/ | All SwiftUI screens: RootView, HomeView, GroundTrackView, PassesView, RadioView, the multi-screen files (FeatureViews, AdvancedFeatureViews, DeepParityViews, UtilityFeatureViews), and shared UI (Components, AppTheme). |
OrbitDeckIOSApp is the @main entry point. It owns two @StateObjects — the OrbitStore and a NotificationRouter — and injects both as environment objects into RootView, forces the dark color scheme, and on launch runs a single .task that activates notifications and calls store.bootstrap():
WindowGroup {
RootView()
.environmentObject(store)
.environmentObject(notifications)
.preferredColorScheme(.dark)
.task { notifications.activate(); await store.bootstrap() }
}
bootstrap() loads the cached catalog, transponder database and space-weather snapshot from disk (so the app is usable offline immediately), then performs the cadence checks that trigger a background refresh only when data is stale.
OrbitStore is an @MainActor final class OrbitStore: ObservableObject, created once and shared via @EnvironmentObject. It is the single source of truth for the whole app.
satellites: [SatelliteRecord] — the working catalog.preferences: StorePreferences — every user setting (see below); its didSet persists and syncs the time-zone flag.spaceWeather: SpaceWeatherSnapshot?, currentLocationEntity: GeoLocationEntity?.isRefreshingGP, isRefreshingTransponders, statusMessage, lastError, lastGPRefresh.| Method | Role |
|---|---|
bootstrap() | Load caches, refresh what's stale. |
refreshGP() | Fetch elements via GPService, respect the CelesTrak cadence, cache to disk. |
refreshAllTransponders() / …IfNeeded() | Download the SatNOGS DB and apply it to every satellite; the "if needed" variant only runs when the cache is missing or older than about a week. |
refreshSpaceWeatherIfNeeded(maxAge:) | Refresh solar/geomagnetic indices when stale (about hourly) or on foreground. |
select(_:), setLocationMode(_:), applyCurrentLocation(latitude:longitude:altitudeMeters:) | Selection and observer-position management. Switching to fixed restores the preserved savedFixedSite; following writes the live fix into the observer under the "Current location" name. |
refreshLocationEntity() | Reverse-geocode the observer into currentLocationEntity, throttled to a ~1 km key so a moving fix doesn't spam the geocoder. |
calibration(for:), setCalibration(_:for:), downlinkCalibrationHz(for:invert:) | Per-satellite radio calibration; the last folds the stored offsets into a single receive-referred correction (uplink sign-flipped on an inverting transponder). |
operatorGrid, operatorGrid6, operatorVuccGrids | Maidenhead locators and the claimable VUCC grid list for the current observer. |
StorePreferences is one Codable struct; its didSet JSON-encodes it into UserDefaults under a versioned key, so every setting is saved the instant it changes and there is no separate save step. New fields are added as Optional so older stored blobs keep decoding without a migration. The GP catalog, SatNOGS database and space-weather snapshot are cached as JSON files under Application Support. Secrets (QRZ and Space-Track passwords) go through OrbitSecretStore to the iOS Keychain — never the preferences JSON. CelesTrak requests are gated by a per-dataset two-hour timestamp so normal use can't trip its rate limit.
@Published, any screen reflects a preference change immediately; SwiftUI re-renders what changed and the app needs no separate reload plumbing.All domain types are value types (mostly Sendable) in SatelliteModels.swift, so heavy work moves off the main actor freely.
ObserverSite — name, latitude, longitude, altitude; satelliteKitLocation bridges to SatelliteKit; coarseKey (~100 m) and stableKey (~1 km) are rounded strings used to key recomputes so GPS jitter doesn't thrash them.TransponderRecord — up/downlink low/high, mode, invert, type, baud, service. Computed: isLinear (needs both passbands and a transponder type or ≥5 kHz width), bandwidth, downlinkCenter/uplinkCenter, isTwoWay, and a human kind ("Linear (inverting)", "FM", "CW / Beacon", "Data (BPSK…)").SatelliteRecord — identity, epoch, the six mean elements plus B*, a SatelliteKit Elements, its transponders, and an isManual flag. Computed: periodMinutes, semiMajorAxisKm, apogeeKm/perigeeKm, elementAgeDays.LiveLook — one topocentric observation: az/el, range, range-rate, sub-point lat/lon/alt, sunlit, beta angle, footprint radius.PredictedPass — AOS/TCA/LOS, max elevation, AOS/LOS azimuth, duration. SkyPoint — a timestamped az/el sample.RadioCalibration (downlink/uplink Hz), StorePreferences (observer, favorites, min elevation, GP source, calibrations, location mode, useLocalTime, …), GPSourceKind, LocationMode, ManualSatelliteDefinition, LabOrbitDefinition.OrbitPredictor is the single wrapper over SatelliteKit. Each call builds a Satellite(elements:), asks it for TEME position/velocity at a Julian date, and derives the rest. Everything is pure and throwing; callers move heavy scans off-main with Task.detached.
| Function | What it computes |
|---|---|
look(_:observer:at:) | Full topocentric LiveLook — azimuth/elevation/range from topPosition, range-rate (including the observer's Earth-rotation velocity), the geodetic sub-point, sunlit test, beta angle and footprint radius. |
predictPasses(…minElevation:maxCount:horizonDays:) | A coarse 30 s rise/set scan refined by bisection to sub-second AOS/LOS, with TCA found by golden-section search. Handles a pass already in progress and continuously-visible high orbits. |
currentOrNextPass, nextEvent | The pass in progress or the next one (ignoring the display minimum so a track always has an arc); the next horizon event with its max elevation. |
groundTrack, subpoint, skyPath | A lat/lon/alt sample series over a window; the instantaneous sub-point; an az/el track for one pass (~20 s steps). |
equatorCrossings | Interpolated latitude-zero crossings (ascending/descending) — the basis of the OSCARLOCATOR reference-orbit tables. |
dopplerFrequencies(downlinkHz:uplinkHz:rangeRateKmS:…) | One-way Doppler dials: with β = vrange/c, the receive dial uses f·(1−β) and the transmit dial f/(1−β), plus an optional calibration term folded into receive. |
passbandFrequencies(_:offsetHz:) | Linear-transponder tuning: an offset from center, walking the uplink the opposite way on an inverting transponder. |
footprintRadius(altitudeKm:) | The 0°-elevation surface radius, Re·acos(Re/(Re+h)). The Ground Track draws the matching geodesic circle as an explicit boundary polygon (see below). |
betaAngle, isSunlit | The angle between the orbit-plane normal and the Sun unit vector; a cylindrical-shadow illumination test. |
OrbitDecayModel (in DeepParityEngine.swift) is a King-Hele style atmospheric-decay integrator. estimate(…) returns a lifetime in days and a DecayAnchor describing what it trusted: a measured decay rate (observed n-dot) where the elements support it, otherwise the fitted B*. It integrates the semi-major axis against an exponential atmosphere (banded reference densities and scale heights from ~100–1000 km), applies a King-Hele eccentricity correction (via modified Bessel functions), scales density by a low/mean/high solar-activity factor, and stops at re-entry altitude or a long-life cap. lifetimeFromArea(…) derives the ballistic coefficient from physical inputs (Cd·A/m) for a spacecraft still on the bench — the basis of the orbit-lifetime and debris-compliance bench tools.
FeatureEngine is the largest engine and backs most Analysis and Sky screens. Highlights:
gridToLatLon/latLonToGrid4/latLonToGrid6 implement Maidenhead both ways; vuccGrids(lat,lon,tolerance) returns the 1–4 grids you may claim on a line or corner, treating a fix within ~6.1 m (the ARRL 20-ft GPS rule) of a boundary as on it.sunMoon and skyObjects give az/el, phase and illumination; planet positions come from a compact VSOP-style element set converted to geocentric RA/Dec then to horizon coordinates via local sidereal time.workableGrids/workableGridsNow/…AcrossNextPass enumerate the grids inside the footprint (an angular-distance test against a grid lattice); bestPassesForTarget finds passes where both your station and a target sit inside the footprint at once.SpaceWeatherSnapshot.FeatureCompletionEngine adds the radio and EME math: linkBudget (EIRP − FSPL − losses), the Doppler radioPlaybook (hold-RX or hold-TX rows across a pass), EME band analysis (Doppler, Faraday rotation, sky temperature, libration spread, path degradation vs. lunar distance) and satellite eclipse timelines. AstronomyParityEngine adds the aurora outlook (a Kp-driven auroral-boundary magnetic-latitude test), lunar occultations, planetary appulses, and solar/lunar eclipse detection with shadow ground tracks.
ParityPlanningEngine backs the Workable, Mutual, Rove and visible-pass screens. It exposes snapshot types (WorkableSetSnapshot of grids/states/DXCC, MutualWindowRecord, RovePassRecord, SatelliteLOSWindow, VisiblePassRecord, HorizonMask) and the functions that build them: workableNow/workableAcrossNextPass/workableHorizon, targetSearch, mutualWindows (times two stations both see a satellite), satelliteLOSWindows (a ray-sphere line-of-sight test between two satellites), visiblePasses (an optical-magnitude estimate applied when the satellite is sunlit, the Sun is well down, and elevation is adequate), and trim (clip a pass to a four-point interpolated horizon mask). US-state membership uses a bundled centroid table; DXCC membership uses the footprint against the DXCC roster.
UtilityEngine holds the pieces shared by the calculators. SafeMathEvaluator is a hand-written lexer/parser/evaluator for the scientific calculator: infix expressions with correct precedence, SI metric-prefix literals (100k, 2.2n), the usual trig/hyperbolic/log/exp/factorial/nCr/nPr functions, physical constants (c, kb, Re, mu, g0, pi, e), and amateur-radio/orbit helpers (fspl, dop, wavelength↔frequency, dB conversions, SWR/return-loss, noise-figure↔temperature, orbital period/velocity/footprint, slant range, dish gain). GraphCalculatorEngine samples a function across a range for the graphing calculator (trace, roots, integral, CSV). An older lightweight TinyBasicEngine also lives here alongside the graphics-op types shared with the full interpreter.
DeepParityEngine.swift contains the full Tiny BASIC interpreter — CardSatTinyBasicEngine (the front-end and static helpers), BasicVM (the execution state machine: variables, string variables, named and anonymous arrays, FOR/GOSUB stacks, DATA pointer), BasicExpressionParser (a recursive-descent numeric/string parser with the documented precedence), and TinyBasicHostContext (the immutable bridge that exposes ~60 live read-only variables such as SATAZ, AOSIN, SFI, LSHELL). It tokenizes by splitting statements on colons and arguments on commas (respecting quotes and parentheses), dispatches statements by keyword, renders immediate-mode graphics into a 240×135 buffer, sandboxes file I/O to a private directory, and bounds execution by statement count and wall-clock time. The dialect is documented end-to-end on the Tiny BASIC page.
The bench tools are data-driven: BenchTools.allTools is a list of ToolDefinitions (id, category, fields), and a compute function switches on the id to produce ToolResults. Categories include Antennas & feedline (yagi, quad, helix, coax, match), RF & measurement (attenuator, cascade, imd, exposure), Electronics & power (pll, thermal, crosssection), Terrestrial VHF/UHF (tropo, terrainLOS), Satellite & orbit (slant, kepler, footprint, orbitLifetime, debrisCompliance, deltaV, stateVector, orbitalThermal, linkMargin), and General (sciCalc, programmer, unitConverter, dxccLookup, gridConvert). Adding a calculator is a definition plus a case.
DXCCData holds the DXCC entity roster (DXCCEntity: prefix, name, representative lat/lon) and a search helper; DXCCNumericData holds the ARRL numeric entity codes and coordinates that Tiny BASIC addresses by number. The interesting logic is GeoEntityLookup, which turns a coordinate into a GeoLocationEntity (DXCC entity + primary/secondary subdivision):
lookup(latitude:longitude:) reverse-geocodes with CLGeocoder, resolves the DXCC entity from an ISO-3166 → DXCC map (US and GB are split on the administrative area for Alaska/Hawaii and the four UK home nations), and normalizes the subdivisions to the names LoTW/TQSL expect.adifPrimary maps a US state name to its two-letter code. usSecondary applies a county-designator-first rule: a subdivision ending in "County/Parish/Borough/Census Area/Municipality" is a county (bare LoTW name); otherwise it is treated as independent-city territory.independentCityName handles the US independent cities — Virginia's (with "City" appended to the four that collide with a county — Fairfax City, Franklin City, Richmond City, Roanoke City), plus Baltimore, St. Louis and Carson City.Services are thin and stateless where possible; the store owns caching and cadence.
GPService — fetches General Perturbations elements from the AMSAT daily bulletin, a CelesTrak group, or a custom URL, and parses OMM (JSON/XML/CSV) and classic TLE (with checksum validation) into SatelliteRecords via SatelliteKit Elements. It exposes the CelesTrak group list, a browser-style User-Agent, per-NORAD "extra" fetches for New Launches, and makeManualRecord for hand-entered elements.TransponderService — downloads the SatNOGS transmitter database (per-NORAD or the whole set), keeps rows with a real downlink, and builds TransponderRecords keyed by NORAD id.AO7Service — fits AMSAT status reports of AO-7's A/B mode to a periodic switch model (coarse then fine search, positive/negative report weighting), returning the current mode, the next switch time, and a confidence figure — windowed by the satellite's last eclipse exit so stale phase is ignored.ActivationService & HamsatAlertService — read the hams.at upcoming-activations Atom feed (with a browser User-Agent) and compute mutual-visibility windows for "Can I work it?"; and post an activation via hams.at's authenticated POST /api/alerts. The posting client is split into pure request-building, validation and response-parsing behind a HamsatTransport seam, so it is unit-tested against a local mock of the hams.at server; the API key is stored in the Keychain.QRZService — QRZ XML callsign lookup using the operator's subscription credentials (password from the Keychain).AmsatStatusService — the AMSAT status API (summary, catalog and reports), catalog-name matching to the GP catalog, and attributed status submission, used by the AMSAT Status screen and the Home one-tap report card.FeatureEngine described above.The CAT/ group implements direct transceiver control (Doppler tuning). It is split into a data model, protocol codecs, transports, an orchestrator and its UI:
RigTypes.swift — the radio catalog (RadioCatalog of RadioSpecs ported from CardSat: CI-V addresses, bauds, full-duplex/RX-only/LAN flags, MAIN/SUB select bytes, sat-mode and tone sub-commands), the persisted CATConfig/RigSlot/CATTuning, the RigMode/RigFamily/RigRole/CATTransportKind enums, and the 39-tone CTCSS table. The config types use tolerant custom Codable (decode-if-present with defaults) so adding fields in a later release never fails to decode a saved setup.CATCodec.swift — pure, stateless frame builders/parsers for every dialect: Icom CI-V (little-endian BCD frequency, mode, MAIN/SUB select, sat mode, CTCSS, 07 D2 band assignment, read/parse), Yaesu 5-byte binary (FT-847 SAT-RX/TX opcodes, FT-817 family, FT-100 little-endian, VR-5000, FT-736R), and Kenwood ASCII (TS base FA/FB/MD, the TS-2000/790 satellite-mode (SATL) entry/exit handshake with dual-control band select, and the TH-D74/D75 band-B FQ/MD/FO with fine-step handling). Narrow FM on FM birds is a mode/filter path (CI-V 06 05 <filter>) driven from the transponder mode. Being pure, they are unit-testable without hardware.CATTransport.swift — the CATTransport abstraction and BLESerialTransport, a CoreBluetooth UART client (Nordic UART or a generic write+notify pair) that scans-and-matches the chosen peripheral by identifier and exposes async send / read. A BLEScanner backs the Settings device picker.IcomNetwork.swift — IcomNetworkTransport, a byte-exact port of Icom's RS-BA1 UDP protocol (control + serial streams, the login/auth/ConnInfo handshake with the passcode cipher, a 100 ms idle/ping keepalive with a data watchdog, and CI-V tunnelling), built on Network.framework. Transient path loss (.waiting) is ridden through rather than treated as a drop, so a brief Wi-Fi blip no longer forces a re-login; the audio stream re-establishes after a reconnect.RigController.swift — a @MainActor ObservableObject that owns the configured radios and the tuning loop. It computes the corrected dials exactly as Home does (OrbitPredictor.dopplerFrequencies + per-satellite calibration + passband + transverter LO), applies FM/linear deadbands and a predictive lead, and routes each leg to the right radio and VFO. It also implements the One True Rule read-back: it reads the followed leg and folds an operator dial move into the passband offset so the other leg stays transponder-correct. The loop is driven by a DispatchSource timer on the main queue with an overlap guard, and all command pacing/reads use GCD timing — not a MainActor Task.sleep loop, which is unreliable on device and would freeze the UI. Config persists to its own UserDefaults key, decoupled from StorePreferences.CATViews.swift — the Rig Control configuration screen (presented as a sheet from Settings so it can't wedge the split-view navigation) and the Home Rig control card (connect/disconnect, live dials, Doppler toggle), which tracks the Home transponder and passband selection.The Rotator/ group steers an az/el antenna rotator to follow the selected satellite. It mirrors the CAT design — data model, codecs, transports, an orchestrator and UI — and reuses CAT's BLE transport and network primitives:
RotatorTypes.swift — the RotatorProtocolKind enum (GS-232A/B, Easycomm I/II/III, SPID Rot2Prog, SAEBRTrack, rotctld, PstRotator, OZ9AAR URC, with network/TCP/default-port traits), the RotAzRange axis convention (0–360 / −180…+180 / 0–450° overlap), and the persisted RotatorConfig — the full set of CardSat options that apply on iOS (protocol, transport endpoint, pre-position lead, a tracking slew lead, 450° lookahead, alignment offsets, deadband, magnetic correction, park az/el, flip, minimum elevation, update rate). It uses the same tolerant custom Codable as the CAT config, in its own UserDefaults key.RotatorCodec.swift — pure, stateless command builders and position parsers: GS-232 (W aaa eee), Easycomm I integer / II&III decimal (AZ… EL…), the SPID Rot2Prog 13-byte binary frame, SAEBRTrack's compact whole-degree AZnnnELnnn, rotctld (P az el / p / S), PstRotator's <PST><TRACK>az el</TRACK></PST> UDP command, and OZ9AAR URC's JSON ({"GOTO":[az,el]} / {"POLL"}). The GS-232/Easycomm/SPID/PstRotator builders are ported byte-for-byte from CardSat's rotator.cpp; SAEBRTrack and URC follow the OscarWatch reference. Each protocol applies its own final clamp.RotatorNetwork.swift — RotatorNetworkTransport, a Network.framework client conforming to CAT's CATTransport so the controller reuses the same plumbing. It does TCP for rotctld and OZ9AAR URC, and UDP for PstRotator (binding the local source port to dest+1 as CardSat does), and awaits the send completion so a real socket error surfaces on the Home card instead of being silently dropped. Serial rotators (including SAEBRTrack) reuse CAT's BLESerialTransport.RotatorController.swift — a @MainActor ObservableObject running the pointing loop on a DispatchSource timer (same GCD-timing discipline as the rig controller). Each tick computes the live look angle, then branches to track (applying alignment offsets, flip for overhead passes, the 450° overlap pre-commit and axis normalization, and optional magnetic-declination correction), pre-position to the AOS bearing within the lead window, or park. A deadband suppresses needless moves; on the fire-and-forget UDP path a short keep-alive resends to self-heal a dropped datagram, while reliable transports send only on real movement.RotatorViews.swift — the Rotator configuration screen (presented as a sheet from Settings, like the CAT screen) with protocol, connection, pointing, alignment and park sections, and the Home Rotator card (connect/disconnect, commanded az/el, mode and transmit status). The card shares a ControlStatusHeader with the rig card for a consistent look.rotctld -m 1); OZ9AAR URC follows the documented TCP/JSON format. The serial protocols (GS-232, Easycomm, SPID, SAEBRTrack) are byte-faithful to their references and await on-hardware confirmation, as with the CAT serial radios. The Green Heron RT-21 Az/El is intentionally not offered — it needs two independent serial links (one per axis), which a single BLE adapter can't drive; reach it via rotctld.
Four groups added in 0.9.14 give OrbitDeck operating features beyond tracking. They reuse the CAT/rotator conventions (a controller + its own persistence, GCD timers, Home cards gated on configuration).
Log/ — the QSO log. QSOTypes (records + tolerant-Codable config), QSOStore (JSON on disk), ADIF (import/export), Gzip (pure-Swift RFC-1952 via the Compression framework + a CRC-32), LoTW (on-device .tq8: SecPKCS12Import of the user's certificate, per-QSO SIGNDATA and the RSA-PKCS1v15-SHA1 signature via SecKeyCreateSignature, ported byte-for-byte from CardSat's validated implementation, incl. LoTWSatName normalization), Cloudlog (JSON API), and LogViews (Log screen, editor, settings, Home quick-log card).Audio/ — a CATTransport-style abstraction: AudioSource with USBAudioSource (AVAudioEngine over a class-compliant USB interface) and IcomAudioSource (the RS-BA1 network audio stream — experimental), an AudioHub that tracks availability and vends the active source, and AudioDSP (Goertzel, an arctan FM discriminator, a ring buffer).Recording/ — PassRecorder streams the received audio to a WAV via AVAudioFile; the Home card + Log listing show it.DigitalModes/ — SSTV/ (a generic segment-based decoder over the demodulated subcarrier, VIS auto-detect, its own gallery + a Photos export), and full-duplex FT4Engine/FT4Views built on the vendored MIT ft8_lib (bridged to Swift): UTC-slot RX decode while synthesizing the uplink, with CAT PTT (CI-V/Yaesu/Kenwood/rigctld/Icom-net via RigController.setPTT) or a manual-PTT indicator.The CAT group also gained a rigctld transport in 0.9.14: a Hamlib NET rigctl TCP client selectable as a connection type, so any Hamlib-supported radio can be driven (full-duplex split, or a single leg).
LocationProvider is a @MainActor ObservableObject wrapper over CLLocationManager that serves two independent consumers: the compass (which needs location fixes so Core Location can resolve true heading from magnetic) and the observer follow. startHeading/stopHeading and startFollowing/stopFollowing track those consumers separately so releasing one doesn't cut updates the other still needs; setPrecise switches between a battery-friendly ~100 m accuracy with a 50 m distance filter and full best-accuracy with no filter (used by the Grid Finder). It also offers opt-in reverse geocoding: with geocodeEnabled set, each fix is geocoded (throttled to ~1 km) and published as entity, which the Grid Finder reads. The shared "current location" follow that drives observer-relative screens is coalesced to ~1 Hz in RootView so live screens don't re-render faster than once per second.
ExportService renders PDFs with Core Graphics and writes text formats. PDFs include the pass schedule, the OSCARLOCATOR reference-orbit tables (equator crossings, skipping geosynchronous birds), the illumination raster, the multi-day progression, the two-site mutual-windows report (paired polar sky plots), and a comprehensive per-satellite analysis sheet. Text exports include pass CSV (full and compact), stepped listings, two-site comparisons, merged favorites, an ICS calendar (with optional lead-time alarms), and JSON. XLSXExportService is a small, dependency-free Office-Open-XML writer that emits a valid .xlsx (a stored ZIP of the required XML parts, a bold frozen header row, and an auto-filter) so spreadsheets export without a third-party library. Everything is shared through the standard iOS share sheet.
PassAlarmService schedules a local UNUserNotification at AOS minus the chosen lead time, with a deterministic id per satellite+pass so it can be queried and cancelled; it throws if the pass is too imminent and requests authorization lazily the first time you schedule one. The NotificationRouter (in RootView.swift) is the UNUserNotificationCenterDelegate: tapping a pass reminder sets a flag that RootView observes to switch the selection to Home.
RootView is a NavigationSplitView (balanced style): a sidebar of grouped destinations and a detail pane. Every screen is a case of the OrbitDestination enum — each with a title, an SF Symbol, and a usesSelectedSatellite flag — organized into the groups LIVE, PASSES, ANALYSIS, OPERATING TOOLS, SKY & SPACE, and CATALOG & CONFIGURATION. destinationView(_:) maps a case to its view. Notable details: reading-oriented screens are capped to ~720 pt and centered on iPad while full-bleed screens (globe, radar, ground track) use the whole pane; a transient nil selection resolves to the last real screen so the detail never flashes Home; the scene-phase handler refreshes stale space weather and catalogs on foreground and releases GPS on background; and an idle-timer hook keeps the screen awake only while Home is showing and the app is active.
AppTheme defines ODTheme (the dark palette — background, panel, accent, good, warning, muted, grid, map colors), the odPanel() card modifier and ODFieldStyle text-field style, and the two workhorse layout views MetricRow (label + right-aligned monospaced value, scaled to avoid wrapping) and SectionCard (titled panel).
Components holds the cross-cutting pieces:
ODFormat — all date/number formatting. A global useLocalTime flag (mirrored from the store) switches every time between UTC and the device zone; local formatters honor the device 12/24-hour setting while UTC stays 24-hour, and primaryClock/secondaryClock render a time in the chosen zone and the other zone for the dual clocks in pass lists.Binding.snapping(to:within:) — a center-detent for sliders whose neutral is the middle (passband, calibration, node longitude, time-freeze).PolarSkyPlot — a Canvas polar sky chart (elevation rings, compass rose, track, AOS/LOS and live direction-of-travel arrows, optional compass-up orientation).CurrentLocationEntityInfo, SelectedSatelliteHeader, PassAlarmButton/PassAlarmUnavailable, the satellite switcher/picker, and GeoLocationEntity.Several patterns recur across the live screens and are worth understanding before editing them:
TimelineView(.periodic(from:by:)), anchored to a stable date so frequent re-renders don't re-fire it faster than 1 Hz. Ad-hoc Task.sleep loops proved unreliable on device, so the codebase standardizes on TimelineView.@State, recomputed only when its key changes — so it always draws without depending on a cancellable .task that a split-view transition might kill.coarseKey (~100 m) or stableKey (~1 km) so a jittering GPS fix doesn't restart multi-second recomputes (pass lists, the Daily Schedule).Task.isCancelled, so a spurious task cancellation during a navigation transition can't leave it stuck on "Computing…".Where to find each screen:
| File | Screens |
|---|---|
HomeView.swift | Home (live track with the memoized arc, transponder/Doppler cards with passband + calibration sliders, the CAT rig-control and rotator cards, one-tap AMSAT status reporting, fleet dashboard) and the Grid Finder (VUCC compass dials + proximity map). |
GroundTrackView.swift | Ground Track — a MapKit map with the sub-point path, a live heading marker, and the geodesic footprint drawn as an explicit 0°-elevation boundary polygon (walked by bearing from the sub-point) instead of a projected MapCircle. |
PassesView.swift | Next Passes (quality-scored list, pass-detail sheet) and the Daily Schedule (per-satellite streaming, day grouping, pin-to-now). |
TrackView, SkyRadarView, OrbitalAnalysisView, IlluminationView, PassDetailView, TenDayView, RadioView, SatellitesView, SettingsView | The standalone screens (real-time polar plot; all-sky radar; orbital analysis; illumination; pass detail; pass progression; the Radio/Doppler screen; the catalog; and Settings/About/Calibrations). |
FeatureViews.swift | Workable, Mutual windows, Sun/Moon, Sky Map, Celestial, EME, Transits, Orbital Zones, Astronomy, Orbital History, Space Wx, MUF, Propagation, New Launches, Sites, Activations/QRZ, AMSAT Status, Sky at a Glance, Planning, Conjunctions, and the DX Doppler sheet. |
AdvancedFeatureViews.swift | OSCARLOCATOR Sim (live/QTH projections, A/B reference-orbit comparison), Reference Orbits, and Exports. |
DeepParityViews.swift, UtilityFeatureViews.swift | Tools, the scientific and graphing calculators, Tiny BASIC, Learn, References, and the 3D globe. |
CAT/CATViews.swift | Rig Control (CAT) configuration sheet and the Home rig-control card. Engine, transports and codecs live alongside it in the CAT/ group (see Rig control). |
Rotator/RotatorViews.swift | Rotator control configuration sheet and the Home rotator card. Codec, transport and controller live alongside it in the Rotator/ group (see Rotator control). |
Log/LogViews.swift | The Log screen (QSOs + pass recordings), QSO editor, Log settings and the Home quick-log card (see Logging). |
DigitalModes/SSTV/SSTVViews.swift, DigitalModes/FT4Views.swift, Recording/RecordingViews.swift | The SSTV Images gallery + viewer, the Home SSTV / FT4 / pass-recording cards (all gated on audio availability). |
OrbitDeckIOS.xcodeproj in a recent Xcode (iOS 17 SDK or later).Info.plist is generated from build settings; the launch screen uses a LaunchBackground color in the asset catalog.OrbitDeckIOSTests target (Swift Testing) covers the hams.at posting client end-to-end against a local mock of the POST /api/alerts server — request building, field validation and 201/401/422 response parsing — since hams.at has no sandbox API.OrbitDeck iOS is MIT-licensed. SatelliteKit is a separate open-source project with its own license (see SatelliteKit/LICENSE in the repo). OrbitDeck is written and maintained by Paul Stoetzer, N8HM. If you find it useful, please consider supporting AMSAT, which builds and launches the satellites it tracks.