Releases: Marvbuster/sphaira-connect
Release list
Sphaira Connect 1.2.0
Sphaira Connect macOS v1.2 Build 1
Date: 2026-07-26 | Status: Done | Minimum macOS: 26.5
MARKETING_VERSION: 1.2 | CURRENT_PROJECT_VERSION: 1
Commit: 3063c66 — large remaining-time countdown, extracted view files
Summary
Adds a large, prominent remaining-time countdown while either mode is actively running — a
transfer in Sphaira Install, or an injection in RCM Payload. RCM injection did not previously
track throughput at all; it now feeds the same Core/ProgressTracker the installer uses.
RootView.swift was split further to stay under the 400-line contract limit as part of this work.
Caught in local testing before this ever shipped: the first pass placed the badge as a floating
overlay (it covered the mode switcher instead of the window growing to fit it), and animated its
raw, per-tick value directly — since a fresh sample arrives roughly every 100 ms but each animation
takes 300 ms, the badge never finished one transition before the next one interrupted it, looking
restless for the entire transfer rather than just at the start. Both are fixed below.
Features & Changes
| Type | Description |
|---|---|
| FEATURE | Large remaining-time countdown, part of the normal content layout above the mode switcher (the window grows to fit it, exactly like the queue or the progress panel) whenever a transfer or injection is actively running — .monospacedDigit() plus .contentTransition(.numericText(countsDown: true)) for the native counting-down digit animation (Features/Views/CountdownBadgeView.swift, Features/Views/RootView.swift) — see [[ui/window-and-views]] |
| FEATURE | The displayed countdown value refreshes at most once a second (RootView.refreshCountdown(with:)), decoupled from the underlying ~100 ms sample rate, so each animation has time to settle instead of being interrupted by the next one — a real "clock ticking down" instead of a scrubbing number. The raw per-tick estimate is additionally withheld (shown as an en dash) until ProgressTracker.tickCount reaches 3, so the exponential smoothing has a moment to settle before anything is displayed at all (Core/ProgressTracker.swift, Features/Views/RootView.swift) — see [[protocol/progress-tracking]] |
| FEATURE | RCM injection now computes throughput: RCMInjectorModel.inject() feeds RCMTransport's cumulative (sent, total) callback into a Core/ProgressTracker, the same tracker and smoothing the installer already used (Features/RCMInjectorModel.swift) — see [[protocol/progress-tracking]] |
| CHANGE | RootView's mode switcher (segmented Sphaira Install / RCM Payload picker with its matched-geometry highlight) extracted into ModeSwitcherView to keep RootView.swift under the 400-line limit from CONTRACT.md (Features/Views/ModeSwitcherView.swift) |
Tests
63 tests passing (was 62) — adds tickCountCountsEmissions to ProgressTrackerTests. The rest of
this feature sits in the SwiftUI/Features layer, outside the SphairaCoreTests boundary (see
[[operations/testing-strategy]]); verified via a clean Debug and Release build. Not yet
re-confirmed against real hardware.
Installation
Open the DMG and drag the app into your Applications folder
(or unpack the ZIP). On first launch, right-click → Open:
the app is ad-hoc signed and not notarised, so Gatekeeper would
otherwise refuse to start it.
| File | SHA-256 |
|---|---|
Sphaira-Connect-1.2.0-arm64.dmg |
e2d007eb309ba78c652616172c5a38bbe91ce1f2a2b602c8a6ba9438fa0ed6ff |
Sphaira-Connect-1.2.0-arm64.zip |
85927a80a206c8f23f575adbfba73f0c095e4c65bfad868a4182e0b47e3694a1 |
Sphaira Connect 1.1.0
Sphaira Connect macOS v1.1 Build 1
Date: 2026-07-26 | Status: Done | Minimum macOS: 26.5
MARKETING_VERSION: 1.1 | CURRENT_PROJECT_VERSION: 1
Commit: 370aada — RCM payload injection, official Hekate/Fusée presets
Summary
Sphaira Connect gains a second mode alongside the existing USB installer: manual RCM
payload injection for unpatched Erista/V1 Switch consoles, based on the publicly
documented Fusée Gelée protocol. The app builds the RCM image itself (header, address
table, intermezzo relocator, user payload), detects the distinct APX device
(0955:7321), transfers it over libusb and triggers the exploit with an oversized
control transfer — never automatically, only on an explicit button press. On top of
manual .bin selection, two persistent presets (Hekate, Fusée) can fetch the current
or four previous official releases directly from GitHub, verifying size and SHA-256
digest before use. The shared log line type used by both the installer and the RCM
view was factored out along the way.
Features & Changes
| Type | Description |
|---|---|
| FEATURE | RCM image builder: 4-byte header (rcmLength), 96-entry address table pointing at the intermezzo relocator, the 92-byte relocator itself, and the user payload appended and packet-aligned to 0x1000 (Core/RCMPayload.swift) |
| FEATURE | APX-mode device detection via VID 0x0955 / PID 0x7321, polled every 2 seconds independent of the installer's own Switch monitor (USB/RCMTransport.swift, Features/RCMInjectorModel.swift) |
| FEATURE | RCM transport: reads the 16-byte device ID, writes the image in 0x1000-byte chunks with parity padding for an even packet count, then triggers Fusée Gelée with an oversized control transfer (USB/RCMTransport.swift) |
| FEATURE | RCMTriggerOutcome classifies the libusb result of the trigger transfer into "payload started" (expected disconnect-style errors), "device responded" (likely patched) and "USB failure", replacing an inline check with a dedicated, independently testable type (Core/RCMTriggerOutcome.swift) |
| FEATURE | RCMInjectorModel mirrors InstallerModel's event-stream architecture for the RCM flow: preparing/injecting/succeeded/failed phases, progress and log, all routed through one AsyncStream (Features/RCMInjectorModel.swift) |
| FEATURE | Manual .bin selection via drag-and-drop or NSOpenPanel, restricted to the .bin extension and validated as a regular, readable file before use (Features/Views/RCMPayloadPickerView.swift) |
| FEATURE | Persistent Hekate and Fusée presets: the chosen file path survives app restarts via UserDefaults, is re-validated on launch, and falls back cleanly if the file has moved or been deleted (Features/RCMInjectorModel.swift) |
| FEATURE | Official-release download menus for both presets: the current and four previous stable releases are listed from the GitHub API (CTCaer/hekate, Atmosphere-NX/Atmosphere), the correct asset is picked by name (rejecting the 8 GB Hekate variant and non-fusee.bin Atmosphère assets), and any path-traversing asset name is rejected outright (Core/HekateRelease.swift, Core/FuseeRelease.swift, Features/HekateDownloader.swift, Features/FuseeDownloader.swift) |
| FEATURE | Downloaded payloads are size-capped at 2 MB and checked against the SHA-256 digest GitHub publishes for the asset, when present, before being written into Application Support (Features/HekateDownloader.swift, Features/FuseeDownloader.swift) |
| FEATURE | Mode switch in the main window (Sphaira installation ⇄ RCM payload), each with its own device monitor, status pill and log, switching monitors cleanly on change (Features/Views/RootView.swift) |
| CHANGE | InstallerModel.LogLine extracted into a shared AppLogLine, now used by both InstallerModel and RCMInjectorModel so the log console view has a single type to render (Shared/AppLogLine.swift, Features/InstallerModel.swift, Features/Views/LogConsoleView.swift) |
| CHANGE | About panel and README/NOTICE credit the Fusée Gelée protocol and the ironfoil reference implementation; README documents the new RCM flow, the preset downloads and the project's licensing stance in a new "Open source, no agenda" section (Sphaira_ConnectApp.swift, README.md, README.de.md, NOTICE.md) |
Tests
62 tests passing (up from 52), adding RCMPayloadTests, RCMTriggerOutcomeTests,
HekateReleaseTests and FuseeReleaseTests — including explicit coverage for
rejecting the 8 GB Hekate variant, rejecting non-fusee.bin assets, and rejecting
path-traversing asset names (../hekate_ctcaer_v1.bin).
Installation
Open the DMG and drag the app into your Applications folder
(or unpack the ZIP). On first launch, right-click → Open:
the app is ad-hoc signed and not notarised, so Gatekeeper would
otherwise refuse to start it.
| File | SHA-256 |
|---|---|
Sphaira-Connect-1.1.0-arm64.dmg |
f2df971e29b64b9e170f61545829a7044623aea56a68973d60d326431755fd49 |
Sphaira-Connect-1.1.0-arm64.zip |
33dc933f2a19497264c8c475a9f3b7bbf517c6d2d9ce390f1bd7a12b1032dcb4 |
Sphaira Connect 1.0.0
Sphaira Connect macOS v1.0 Build 1
Datum: 26.07.2026 | Status: Fertig | Minimales macOS: 26.5
MARKETING_VERSION: 1.0 | CURRENT_PROJECT_VERSION: 1
Commit: 77527a1 — feat: Sphaira Connect — native macOS-App für USB-Installation
Zusammenfassung
Erstes Release. Sphaira Connect ersetzt das Python-Skript usb_install.py durch eine
native SwiftUI-App: Sie spricht das SPH0-Protokoll direkt gegen statisch gelinktes
libusb, nimmt NSP/XCI/NSZ/XCZ per Drag-and-drop entgegen und überträgt sie an die
Homebrew-App Sphaira auf der Nintendo Switch. Die Kernlogik liegt SwiftUI- und
USB-frei unter Sphaira Connect/Core/ und wird von einem eigenständigen
SwiftPM-Testpaket mit 52 Tests geprüft, das die Quellen per Symlink einbindet —
ohne zweite Kopie der Dateien.
Der zweite Teil des Releases sind die Korrekturen aus den ersten Praxisläufen: der
Fortschrittsbalken stand bei XCI sofort auf 100 %, die Abschlussmeldung blieb nach
Sitzungsende aus, und ein hängender Schreibvorgang machte „Abbrechen" wirkungslos.
Features & Änderungen
| Typ | Beschreibung |
|---|---|
| FEATURE | SPH0-Paketformat nativ implementiert: 24 Bytes, sechs UInt32 little-endian, Magic 0x53504830, CRC32C über die ersten 20 Bytes (Core/SphairaProtocol.swift) |
| FEATURE | CRC-32C (Castagnoli, gespiegeltes Polynom 0x82F63B78) als tabellenbasierte, nonisolated Implementierung ohne Fremdbibliothek (Core/CRC32C.swift) |
| FEATURE | Sitzungsablauf als Zustandsautomat: Namensliste senden, danach Kommandoschleife über open und quit, Dateiblöcke auf Anfrage der Switch (USB/InstallSession.swift) — siehe Wiki protocol/install-session-flow |
| FEATURE | libusb-Anbindung mit Bulk-Transfers, Endpunkte aus der aktiven Konfiguration ermittelt, Rückfall auf 0x81/0x01 (USB/USBTransport.swift) |
| FEATURE | libusb statisch gelinkt (/opt/homebrew/lib/libusb-1.0.a) über Bridging-Header; App-Sandbox aus, Hardened Runtime an (Sphaira Connect.xcodeproj/project.pbxproj, USB/SphairaConnect-Bridging-Header.h) |
| FEATURE | Geräteerkennung über VID 0x057E / PID 0x3000, Statusanzeige pollt alle 2 Sekunden abseits des MainActors (USB/LibUSBProbe.swift, Features/InstallerModel.swift) |
| FEATURE | Warteschlange per Drag-and-drop oder Auswahldialog; Ordner werden rekursiv aufgelöst, Duplikate über den standardisierten Pfad verworfen, nur nsp/xci/nsz/xcz übernommen, Reihenfolge stabil alphabetisch (Core/InstallItem.swift, Features/Views/DropZoneView.swift) |
| FEATURE | Warteschlangenliste mit Zustand je Titel (wartet / überträgt / fertig / fehlgeschlagen), Einzelentfernung und „alle entfernen" — im laufenden Betrieb gesperrt (Features/Views/QueueListView.swift) |
| FEATURE | Fortschrittsanzeige mit Datei- und Gesamtanteil, geglättetem Durchsatz und geschätzter Restzeit; Aktualisierung höchstens alle 100 ms (Core/ProgressTracker.swift, Features/Views/ProgressPanelView.swift) |
| FEATURE | Deutsche Größen-, Durchsatz- und Zeitformatierung mit Dezimalkomma, ohne NumberFormatter (Core/ByteFormat.swift) |
| FEATURE | Aufklappbares Protokoll mit Zeitstempeln, begrenzt auf die letzten 500 Zeilen (Features/Views/LogConsoleView.swift, Features/InstallerModel.swift) |
| FEATURE | Abbruch jederzeit möglich: Das Abbruch-Flag ist über ein NSLock atomar und wird im USB-Thread zwischen den Transfers geprüft, ohne auf MainActor-Zustand zu warten (Features/InstallerModel.swift, USB/USBTransport.swift) |
| FEATURE | Deutschsprachige Fehlertexte für alle Fehlerfälle — Switch nicht gefunden, Zugriff verweigert, USB-Fehler mit Code, kurzes Paket, falsche Magic, Prüfsummenfehler, unerwarteter Befehl, unlesbare Datei, Abbruch (Core/InstallError.swift) |
| FEATURE | Liquid-Glass-Oberfläche mit eigenem Designsystem: Farb- und Abstands-Token, Glas-Buttons, Fenster folgt dem Inhalt animiert (Shared/DesignTokens.swift, Shared/GlassButton.swift, Shared/WindowStyleConfigurator.swift, Features/Views/RootView.swift) |
| FEATURE | Eigenständiges SwiftPM-Testpaket mit 52 Tests über Paketformat, CRC32C, Ordnerscan, Fortschritt, Formatierung und Fehlertexte; bindet Sphaira Connect/Core/ per Symlink ein (SphairaCoreTests/Package.swift) |
| FIX | Fortschritt zählte die höchste von der Switch angefragte Position statt der real gesendeten Bytes — bei XCI stand der Balken dadurch sofort auf 100 %. ProgressTracker.record summiert jetzt nur übertragene Blöcke und klemmt auf die Dateigröße, da die Switch Bereiche mehrfach anfordern darf (Core/ProgressTracker.swift) |
| FIX | close() blockierte nach Sitzungsende und hielt die Abschlussmeldung auf, wodurch „Abbrechen" aktiv blieb. Der libusb-Abbau läuft jetzt asynchron auf einer Hintergrund-Queue, die Referenzen werden vorher gelöst (USB/USBTransport.swift) |
| FIX | Schreibvorgänge hatten kein Zeitlimit, ein Abbruch griff mitten im Block nicht. Bulk-Writes laufen jetzt mit 5000 ms Zeitlimit, Timeouts gelten als Gelegenheit zur Abbruchprüfung, bereits übertragene Teilbytes werden mitgezählt (USB/USBTransport.swift) |
| FIX | Rückmeldungen der Sitzung liefen über einzelne unstrukturierte Tasks und kamen dadurch ungeordnet an. Alle Ereignisse laufen jetzt durch genau einen AsyncStream, die Zahl fertiger Dateien zählt ein gesperrter Zähler auf dem Sitzungs-Thread (Features/InstallerModel.swift) |
| FIX | Endet der Ereignisstrom ohne Abschlussmeldung, blieb die Oberfläche auf „läuft" stehen; ein Rückfall setzt die Phase jetzt auf „bereit" und schreibt eine Protokollzeile (Features/InstallerModel.swift) |
| FIX | Der Gesamtfortschritt sprang über eine gescheiterte Datei hinweg — gezählt werden jetzt nur vollständig übertragene Dateien (USB/InstallSession.swift) |
| FIX | Bei einem Dateifehler geht ein Fehlerpaket an die Switch, die Kommandoschleife beantwortet danach den Quit-Befehl regulär; die Sitzung endet geordnet statt mit abruptem USB-Abriss (USB/InstallSession.swift) |
| CHANGE | Die SwiftData-Vorlage des Xcode-Templates entfällt: Item.swift gelöscht, ContentView.swift und Sphaira_ConnectApp.swift auf RootView umgestellt |
Installation
DMG öffnen und die App in den Programme-Ordner ziehen
(oder das ZIP entpacken). Beim ersten Start Rechtsklick →
Öffnen: Die App ist ad-hoc signiert und nicht notarisiert,
sonst verweigert Gatekeeper den Start.
| Datei | SHA-256 |
|---|---|
Sphaira-Connect-1.0.0-arm64.dmg |
f251e116a0d13f04c2b61e0f6f11bef57057883c9e6e419bbb1742e0daa68719 |
Sphaira-Connect-1.0.0-arm64.zip |
63077e570550d9268800cc847136f298886f9b4cefc14d1735257712519ace28 |