A native SwiftUI + CoreBluetooth controller for the OSSTempo / PuttTempo ESP32 LED
tempo trainer (firmware + hardware spec live in the separate osstempo repo). Connect over
Bluetooth, pick a mode, trigger strokes, and tune the swing with a live on-strip reach preview.
Dev-only — no App Store.
Part of OSSGolf, open source golf projects.
Four layers, one-way dependencies, Swift 6 strict concurrency:
UI (SwiftUI, @Observable bindings)
→ App (pure AppReducer FSM + thin @MainActor TempoController shell)
→ BLE (TempoTransport seam in Sendable domain values; CoreBluetoothTransport / FakeTransport)
→ Domain (Param registry, GATTCodec, StrokeKinematics, VisualizerModel — pure, Foundation-only)
The connection FSM and write reconciliation live in a pure reducer (reduce(state, event) → (state, [effect])), so almost everything is tested in-process via FakeTransport + an injected
Scheduler, with no clocks or sleeps. See DESIGN.md for the full low-level design.
Open OSSTempo.xcodeproj in Xcode 26. To run on a real iPhone: select it as the destination, set
your Team under Signing & Capabilities (bundle id com.ossgolf.OSSTempo), then build & run.
# Build
xcodebuild build -scheme OSSTempo -destination 'platform=iOS Simulator,name=iPhone 17'
# Test (use iPhone 17 — iPhone 16 isn't installed for iOS 26.5; parallel off avoids clone races)
xcodebuild test -scheme OSSTempo -destination 'platform=iOS Simulator,name=iPhone 17' \
-parallel-testing-enabled NO
# Regenerate the app icon (headless Core Graphics)
swift Tools/generate_icon.swiftUI tests run against an in-memory mock via the -uitestMockBLE launch argument (scenarios:
writeFail | dropConnect | btOff | missingChar | bootFull).
- Connect → Find my trainer → grant Bluetooth → it scans by service UUID for
PuttTempo. - If you just re-flashed the firmware with a changed characteristic set, toggle the iPhone's Bluetooth off/on first — iOS caches the GATT table and will otherwise serve a stale one.
- The Diagnostics sheet (overflow menu) shows every characteristic's decoded value + raw LE bytes — the fastest way to spot a contract mismatch on real hardware.
13 characteristics, tails 0002–000e, service 6e400001-b5a3-f393-e0a9-e50e24dcca9e, all
little-endian. The registry is mirrored in OSSTempo/Domain/Param.swift and verified against the
firmware. See the GATT table in the osstempo repo's README.md.