A macOS app that shows AirPods head/neck posture in the menu bar. The real project set up after the Phase 0a GO.
./build.sh # SwiftPM build + .app bundle + ad-hoc sign
./uppod.app/Contents/MacOS/uppod # with REAL AirPodsOn first run it asks for "Motion and Fitness" permission → Allow. A head-profile icon appears in the menu bar; click it → popover.
| Flag | Effect |
|---|---|
UPPOD_MOCK=1 |
fake ±34° pitch oscillation instead of AirPods |
UPPOD_DEBUG=1 |
state/score/heartbeat log to stdout |
UPPOD_AUTOCAL=1 |
auto-calibrate ~1s after the first sample (headless test) |
UPPOD_FLIP_SIGN=1 |
flip the forward-flexion sign (will be locked in permanently in Phase 0b) |
Motion.swift MotionSample (gravity included), MotionProviding, HeadphoneMotionService, MockMotionService
Pipeline.swift FlexionEstimator (gravity fusion, drift-free absolute angle) · ScalarEMA · MotionGate ·
PostureClassifier (strain bands, hysteresis) · PostureStateMachine (debounce)
Strain.swift CervicalLoad (Hansraj load curve: angle → kg → strain 0..1)
Scoring.swift TimeInStateStore · DoseAccumulator (angle×time score) · CalibrationManager
Persistence.swift SessionStore protocol · JSONFileStore · MemoryStore (calibration+settings+daily summary)
PostureEngine orchestrator (ObservableObject) — sample → pipeline → publish + persistence
StatusBar.swift StatusGlyph (rotating head-profile icon) · StatusBarController (NSStatusItem + popover)
PopoverContentView SwiftUI: state, neck load, score ring, time-in-state, sensitivity, calibration
Gravity-fused drift-free absolute angle · biomechanical continuous strain (Hansraj) + personal sensitivity · dose-based (angle×time) daily score · live 3+1 state · hysteresis+debounce · motion gate · lying-down detection · time-in-state · persistence (calibration + settings + daily history, restored on launch) · menu bar icon + popover.
Notifications · onboarding flow · history/trend charts (data is already stored day-by-day) · HID-idle detection · two-rate pipeline optimization · App Sandbox (MAS) · launch-at-login · migration to GRDB (if volume grows).
- ✅ End-to-end pipeline with mock (gravity fusion → load → strain → state → dose)
- ✅ Persistence round-trip (calibration + daily data preserved across restart)
- ✅ Live with real AirPods: sign/load/state correct, "10°" counts
- ⏳ Threshold bands + sensitivity default fine-tuned in the field (Phase 0b)