diff --git a/.almanac/pages/machine-diagnostics.md b/.almanac/pages/machine-diagnostics.md index b47cbdda6..31d8df6f3 100644 --- a/.almanac/pages/machine-diagnostics.md +++ b/.almanac/pages/machine-diagnostics.md @@ -56,7 +56,7 @@ The screen is driven by live BLE state. `BleRepository` exposes `diagnostics` as The binary contract is broader than just four fault words. `parseDiagnosticPacket()` accepts an empty payload as a valid zero snapshot, rejects non-empty payloads shorter than `18` bytes, then decodes uptime seconds, four unsigned `16-bit` fault words, six required temperatures, two optional extra temperatures, an optional `52`-byte crash block, and an optional `32-bit` warnings field [@protocol-parser] [@protocol-models]. [[vitruvian-ble-protocol]] is the neighboring page for the broader scan, rep, monitor, and command surface around this diagnostic characteristic. -Fault decoding is intentionally category-specific. `DiagnosticFaultDecoder` always projects the packet into four display slots named `Vee`, `Other`, `Motor A`, and `Motor B`, then maps bitmasks to labels such as `TI restarted`, `Over voltage`, `Encoder`, or `Motor overtemp` instead of exposing only raw integers [@fault-decoder]. The view-model tests pin that labeling contract and verify that the export text includes both the human label and the raw hex code [@diagnostics-tests]. +Fault decoding is intentionally category-specific. `DiagnosticFaultDecoder` always projects the packet into four display slots named `Controller`, `Other`, `Motor A`, and `Motor B`, then maps bitmasks to labels such as `Controller restarted`, `Overvoltage`, `Encoder fault`, or `Motor over-temperature` instead of exposing only raw integers [@fault-decoder]. The view-model tests pin that labeling contract and verify that the export text includes both the human label and the raw hex code [@diagnostics-tests]. The export contract is deliberately narrow. `buildDiagnosticsExportText()` prepends app version, export timestamp, `REDACTED_DIAGNOSTICS` classification, and a privacy line that excludes workout history, profiles, auth or session tokens, Supabase config, and keystore data before listing the current packet contents [@diagnostics-vm]. The copy button stays disabled until a packet exists, so the feature exports the latest machine snapshot rather than an empty template [@diagnostics-screen]. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e5ca2d3a6..73c9a1b41 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -28,7 +28,7 @@ body: id: app-version attributes: label: App Version - description: What version of Vitruvian Phoenix are you using? + description: What version of Project Phoenix are you using? placeholder: "e.g., 1.0.0" validations: required: true diff --git a/.phoenix-review/BUG-FIX-PLAN.md b/.phoenix-review/BUG-FIX-PLAN.md index 9ba4f91f3..5720bf042 100644 --- a/.phoenix-review/BUG-FIX-PLAN.md +++ b/.phoenix-review/BUG-FIX-PLAN.md @@ -142,7 +142,7 @@ - F173 | impact=250 | Domain | medium / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/voice/SafeWordListener.kt | lines=26, 38-44 | The common listener contract exposes `startListening()`, `isListening`, and `detectedWord`, but no error/status channel for unavailable recognition, denied microphone/speech permission, unsupported on-device recognition, or start failures. The platform implementations can fail by logging and leaving `isListening` false, which leaves callers unable to distinguish “not yet started”, “already stopped”, and “emergency voice stop is unavailable”. For a safety-oriented emergency stop path, silent non-operation can make the workout UI appear configured while no voice stop will ever fire. | fix: Add a shared status/error flow or sealed state such as `Idle`, `Listening`, `PermissionDenied`, `RecognitionUnavailable`, and `StartFailed`, and have calibration/workout UI surface failures instead of relying only on logs and `isListening`. - F174 | impact=250 | Domain | medium / failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/voice/SafeWordListener.kt | lines=41-44 | The API documents that emitted detections occur when results “match the safe word”, but it does not define or enforce what a valid safe word is. The current platform implementations perform whole-token matching after splitting transcripts on whitespace, while settings/calibration paths only require a non-blank string. A user can configure a multi-word phrase or punctuation-bearing value that appears valid in common code but can never match the actual token-based listener. | fix: Define the safe-word normalization/validation contract in common code. Either restrict configured safe words to one normalized token and reject/trim punctuation at save/calibration time, or update platform matching to normalize transcripts and support the same phrase rules accepted by the UI. - F304 | impact=250 | Utilities | medium / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt | lines=1355-1380 | The streaming personal-record importer drops `PersonalRecordBackup.cableCount` by passing `cable_count = null` to `queries.upsertPR`. The non-streaming importer preserves this field at lines 631-643, so large backups imported through the streaming path lose cable-count metadata while small backups do not. | fix: Pass `pr.cableCount?.toLong()` in the streaming path to match `importFromJson()`. -- F306 | impact=250 | Utilities | medium / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt | lines=75-86 | `createStopCommand()` is documented as the primary STOP command but emits legacy opcode `0x05`, while `BleConstants.Commands.STOP_COMMAND` and `createOfficialStopPacket()` use official stop opcode `0x50`. This creates an attractive but unsafe API footgun: callers choosing `createStopCommand()` by name/comment may send a different stop semantics than the official stop/clear-fault packet. | fix: Rename/deprecate the legacy helper (for example `createLegacyStopCommand()`), make the primary helper delegate to `createOfficialStopPacket()`, and keep tests explicit about the legacy opcode only where it is intentionally required. +- F306 | impact=250 | Utilities | medium / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt | lines=75-86 | `createStopCommand()` is documented as the primary STOP command but emits legacy opcode `0x05`, while `BleConstants.Commands.STOP_COMMAND` and `createSoftStopPacket()` use 0x50 soft-stop opcode `0x50`. This creates an attractive but unsafe API footgun: callers choosing `createStopCommand()` by name/comment may send a different stop semantics than the 0x50 soft-stop/clear-fault packet. | fix: Rename/deprecate the legacy helper (for example `createLegacyStopCommand()`), make the primary helper delegate to `createSoftStopPacket()`, and keep tests explicit about the legacy opcode only where it is intentionally required. - F311 | impact=250 | Utilities | medium / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/HardwareDetection.kt | lines=8-13, 20-29 | The file-level documentation says name-prefix hardware detection was flawed and should be avoided, but `detectModel()` still infers `VFormTrainer`/`TrainerPlus` from `Vee_` and `VIT` prefixes. That stale detection result is used when reporting `ConnectionState.Connected`, so UI or downstream logic can still receive a model classification the comments say is unreliable. | fix: Either remove model inference and return `Unknown` until firmware-backed detection exists, or update the documentation and tests to explain why these prefixes are now reliable. Prefer firmware/version-characteristic detection before exposing a concrete hardware model. - F313 | impact=250 | Utilities | medium / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/KmpUtils.kt | lines=232-236 | `formatFloat()` builds the decimal part from `rounded - intPart`, but `toLong()` truncates negative values toward zero. Negative values with decimal places therefore format with a negative fractional component, e.g. the current algorithm formats `-1.23` with two decimals as `-1.-23` and `-0.25` as `0.-25`. Any UI using this helper for signed deltas, percentages, velocities, or corrections can display malformed numbers. | fix: Format the absolute fractional component separately from the sign, or use a locale-stable multiplatform formatter that rounds the whole scaled integer first and then reconstructs sign, integer, and padded fractional digits from absolute values. Add tests for negative values such as `-1.23`, `-5.5`, and `-0.25`. - F314 | impact=250 | Utilities | medium / bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/KmpUtils.kt | lines=245, 283 | `formatDouble()` downcasts every `Double` to `Float` before formatting. This loses precision for normal `Double` values and turns large finite doubles outside the `Float` range into infinities, which then hit the non-finite guard and display as zero. The `Double.format()` extension exposes this behavior broadly, so any future Double-valued analytics/statistics display can silently show rounded or completely wrong values. | fix: Implement `formatDouble()` using Double arithmetic end-to-end and a Double finite check rather than delegating through Float. Add regression tests with a high-precision value and a finite value greater than `Float.MAX_VALUE` to prevent silent zeroing. diff --git a/.phoenix-review/CONSOLIDATED-REPORT.md b/.phoenix-review/CONSOLIDATED-REPORT.md index c1f10bebd..ef94a7a0c 100644 --- a/.phoenix-review/CONSOLIDATED-REPORT.md +++ b/.phoenix-review/CONSOLIDATED-REPORT.md @@ -323,7 +323,7 @@ - F303 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt | lines=1138-1215 | description: Metric batching counts only metrics that are actually inserted (`batchCount++` is inside `metric.sessionId in importedSessionIds`). If a large backup contains mostly metrics for skipped/pre-existing sessions, the second batching loop can consume the entire remaining `metricSamples` array in one transaction because `batchCount` never reaches `IMPORT_BATCH_SIZE`. This defeats the OOM/lock-avoidance goal of streaming import for exactly the large-file path. | fix: Limit each transaction by rows processed/seen, not rows inserted, or maintain separate `rowsInBatch` and `insertedInBatch` counters. - F304 | module=Utilities | category=bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt | lines=1355-1380 | description: The streaming personal-record importer drops `PersonalRecordBackup.cableCount` by passing `cable_count = null` to `queries.upsertPR`. The non-streaming importer preserves this field at lines 631-643, so large backups imported through the streaming path lose cable-count metadata while small backups do not. | fix: Pass `pr.cableCount?.toLong()` in the streaming path to match `importFromJson()`. - F305 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt | lines=976-980, 1025-1737 | description: The streaming importer is order-dependent even though JSON object member order is not semantically significant. Child arrays such as `metricSamples`, `routineExercises`, `cycleDays`, `plannedSets`, and `completedSets` are imported only if their parent IDs have already been seen in earlier fields. A valid backup with fields reordered can silently skip children or log warnings while returning success. | fix: Make streaming import robust to object order by doing multiple passes over a seekable source, buffering small parent/child ID sets and deferred child rows, or enforcing/exporting a documented field order and rejecting out-of-order backups instead of silently skipping data. -- F306 | module=Utilities | category=bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt | lines=75-86 | description: `createStopCommand()` is documented as the primary STOP command but emits legacy opcode `0x05`, while `BleConstants.Commands.STOP_COMMAND` and `createOfficialStopPacket()` use official stop opcode `0x50`. This creates an attractive but unsafe API footgun: callers choosing `createStopCommand()` by name/comment may send a different stop semantics than the official stop/clear-fault packet. | fix: Rename/deprecate the legacy helper (for example `createLegacyStopCommand()`), make the primary helper delegate to `createOfficialStopPacket()`, and keep tests explicit about the legacy opcode only where it is intentionally required. +- F306 | module=Utilities | category=bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt | lines=75-86 | description: `createStopCommand()` is documented as the primary STOP command but emits legacy opcode `0x05`, while `BleConstants.Commands.STOP_COMMAND` and `createSoftStopPacket()` use 0x50 soft-stop opcode `0x50`. This creates an attractive but unsafe API footgun: callers choosing `createStopCommand()` by name/comment may send a different stop semantics than the 0x50 soft-stop/clear-fault packet. | fix: Rename/deprecate the legacy helper (for example `createLegacyStopCommand()`), make the primary helper delegate to `createSoftStopPacket()`, and keep tests explicit about the legacy opcode only where it is intentionally required. - F307 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/ConnectivityChecker.kt | lines=7-12 | description: The common contract only exposes a synchronous `isOnline()` method and has no lifecycle/dispose hook or observable connectivity-change signal. Platform implementations that allocate long-lived resources or background monitors cannot be cancelled through the common API, and common sync code cannot subscribe to a reconnect event after setting a waiting-for-connectivity state. | fix: Add lifecycle and state-change semantics to the expect contract, such as `close()`/`dispose()` plus a `StateFlow` or callback registration, and make DI scopes own the checker lifecycle explicitly. - F308 | module=Utilities | category=bug | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/Constants.kt | lines=139-142 | description: `ProtocolConstants` declares packet sizes that contradict the actual packet builders and tests. `ECHO_PACKET_SIZE` is `29`, but `BlePacketFactory.createEchoControl()` builds a 32-byte frame and tests assert 32. `ACTIVATION_PACKET_SIZE` is `97`, but `BleConstants.ActivationPacket.SIZE`, `BlePacketFactory.createProgramParams()`, model documentation, and tests all use 96 bytes. Any code using these constants for allocation, validation, or MTU checks would reject valid packets or allocate the wrong size. | fix: Define packet sizes from a single source of truth (`BleConstants.ActivationPacket.SIZE` and the factory Echo size), update Echo to 32 and activation to 96, and add tests covering `ProtocolConstants` parity with factory output. - F309 | module=Utilities | category=failure-point | file=shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.kt | lines=71-73 | description: The common API promises device info as a JSON `String`, but the contract does not require escaping or a structured serializer-backed result. Platform values such as model names, device names, build strings, or bundle values can contain quotes, backslashes, or control characters; manual string-building actual implementations can then return invalid JSON or malformed metadata. | fix: Replace `toJson(): String` with a serializable data model or require all actual implementations to use `kotlinx.serialization`/a shared escaping helper. Add tests with quotes, backslashes, and newlines in representative fields. diff --git a/.phoenix-review/utilities-part-2.md b/.phoenix-review/utilities-part-2.md index 6bba2cfa9..6d4196654 100644 --- a/.phoenix-review/utilities-part-2.md +++ b/.phoenix-review/utilities-part-2.md @@ -35,8 +35,8 @@ Findings: 11 total - Category: bug - Severity: medium - Line numbers: 75-86 -- Description: `createStopCommand()` is documented as the primary STOP command but emits legacy opcode `0x05`, while `BleConstants.Commands.STOP_COMMAND` and `createOfficialStopPacket()` use official stop opcode `0x50`. This creates an attractive but unsafe API footgun: callers choosing `createStopCommand()` by name/comment may send a different stop semantics than the official stop/clear-fault packet. -- Suggested fix direction: Rename/deprecate the legacy helper (for example `createLegacyStopCommand()`), make the primary helper delegate to `createOfficialStopPacket()`, and keep tests explicit about the legacy opcode only where it is intentionally required. +- Description: `createStopCommand()` is documented as the primary STOP command but emits legacy opcode `0x05`, while `BleConstants.Commands.STOP_COMMAND` and `createSoftStopPacket()` use 0x50 soft-stop opcode `0x50`. This creates an attractive but unsafe API footgun: callers choosing `createStopCommand()` by name/comment may send a different stop semantics than the 0x50 soft-stop/clear-fault packet. +- Suggested fix direction: Rename/deprecate the legacy helper (for example `createLegacyStopCommand()`), make the primary helper delegate to `createSoftStopPacket()`, and keep tests explicit about the legacy opcode only where it is intentionally required. #### Finding 3 - Category: bug diff --git a/ANDROID_INSTALL.md b/ANDROID_INSTALL.md index 886a640ef..6a93197d3 100644 --- a/ANDROID_INSTALL.md +++ b/ANDROID_INSTALL.md @@ -1,6 +1,6 @@ # Android Installation Guide -This guide explains how to install Vitruvian Phoenix on your Android device. +This guide explains how to install Project Phoenix on your Android device. ## Prerequisites @@ -47,7 +47,7 @@ Android requires permission to install apps from outside the Play Store. 1. Open the downloaded APK file 2. Tap **Install** 3. Wait for installation to complete -4. Tap **Open** or find **Vitruvian Phoenix** in your app drawer +4. Tap **Open** or find **Project Phoenix** in your app drawer --- @@ -64,7 +64,7 @@ When you first launch the app, you'll be asked to grant permissions: - This is an Android requirement - the app does not track your location - Tap **Allow** when prompted -**Note:** If you deny permissions, the app cannot connect to your trainer. You can always grant permissions later in Settings > Apps > Vitruvian Phoenix > Permissions. +**Note:** If you deny permissions, the app cannot connect to your trainer. You can always grant permissions later in Settings > Apps > Project Phoenix > Permissions. --- @@ -95,12 +95,12 @@ When a new version is released: ### App Crashes on Launch - Make sure your device is running Android 8.0 or later -- Try clearing app data: Settings > Apps > Vitruvian Phoenix > Storage > Clear data +- Try clearing app data: Settings > Apps > Project Phoenix > Storage > Clear data - Report the issue on GitHub with your device model and Android version ### Bluetooth Permission Denied -1. Go to **Settings > Apps > Vitruvian Phoenix > Permissions** +1. Go to **Settings > Apps > Project Phoenix > Permissions** 2. Enable **Nearby devices** (Android 12+) or **Location** (Android 11 and below) 3. Restart the app diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 5b9d3d893..c9bd6641d 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -321,7 +321,7 @@ android { targetSdk = 37 // Fail fast if CI injects an invalid version code instead of silently shipping a default. versionCode = injectedVersionCode ?: 5 - versionName = "0.9.6" + versionName = "1.0.0" // Supabase config injected from local.properties buildConfigField("String", "SUPABASE_URL", "\"$supabaseUrl\"") diff --git a/androidApp/src/main/res/xml/backup_rules.xml b/androidApp/src/main/res/xml/backup_rules.xml index b327f184d..0871fb1f6 100644 --- a/androidApp/src/main/res/xml/backup_rules.xml +++ b/androidApp/src/main/res/xml/backup_rules.xml @@ -1,6 +1,6 @@ - + diff --git a/androidApp/src/main/res/xml/data_extraction_rules.xml b/androidApp/src/main/res/xml/data_extraction_rules.xml index e75113bf4..4e95d1f7a 100644 --- a/androidApp/src/main/res/xml/data_extraction_rules.xml +++ b/androidApp/src/main/res/xml/data_extraction_rules.xml @@ -1,6 +1,6 @@ - + diff --git a/docs/index.html b/docs/index.html index b6c199de5..4fd3fa9a3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,14 +4,14 @@ Project Phoenix — Full Bluetooth Training Control - + - + @@ -499,7 +499,7 @@

Full Bluetooth control.
Built for serious training.
-
572
Exercises, with video
+
870+
Exercises, with demo images
7
Ways to train
2
Machines supported
5
Languages
@@ -565,7 +565,7 @@

Real-time intelligence

Build & program

-

A 572-exercise library and a routine builder powerful enough for real periodized programming.

+

A library of 870+ openly licensed exercises and a routine builder powerful enough for real periodized programming.

  • Supersets with drag-and-drop & per-set rest
  • % of PR scaling & AMRAP sets
  • @@ -598,7 +598,7 @@

    Your data, your way

    The "Safe Word" stop

    -

    A safety feature the original app never had: just say the word and the machine lets go.

    +

    A voice-activated safety feature: just say the word and the machine lets go.

    • Voice-activated emergency stop
    • Quick 3-rep calibration to your voice
    • diff --git a/docs/superpowers/specs/2026-06-16-official-weight-handling-design.md b/docs/superpowers/specs/2026-06-16-per-cable-weight-handling-design.md similarity index 93% rename from docs/superpowers/specs/2026-06-16-official-weight-handling-design.md rename to docs/superpowers/specs/2026-06-16-per-cable-weight-handling-design.md index 61680a2fa..c5ccd6301 100644 --- a/docs/superpowers/specs/2026-06-16-official-weight-handling-design.md +++ b/docs/superpowers/specs/2026-06-16-per-cable-weight-handling-design.md @@ -1,12 +1,12 @@ -# Official Weight Handling Alignment Design +# Per-Cable Weight Handling Design ## Goal -Align Phoenix weight handling with the official Vitruvian app: the selected, stored, commanded, and primary displayed load is a per-cable value. Two-cable totals may be shown only as clearly labeled supplemental context and must not feed back into saved weights, personal records, recommendations, BLE commands, sync payloads, or routine configuration. +Make Phoenix weight handling consistent with the machine contract: the selected, stored, commanded, and primary displayed load is a per-cable value. Two-cable totals may be shown only as clearly labeled supplemental context and must not feed back into saved weights, personal records, recommendations, BLE commands, sync payloads, or routine configuration. ## Current Evidence -Hardware captures show a single scalar force value in kilograms flowing through set storage and BLE command encoding. Cable count is exercise metadata and does not branch load math. The only doubling found is a display-only caption: "Total weight for 2 cables". +Hardware captures show a single scalar force value in kilograms flowing through set storage and BLE command encoding. Cable count is exercise metadata and does not branch load math. No doubling occurs at the protocol level; any two-cable total is display-only. Phoenix already mostly matches this at the machine contract: @@ -123,7 +123,7 @@ Recommended verification commands: ## Acceptance Criteria -- Main user-visible load values match the official app's per-cable convention. +- Main user-visible load values are per-cable, matching the value sent to the device. - The app can still show an explicitly labeled two-cable total helper where useful. - Stored/session/PR/recommendation/sync/backup contracts remain per-cable. - BLE packet tests still prove machine commands are per-cable. diff --git a/final-visuals/android/01-master-off.png b/final-visuals/android/01-master-off.png deleted file mode 100644 index f7bee37ce..000000000 Binary files a/final-visuals/android/01-master-off.png and /dev/null differ diff --git a/final-visuals/android/02-master-on.png b/final-visuals/android/02-master-on.png deleted file mode 100644 index 9168d3b84..000000000 Binary files a/final-visuals/android/02-master-on.png and /dev/null differ diff --git a/final-visuals/android/03-master-vulgar-on.png b/final-visuals/android/03-master-vulgar-on.png deleted file mode 100644 index d1bf0c44b..000000000 Binary files a/final-visuals/android/03-master-vulgar-on.png and /dev/null differ diff --git a/final-visuals/android/04-modal.png b/final-visuals/android/04-modal.png deleted file mode 100644 index 5451724ce..000000000 Binary files a/final-visuals/android/04-modal.png and /dev/null differ diff --git a/final-visuals/android/05-dominatrix-unlocked.png b/final-visuals/android/05-dominatrix-unlocked.png deleted file mode 100644 index f48d1d85f..000000000 Binary files a/final-visuals/android/05-dominatrix-unlocked.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/01-home-dark.png b/final-visuals/android/ux-phase1/01-home-dark.png deleted file mode 100644 index 8213d76e6..000000000 Binary files a/final-visuals/android/ux-phase1/01-home-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/01-home-light.png b/final-visuals/android/ux-phase1/01-home-light.png deleted file mode 100644 index 80abc48f7..000000000 Binary files a/final-visuals/android/ux-phase1/01-home-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/02-just-lift-dark.png b/final-visuals/android/ux-phase1/02-just-lift-dark.png deleted file mode 100644 index 640343212..000000000 Binary files a/final-visuals/android/ux-phase1/02-just-lift-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/02-just-lift-light.png b/final-visuals/android/ux-phase1/02-just-lift-light.png deleted file mode 100644 index 702a57b31..000000000 Binary files a/final-visuals/android/ux-phase1/02-just-lift-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/03-exercise-picker-dark.png b/final-visuals/android/ux-phase1/03-exercise-picker-dark.png deleted file mode 100644 index f39e6b379..000000000 Binary files a/final-visuals/android/ux-phase1/03-exercise-picker-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/03-exercise-picker-light.png b/final-visuals/android/ux-phase1/03-exercise-picker-light.png deleted file mode 100644 index 963ac5ab7..000000000 Binary files a/final-visuals/android/ux-phase1/03-exercise-picker-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/04-routines-dark.png b/final-visuals/android/ux-phase1/04-routines-dark.png deleted file mode 100644 index b102ac68a..000000000 Binary files a/final-visuals/android/ux-phase1/04-routines-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/04-routines-light.png b/final-visuals/android/ux-phase1/04-routines-light.png deleted file mode 100644 index 64fa62a51..000000000 Binary files a/final-visuals/android/ux-phase1/04-routines-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/05-history-dark.png b/final-visuals/android/ux-phase1/05-history-dark.png deleted file mode 100644 index 7cd462e12..000000000 Binary files a/final-visuals/android/ux-phase1/05-history-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/05-history-light.png b/final-visuals/android/ux-phase1/05-history-light.png deleted file mode 100644 index f0481eadf..000000000 Binary files a/final-visuals/android/ux-phase1/05-history-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/06-settings-dark.png b/final-visuals/android/ux-phase1/06-settings-dark.png deleted file mode 100644 index 7075a726c..000000000 Binary files a/final-visuals/android/ux-phase1/06-settings-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/06-settings-light.png b/final-visuals/android/ux-phase1/06-settings-light.png deleted file mode 100644 index 0991c0adc..000000000 Binary files a/final-visuals/android/ux-phase1/06-settings-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/07-integrations-dark.png b/final-visuals/android/ux-phase1/07-integrations-dark.png deleted file mode 100644 index 34a9aa91e..000000000 Binary files a/final-visuals/android/ux-phase1/07-integrations-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase1/07-integrations-light.png b/final-visuals/android/ux-phase1/07-integrations-light.png deleted file mode 100644 index f19dc4477..000000000 Binary files a/final-visuals/android/ux-phase1/07-integrations-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/01-home-dark.png b/final-visuals/android/ux-phase2/01-home-dark.png deleted file mode 100644 index e0882c53a..000000000 Binary files a/final-visuals/android/ux-phase2/01-home-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/01-home-light.png b/final-visuals/android/ux-phase2/01-home-light.png deleted file mode 100644 index ba53bacb0..000000000 Binary files a/final-visuals/android/ux-phase2/01-home-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/02-just-lift-dark.png b/final-visuals/android/ux-phase2/02-just-lift-dark.png deleted file mode 100644 index 3103be027..000000000 Binary files a/final-visuals/android/ux-phase2/02-just-lift-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/02-just-lift-light.png b/final-visuals/android/ux-phase2/02-just-lift-light.png deleted file mode 100644 index a9de3656a..000000000 Binary files a/final-visuals/android/ux-phase2/02-just-lift-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/03-analytics-dark.png b/final-visuals/android/ux-phase2/03-analytics-dark.png deleted file mode 100644 index 644da8cb2..000000000 Binary files a/final-visuals/android/ux-phase2/03-analytics-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/03-analytics-light.png b/final-visuals/android/ux-phase2/03-analytics-light.png deleted file mode 100644 index 03226b102..000000000 Binary files a/final-visuals/android/ux-phase2/03-analytics-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/04-smart-insights-dark.png b/final-visuals/android/ux-phase2/04-smart-insights-dark.png deleted file mode 100644 index 9c7b78072..000000000 Binary files a/final-visuals/android/ux-phase2/04-smart-insights-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/04-smart-insights-light.png b/final-visuals/android/ux-phase2/04-smart-insights-light.png deleted file mode 100644 index 408213dbd..000000000 Binary files a/final-visuals/android/ux-phase2/04-smart-insights-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/05-settings-dark.png b/final-visuals/android/ux-phase2/05-settings-dark.png deleted file mode 100644 index d73dd18f4..000000000 Binary files a/final-visuals/android/ux-phase2/05-settings-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/05-settings-light.png b/final-visuals/android/ux-phase2/05-settings-light.png deleted file mode 100644 index bcbc23b57..000000000 Binary files a/final-visuals/android/ux-phase2/05-settings-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/06-training-cycles-dark.png b/final-visuals/android/ux-phase2/06-training-cycles-dark.png deleted file mode 100644 index 063fe85d2..000000000 Binary files a/final-visuals/android/ux-phase2/06-training-cycles-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/06-training-cycles-editor-dark.png b/final-visuals/android/ux-phase2/06-training-cycles-editor-dark.png deleted file mode 100644 index 1dba41af7..000000000 Binary files a/final-visuals/android/ux-phase2/06-training-cycles-editor-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/06-training-cycles-editor-light.png b/final-visuals/android/ux-phase2/06-training-cycles-editor-light.png deleted file mode 100644 index 66d1bfed3..000000000 Binary files a/final-visuals/android/ux-phase2/06-training-cycles-editor-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/06-training-cycles-light.png b/final-visuals/android/ux-phase2/06-training-cycles-light.png deleted file mode 100644 index 48e090d2d..000000000 Binary files a/final-visuals/android/ux-phase2/06-training-cycles-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/07-exercise-picker-dark.png b/final-visuals/android/ux-phase2/07-exercise-picker-dark.png deleted file mode 100644 index a8ea5a1af..000000000 Binary files a/final-visuals/android/ux-phase2/07-exercise-picker-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase2/07-exercise-picker-light.png b/final-visuals/android/ux-phase2/07-exercise-picker-light.png deleted file mode 100644 index 47ab9aa19..000000000 Binary files a/final-visuals/android/ux-phase2/07-exercise-picker-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/cards/badges-dark.png b/final-visuals/android/ux-phase3/cards/badges-dark.png deleted file mode 100644 index 612c79f23..000000000 Binary files a/final-visuals/android/ux-phase3/cards/badges-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/cards/badges-light.png b/final-visuals/android/ux-phase3/cards/badges-light.png deleted file mode 100644 index 94962583f..000000000 Binary files a/final-visuals/android/ux-phase3/cards/badges-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/cards/cycles-dark-empty.png b/final-visuals/android/ux-phase3/cards/cycles-dark-empty.png deleted file mode 100644 index 82b6d20fc..000000000 Binary files a/final-visuals/android/ux-phase3/cards/cycles-dark-empty.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/cards/cycles-light-empty.png b/final-visuals/android/ux-phase3/cards/cycles-light-empty.png deleted file mode 100644 index 1f2c2ca15..000000000 Binary files a/final-visuals/android/ux-phase3/cards/cycles-light-empty.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/cards/history-dark-empty.png b/final-visuals/android/ux-phase3/cards/history-dark-empty.png deleted file mode 100644 index 554f0b018..000000000 Binary files a/final-visuals/android/ux-phase3/cards/history-dark-empty.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/cards/history-light-empty.png b/final-visuals/android/ux-phase3/cards/history-light-empty.png deleted file mode 100644 index 316fda576..000000000 Binary files a/final-visuals/android/ux-phase3/cards/history-light-empty.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/cards/insights-dark.png b/final-visuals/android/ux-phase3/cards/insights-dark.png deleted file mode 100644 index 35d2613db..000000000 Binary files a/final-visuals/android/ux-phase3/cards/insights-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/cards/insights-dashboard-dark.png b/final-visuals/android/ux-phase3/cards/insights-dashboard-dark.png deleted file mode 100644 index d77c77d7f..000000000 Binary files a/final-visuals/android/ux-phase3/cards/insights-dashboard-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/cards/insights-dashboard-light.png b/final-visuals/android/ux-phase3/cards/insights-dashboard-light.png deleted file mode 100644 index 91ddecc84..000000000 Binary files a/final-visuals/android/ux-phase3/cards/insights-dashboard-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/cards/insights-light.png b/final-visuals/android/ux-phase3/cards/insights-light.png deleted file mode 100644 index 3558be501..000000000 Binary files a/final-visuals/android/ux-phase3/cards/insights-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/cards/integrations-dark.png b/final-visuals/android/ux-phase3/cards/integrations-dark.png deleted file mode 100644 index 9625235b3..000000000 Binary files a/final-visuals/android/ux-phase3/cards/integrations-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/cards/integrations-light.png b/final-visuals/android/ux-phase3/cards/integrations-light.png deleted file mode 100644 index 07fe6166d..000000000 Binary files a/final-visuals/android/ux-phase3/cards/integrations-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/delete-dialog-dark.png b/final-visuals/android/ux-phase3/delete-dialog-dark.png deleted file mode 100644 index f44097078..000000000 Binary files a/final-visuals/android/ux-phase3/delete-dialog-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/delete-dialog-light.png b/final-visuals/android/ux-phase3/delete-dialog-light.png deleted file mode 100644 index 212336f52..000000000 Binary files a/final-visuals/android/ux-phase3/delete-dialog-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/echo-selector-dark.png b/final-visuals/android/ux-phase3/echo-selector-dark.png deleted file mode 100644 index 65cf4f4a2..000000000 Binary files a/final-visuals/android/ux-phase3/echo-selector-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/echo-selector-light.png b/final-visuals/android/ux-phase3/echo-selector-light.png deleted file mode 100644 index 6d73aafd5..000000000 Binary files a/final-visuals/android/ux-phase3/echo-selector-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/integrations-spinners-dark.png b/final-visuals/android/ux-phase3/integrations-spinners-dark.png deleted file mode 100644 index ebb922d88..000000000 Binary files a/final-visuals/android/ux-phase3/integrations-spinners-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/integrations-spinners-light.png b/final-visuals/android/ux-phase3/integrations-spinners-light.png deleted file mode 100644 index d7e3f9226..000000000 Binary files a/final-visuals/android/ux-phase3/integrations-spinners-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/justlift-dark.png b/final-visuals/android/ux-phase3/justlift-dark.png deleted file mode 100644 index 3139b2f96..000000000 Binary files a/final-visuals/android/ux-phase3/justlift-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/justlift-light.png b/final-visuals/android/ux-phase3/justlift-light.png deleted file mode 100644 index d42c9a7e6..000000000 Binary files a/final-visuals/android/ux-phase3/justlift-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/routines-dark.png b/final-visuals/android/ux-phase3/routines-dark.png deleted file mode 100644 index a062785b7..000000000 Binary files a/final-visuals/android/ux-phase3/routines-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/routines-light.png b/final-visuals/android/ux-phase3/routines-light.png deleted file mode 100644 index 4aeaad075..000000000 Binary files a/final-visuals/android/ux-phase3/routines-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/smartinsights-dark.png b/final-visuals/android/ux-phase3/smartinsights-dark.png deleted file mode 100644 index dd65697cf..000000000 Binary files a/final-visuals/android/ux-phase3/smartinsights-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/smartinsights-light.png b/final-visuals/android/ux-phase3/smartinsights-light.png deleted file mode 100644 index 4624f21ae..000000000 Binary files a/final-visuals/android/ux-phase3/smartinsights-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/workoutsetup-picker-dark.png b/final-visuals/android/ux-phase3/workoutsetup-picker-dark.png deleted file mode 100644 index e4e60f948..000000000 Binary files a/final-visuals/android/ux-phase3/workoutsetup-picker-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase3/workoutsetup-picker-light.png b/final-visuals/android/ux-phase3/workoutsetup-picker-light.png deleted file mode 100644 index c8a22c538..000000000 Binary files a/final-visuals/android/ux-phase3/workoutsetup-picker-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4a/backup-dialog-dark.png b/final-visuals/android/ux-phase4a/backup-dialog-dark.png deleted file mode 100644 index 25c9ef301..000000000 Binary files a/final-visuals/android/ux-phase4a/backup-dialog-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4a/backup-dialog-light.png b/final-visuals/android/ux-phase4a/backup-dialog-light.png deleted file mode 100644 index 6037833d8..000000000 Binary files a/final-visuals/android/ux-phase4a/backup-dialog-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4a/banner-disconnected-dark.png b/final-visuals/android/ux-phase4a/banner-disconnected-dark.png deleted file mode 100644 index 910e986fa..000000000 Binary files a/final-visuals/android/ux-phase4a/banner-disconnected-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4a/banner-disconnected-light.png b/final-visuals/android/ux-phase4a/banner-disconnected-light.png deleted file mode 100644 index 6c8c325d4..000000000 Binary files a/final-visuals/android/ux-phase4a/banner-disconnected-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4a/disconnect-confirm-dark.png b/final-visuals/android/ux-phase4a/disconnect-confirm-dark.png deleted file mode 100644 index ebb922d88..000000000 Binary files a/final-visuals/android/ux-phase4a/disconnect-confirm-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4a/disconnect-confirm-light.png b/final-visuals/android/ux-phase4a/disconnect-confirm-light.png deleted file mode 100644 index d7e3f9226..000000000 Binary files a/final-visuals/android/ux-phase4a/disconnect-confirm-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4a/eula-age-row-dark.png b/final-visuals/android/ux-phase4a/eula-age-row-dark.png deleted file mode 100644 index b1171c352..000000000 Binary files a/final-visuals/android/ux-phase4a/eula-age-row-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4a/eula-age-row-light.png b/final-visuals/android/ux-phase4a/eula-age-row-light.png deleted file mode 100644 index 3e23c3999..000000000 Binary files a/final-visuals/android/ux-phase4a/eula-age-row-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4a/exercise-edit-sheet-dark.png b/final-visuals/android/ux-phase4a/exercise-edit-sheet-dark.png deleted file mode 100644 index 4e682a901..000000000 Binary files a/final-visuals/android/ux-phase4a/exercise-edit-sheet-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4a/exercise-edit-sheet-light.png b/final-visuals/android/ux-phase4a/exercise-edit-sheet-light.png deleted file mode 100644 index a7ad00334..000000000 Binary files a/final-visuals/android/ux-phase4a/exercise-edit-sheet-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4a/routine-editor-discard-dark.png b/final-visuals/android/ux-phase4a/routine-editor-discard-dark.png deleted file mode 100644 index 85f982b26..000000000 Binary files a/final-visuals/android/ux-phase4a/routine-editor-discard-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4a/routine-editor-discard-light.png b/final-visuals/android/ux-phase4a/routine-editor-discard-light.png deleted file mode 100644 index 87ee32e08..000000000 Binary files a/final-visuals/android/ux-phase4a/routine-editor-discard-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/00-home-baseline.png b/final-visuals/android/ux-phase4b/00-home-baseline.png deleted file mode 100644 index 46852034d..000000000 Binary files a/final-visuals/android/ux-phase4b/00-home-baseline.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/01-assessment-entry-progress-tab.png b/final-visuals/android/ux-phase4b/01-assessment-entry-progress-tab.png deleted file mode 100644 index be94eeeb0..000000000 Binary files a/final-visuals/android/ux-phase4b/01-assessment-entry-progress-tab.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/01b-assessment-picker-step1of4.png b/final-visuals/android/ux-phase4b/01b-assessment-picker-step1of4.png deleted file mode 100644 index 1c2784b80..000000000 Binary files a/final-visuals/android/ux-phase4b/01b-assessment-picker-step1of4.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/02a-single-exercise-list.png b/final-visuals/android/ux-phase4b/02a-single-exercise-list.png deleted file mode 100644 index e4e60f948..000000000 Binary files a/final-visuals/android/ux-phase4b/02a-single-exercise-list.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/02b-exercise-detail-altbenchpress.png b/final-visuals/android/ux-phase4b/02b-exercise-detail-altbenchpress.png deleted file mode 100644 index e482aa0a5..000000000 Binary files a/final-visuals/android/ux-phase4b/02b-exercise-detail-altbenchpress.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/03a-routine-overview.png b/final-visuals/android/ux-phase4b/03a-routine-overview.png deleted file mode 100644 index 8d03e05cb..000000000 Binary files a/final-visuals/android/ux-phase4b/03a-routine-overview.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/03b-routine-overview-inworkout.png b/final-visuals/android/ux-phase4b/03b-routine-overview-inworkout.png deleted file mode 100644 index d413cb1eb..000000000 Binary files a/final-visuals/android/ux-phase4b/03b-routine-overview-inworkout.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/03c-exit-dialog-sysback.png b/final-visuals/android/ux-phase4b/03c-exit-dialog-sysback.png deleted file mode 100644 index 826fc5f86..000000000 Binary files a/final-visuals/android/ux-phase4b/03c-exit-dialog-sysback.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/03d-exit-dialog-topbarback.png b/final-visuals/android/ux-phase4b/03d-exit-dialog-topbarback.png deleted file mode 100644 index 826fc5f86..000000000 Binary files a/final-visuals/android/ux-phase4b/03d-exit-dialog-topbarback.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/03e-exit-dialog-stopbutton.png b/final-visuals/android/ux-phase4b/03e-exit-dialog-stopbutton.png deleted file mode 100644 index 826fc5f86..000000000 Binary files a/final-visuals/android/ux-phase4b/03e-exit-dialog-stopbutton.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/05a-justlift-setready.png b/final-visuals/android/ux-phase4b/05a-justlift-setready.png deleted file mode 100644 index c1530ce79..000000000 Binary files a/final-visuals/android/ux-phase4b/05a-justlift-setready.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/06a-routine-overview-withexercise.png b/final-visuals/android/ux-phase4b/06a-routine-overview-withexercise.png deleted file mode 100644 index c33afce91..000000000 Binary files a/final-visuals/android/ux-phase4b/06a-routine-overview-withexercise.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/06b-routine-overview-withexercise-bleblocked.png b/final-visuals/android/ux-phase4b/06b-routine-overview-withexercise-bleblocked.png deleted file mode 100644 index c33afce91..000000000 Binary files a/final-visuals/android/ux-phase4b/06b-routine-overview-withexercise-bleblocked.png and /dev/null differ diff --git a/final-visuals/android/ux-phase4b/06c-exit-dialog-overview-withexercise.png b/final-visuals/android/ux-phase4b/06c-exit-dialog-overview-withexercise.png deleted file mode 100644 index abb44b88e..000000000 Binary files a/final-visuals/android/ux-phase4b/06c-exit-dialog-overview-withexercise.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/badges-streak-dark.png b/final-visuals/android/ux-phase5a/badges-streak-dark.png deleted file mode 100644 index 75dbf9a40..000000000 Binary files a/final-visuals/android/ux-phase5a/badges-streak-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/badges-streak-light.png b/final-visuals/android/ux-phase5a/badges-streak-light.png deleted file mode 100644 index a4eaf3847..000000000 Binary files a/final-visuals/android/ux-phase5a/badges-streak-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/daystrip-dark.png b/final-visuals/android/ux-phase5a/daystrip-dark.png deleted file mode 100644 index 62b4875fc..000000000 Binary files a/final-visuals/android/ux-phase5a/daystrip-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/daystrip-light.png b/final-visuals/android/ux-phase5a/daystrip-light.png deleted file mode 100644 index bf8617ca4..000000000 Binary files a/final-visuals/android/ux-phase5a/daystrip-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/diagnostics-waiting-dark.png b/final-visuals/android/ux-phase5a/diagnostics-waiting-dark.png deleted file mode 100644 index 678719bae..000000000 Binary files a/final-visuals/android/ux-phase5a/diagnostics-waiting-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/diagnostics-waiting-light.png b/final-visuals/android/ux-phase5a/diagnostics-waiting-light.png deleted file mode 100644 index fdb3336df..000000000 Binary files a/final-visuals/android/ux-phase5a/diagnostics-waiting-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/empty-state-dark.png b/final-visuals/android/ux-phase5a/empty-state-dark.png deleted file mode 100644 index 4b18a7ec5..000000000 Binary files a/final-visuals/android/ux-phase5a/empty-state-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/empty-state-light.png b/final-visuals/android/ux-phase5a/empty-state-light.png deleted file mode 100644 index 23c0bdf94..000000000 Binary files a/final-visuals/android/ux-phase5a/empty-state-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/exercise-config-header-dark.png b/final-visuals/android/ux-phase5a/exercise-config-header-dark.png deleted file mode 100644 index f313713ca..000000000 Binary files a/final-visuals/android/ux-phase5a/exercise-config-header-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/exercise-config-header-light.png b/final-visuals/android/ux-phase5a/exercise-config-header-light.png deleted file mode 100644 index 933dd5a21..000000000 Binary files a/final-visuals/android/ux-phase5a/exercise-config-header-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/home-gradient-dark.png b/final-visuals/android/ux-phase5a/home-gradient-dark.png deleted file mode 100644 index baed86867..000000000 Binary files a/final-visuals/android/ux-phase5a/home-gradient-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/home-gradient-light.png b/final-visuals/android/ux-phase5a/home-gradient-light.png deleted file mode 100644 index 3c05ce447..000000000 Binary files a/final-visuals/android/ux-phase5a/home-gradient-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/reducemotion-badges-dark.png b/final-visuals/android/ux-phase5a/reducemotion-badges-dark.png deleted file mode 100644 index f95bfc533..000000000 Binary files a/final-visuals/android/ux-phase5a/reducemotion-badges-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/reducemotion-badges-light.png b/final-visuals/android/ux-phase5a/reducemotion-badges-light.png deleted file mode 100644 index 267900a3c..000000000 Binary files a/final-visuals/android/ux-phase5a/reducemotion-badges-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/reducemotion-empty-state-dark.png b/final-visuals/android/ux-phase5a/reducemotion-empty-state-dark.png deleted file mode 100644 index 4b18a7ec5..000000000 Binary files a/final-visuals/android/ux-phase5a/reducemotion-empty-state-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/reducemotion-empty-state-light.png b/final-visuals/android/ux-phase5a/reducemotion-empty-state-light.png deleted file mode 100644 index 23c0bdf94..000000000 Binary files a/final-visuals/android/ux-phase5a/reducemotion-empty-state-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/routine-groupheader-dark.png b/final-visuals/android/ux-phase5a/routine-groupheader-dark.png deleted file mode 100644 index fa6b3d33a..000000000 Binary files a/final-visuals/android/ux-phase5a/routine-groupheader-dark.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5a/routine-groupheader-light.png b/final-visuals/android/ux-phase5a/routine-groupheader-light.png deleted file mode 100644 index 70b3a4bd5..000000000 Binary files a/final-visuals/android/ux-phase5a/routine-groupheader-light.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5b/autostart-disconnected.png b/final-visuals/android/ux-phase5b/autostart-disconnected.png deleted file mode 100644 index dd9431d0a..000000000 Binary files a/final-visuals/android/ux-phase5b/autostart-disconnected.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5b/justlift-baseline.png b/final-visuals/android/ux-phase5b/justlift-baseline.png deleted file mode 100644 index dd9431d0a..000000000 Binary files a/final-visuals/android/ux-phase5b/justlift-baseline.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5b/sweep-history.png b/final-visuals/android/ux-phase5b/sweep-history.png deleted file mode 100644 index a609f008c..000000000 Binary files a/final-visuals/android/ux-phase5b/sweep-history.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5b/sweep-routines.png b/final-visuals/android/ux-phase5b/sweep-routines.png deleted file mode 100644 index 29a00c3a4..000000000 Binary files a/final-visuals/android/ux-phase5b/sweep-routines.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5b/sweep-settings.png b/final-visuals/android/ux-phase5b/sweep-settings.png deleted file mode 100644 index 8e55eacc1..000000000 Binary files a/final-visuals/android/ux-phase5b/sweep-settings.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5b/workoutsetup-sheet-scrolled.png b/final-visuals/android/ux-phase5b/workoutsetup-sheet-scrolled.png deleted file mode 100644 index 868b2469e..000000000 Binary files a/final-visuals/android/ux-phase5b/workoutsetup-sheet-scrolled.png and /dev/null differ diff --git a/final-visuals/android/ux-phase5b/workoutsetup-sheet.png b/final-visuals/android/ux-phase5b/workoutsetup-sheet.png deleted file mode 100644 index dd8855139..000000000 Binary files a/final-visuals/android/ux-phase5b/workoutsetup-sheet.png and /dev/null differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9da56dc5e..6477f955e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,9 +64,6 @@ reorderable = "3.1.0" compottie = "2.2.2" # Testing -# Media3 (ExoPlayer - for HLS video support) -media3 = "1.10.1" - junit = "4.13.2" mockk = "1.14.11" turbine = "1.2.1" @@ -184,11 +181,6 @@ reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reo compottie = { module = "io.github.alexzhirkevich:compottie", version.ref = "compottie" } compottie-resources = { module = "io.github.alexzhirkevich:compottie-resources", version.ref = "compottie" } -# Media3 (ExoPlayer - Android video with HLS support) -media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } -media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "media3" } -media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" } - [plugins] # Android diff --git a/iOS_INSTALL.md b/iOS_INSTALL.md index b80f5d96b..1c14d9a9e 100644 --- a/iOS_INSTALL.md +++ b/iOS_INSTALL.md @@ -1,6 +1,6 @@ # iOS Installation Guide -This guide explains how to install Vitruvian Phoenix on your iPhone or iPad. +This guide explains how to install Project Phoenix on your iPhone or iPad. ## Prerequisites @@ -20,7 +20,7 @@ The app is currently in beta. Join our TestFlight beta program for easy installa ### Step 2: Join the Beta -1. [Tap here to join the Vitruvian Phoenix beta](https://testflight.apple.com/join/TFw1m89R) +1. [Tap here to join the Project Phoenix beta](https://testflight.apple.com/join/TFw1m89R) 2. Or open this link on your device: `https://testflight.apple.com/join/TFw1m89R` 3. Tap **Accept** to join the beta program 4. Tap **Install** to download the beta version @@ -64,7 +64,7 @@ AltStore is a free app that lets you sideload apps using your Apple ID. 5. Enter your Apple ID and password when prompted 6. AltStore will appear on your iPhone home screen -#### Step 3: Install Vitruvian Phoenix +#### Step 3: Install Project Phoenix 1. Download the `VitruvianPhoenix.ipa` file from [GitHub Releases](../../releases) 2. Open AltStore on your iPhone @@ -99,7 +99,7 @@ When you first launch the app, you'll be asked to grant permissions: - Required to scan for and connect to your Vitruvian trainer - Tap **OK** or **Allow** when prompted -**Note:** If you deny Bluetooth permission, the app cannot connect to your trainer. You can grant it later in Settings > Vitruvian Phoenix > Bluetooth. +**Note:** If you deny Bluetooth permission, the app cannot connect to your trainer. You can grant it later in Settings > Project Phoenix > Bluetooth. --- diff --git a/iosApp/README.md b/iosApp/README.md index 8cda9d5c4..6614a5871 100644 --- a/iosApp/README.md +++ b/iosApp/README.md @@ -1,4 +1,4 @@ -# Vitruvian Phoenix - iOS App +# Project Phoenix - iOS App iOS application for controlling Vitruvian Trainer workout machines via BLE. @@ -44,7 +44,7 @@ it in Supabase and update the GitHub secrets. 1. Open Xcode and create a new iOS App project: - Product Name: `VitruvianPhoenix` - Team: Your development team - - Organization Identifier: `com.example.vitruvianredux` + - Organization Identifier: `com.devil.phoenixproject` - Interface: SwiftUI - Language: Swift @@ -95,7 +95,7 @@ iosApp/ └── Info.plist # App configuration shared/src/iosMain/ -├── kotlin/com/example/vitruvianredux/ +├── kotlin/com/devil/phoenixproject/ │ ├── MainViewController.kt # Compose UI entry point │ ├── Platform.ios.kt # Platform info │ ├── data/ @@ -108,7 +108,6 @@ shared/src/iosMain/ │ ├── presentation/components/ │ │ ├── CompactNumberPicker.ios.kt # Wheel picker │ │ ├── HapticFeedbackEffect.ios.kt # Haptic feedback -│ │ └── VideoPlayer.ios.kt # Video player stub │ └── util/CsvExporter.ios.kt # CSV export & sharing ``` diff --git a/iosApp/TESTFLIGHT_NOTES.txt b/iosApp/TESTFLIGHT_NOTES.txt index e706d0a04..3aac9b9e5 100644 --- a/iosApp/TESTFLIGHT_NOTES.txt +++ b/iosApp/TESTFLIGHT_NOTES.txt @@ -4,7 +4,7 @@ Community-built app to keep Vitruvian Trainer machines working after company clo CORE FEATURES: - BLE connection to V-Form Trainer and Trainer+ machines -- 572 exercises with video demonstrations +- 870+ openly licensed exercises (free-exercise-db) with demonstration images - Real-time workout metrics (load, position, velocity, power) WORKOUT MODES: @@ -27,5 +27,8 @@ TRACKING: - Workout history and analytics - Badge achievements and streak tracking -Early beta - requires physical Vitruvian Trainer. -Report issues: github.com/DasBluEyedDevil/VitruvianProjectPhoenix +Early beta - requires a compatible V-Form or Trainer+ machine. +Report issues: github.com/9thLevelSoftware/Project-Phoenix-MP + +Project Phoenix is an independent community project and is not affiliated with, +endorsed by, or sponsored by Vitruvian. All trademarks belong to their owners. diff --git a/iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj b/iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj index 5f22c801a..760b596b6 100644 --- a/iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj +++ b/iosApp/VitruvianPhoenix/VitruvianPhoenix.xcodeproj/project.pbxproj @@ -367,7 +367,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.9.6; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.devil.phoenixproject.projectphoenix; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -412,7 +412,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.9.6; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.devil.phoenixproject.projectphoenix; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; diff --git a/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/local/SchemaParityTest.kt b/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/local/SchemaParityTest.kt index 84555c3bb..50a74f2c5 100644 --- a/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/local/SchemaParityTest.kt +++ b/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/local/SchemaParityTest.kt @@ -347,7 +347,7 @@ class SchemaParityTest { id, name, created, muscleGroup, muscleGroups, equipment, popularity, archived, isFavorite, isCustom, timesPerformed, defaultCableConfig ) VALUES ( - 'UjIGHxCav-lS9B2I', 'Squat', 0, 'LEGS', 'LEGS', '', + 'legacy-squat', 'Squat', 0, 'LEGS', 'LEGS', '', 0, 0, 0, 0, 0, 'DOUBLE' ) """.trimIndent(), @@ -361,7 +361,7 @@ class SchemaParityTest { id, name, created, muscleGroup, muscleGroups, equipment, popularity, archived, isFavorite, isCustom, timesPerformed, defaultCableConfig ) VALUES ( - 'U9nn8f-vcAltrR-E', 'Plank', 0, 'CORE', 'CORE', '', + 'legacy-plank', 'Plank', 0, 'CORE', 'CORE', '', 0, 0, 0, 0, 0, 'DOUBLE' ) """.trimIndent(), @@ -391,7 +391,7 @@ class SchemaParityTest { INSERT INTO RoutineExercise ( id, routineId, exerciseName, exerciseMuscleGroup, exerciseEquipment, exerciseId, orderIndex, weightPerCableKg ) VALUES ( - 'rex-squat', 'routine-635', 'Squat', 'LEGS', '', 'UjIGHxCav-lS9B2I', 1, 40.0 + 'rex-squat', 'routine-635', 'Squat', 'LEGS', '', 'legacy-squat', 1, 40.0 ) """.trimIndent(), 0, @@ -428,8 +428,8 @@ class SchemaParityTest { assertEquals(true, columnExistsInDriver(driver, "RoutineExercise", "isBodyweight")) // Squat backfilled to explicit cable (0); Plank untouched (NULL = derived) - assertEquals("0", queryScalar(driver, "SELECT CAST(isBodyweight AS TEXT) FROM Exercise WHERE id = 'UjIGHxCav-lS9B2I'")) - assertEquals(null, queryScalar(driver, "SELECT CAST(isBodyweight AS TEXT) FROM Exercise WHERE id = 'U9nn8f-vcAltrR-E'")) + assertEquals("0", queryScalar(driver, "SELECT CAST(isBodyweight AS TEXT) FROM Exercise WHERE id = 'legacy-squat'")) + assertEquals(null, queryScalar(driver, "SELECT CAST(isBodyweight AS TEXT) FROM Exercise WHERE id = 'legacy-plank'")) // Sentinel converted to explicit flag and cleared from equipment assertEquals("1", queryScalar(driver, "SELECT CAST(isBodyweight AS TEXT) FROM RoutineExercise WHERE id = 'rex-sentinel'")) @@ -464,7 +464,7 @@ class SchemaParityTest { id, name, created, muscleGroup, muscleGroups, equipment, popularity, archived, isFavorite, isCustom, timesPerformed, defaultCableConfig ) VALUES ( - 'UjIGHxCav-lS9B2I', 'Squat', 0, 'LEGS', 'LEGS', '', + 'legacy-squat', 'Squat', 0, 'LEGS', 'LEGS', '', 0, 0, 0, 0, 0, 'DOUBLE' ) """.trimIndent(), @@ -491,7 +491,7 @@ class SchemaParityTest { migrateWithResilience(driver, 39, 40) assertEquals(true, columnExistsInDriver(driver, "RoutineExercise", "isBodyweight")) - assertEquals("0", queryScalar(driver, "SELECT CAST(isBodyweight AS TEXT) FROM Exercise WHERE id = 'UjIGHxCav-lS9B2I'")) + assertEquals("0", queryScalar(driver, "SELECT CAST(isBodyweight AS TEXT) FROM Exercise WHERE id = 'legacy-squat'")) assertEquals("1", queryScalar(driver, "SELECT CAST(isBodyweight AS TEXT) FROM RoutineExercise WHERE id = 'rex-sentinel-resilient'")) assertEquals("", queryScalar(driver, "SELECT exerciseEquipment FROM RoutineExercise WHERE id = 'rex-sentinel-resilient'")) } diff --git a/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/repository/SqlDelightSyncRepositoryTest.kt b/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/repository/SqlDelightSyncRepositoryTest.kt index db82935bc..a93f37674 100644 --- a/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/repository/SqlDelightSyncRepositoryTest.kt +++ b/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/repository/SqlDelightSyncRepositoryTest.kt @@ -988,7 +988,7 @@ class SqlDelightSyncRepositoryTest { // snapshot Exercises re-derived isBodyweight from the equipment string. // Catalog cable lift with empty equipment and an explicit stored flag (Squat) database.vitruvianDatabaseQueries.insertExercise( - id = "UjIGHxCav-lS9B2I", + id = "legacy-squat", name = "Squat", displayName = "Squat", description = null, @@ -1063,7 +1063,7 @@ class SqlDelightSyncRepositoryTest { routineId = "routine-635", name = "Squat", muscleGroup = "LEGS", - exerciseId = "UjIGHxCav-lS9B2I", + exerciseId = "legacy-squat", orderIndex = 3, reps = 5, weight = 60f, diff --git a/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/e2e/WorkoutFlowE2ETest.kt b/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/e2e/WorkoutFlowE2ETest.kt index 7b48f28a6..f01dc43cf 100644 --- a/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/e2e/WorkoutFlowE2ETest.kt +++ b/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/e2e/WorkoutFlowE2ETest.kt @@ -352,7 +352,7 @@ class WorkoutFlowE2ETest { advanceUntilIdle() localRobot.verifyWorkoutActive() - // Official activation starts send CONFIG (0x04) only, without legacy START (0x03). + // Activation sends CONFIG (0x04) only; the legacy START (0x03) command is not sent. kotlin.test.assertEquals(1, fakeBleRepository.commandsReceived.size) kotlin.test.assertEquals(0x04.toByte(), fakeBleRepository.commandsReceived[0][0]) kotlin.test.assertFalse(fakeBleRepository.commandsReceived.any { it.firstOrNull() == 0x03.toByte() }) diff --git a/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/viewmodel/DiagnosticsViewModelTest.kt b/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/viewmodel/DiagnosticsViewModelTest.kt index df3a6c25d..60c2d0c63 100644 --- a/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/viewmodel/DiagnosticsViewModelTest.kt +++ b/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/viewmodel/DiagnosticsViewModelTest.kt @@ -47,10 +47,10 @@ class DiagnosticsViewModelTest { val state = awaitItemAfterUpdates { it.packet != null } assertTrue(state.isConnected) assertEquals(42L, state.packet?.runtimeSeconds) - assertEquals("TI restarted", state.faults[0].label) - assertEquals("Over voltage", state.faults[2].label) - assertTrue(state.exportText.contains("Vee: TI restarted (0x0004 / 4)")) - assertTrue(state.exportText.contains("Motor A: Over voltage (0x0004 / 4)")) + assertEquals("Controller restarted", state.faults[0].label) + assertEquals("Overvoltage", state.faults[2].label) + assertTrue(state.exportText.contains("Controller: Controller restarted (0x0004 / 4)")) + assertTrue(state.exportText.contains("Motor A: Overvoltage (0x0004 / 4)")) cancelAndIgnoreRemainingEvents() } diff --git a/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/viewmodel/MainViewModelTest.kt b/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/viewmodel/MainViewModelTest.kt index 0eb64dad2..aad593c9d 100644 --- a/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/viewmodel/MainViewModelTest.kt +++ b/shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/viewmodel/MainViewModelTest.kt @@ -624,7 +624,7 @@ class MainViewModelTest { advanceUntilIdle() assertEquals(WorkoutState.Active, viewModel.workoutState.value) - // Official activation starts send CONFIG (0x04) only, without legacy START (0x03). + // Activation sends CONFIG (0x04) only; the legacy START (0x03) command is not sent. assertEquals(1, fakeBleRepository.commandsReceived.size) assertEquals(0x04.toByte(), fakeBleRepository.commandsReceived[0][0]) assertFalse(fakeBleRepository.commandsReceived.any { it.firstOrNull() == 0x03.toByte() }) diff --git a/shared/src/androidMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutTabPreviews.kt b/shared/src/androidMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutTabPreviews.kt index bc06c2a82..9364a403a 100644 --- a/shared/src/androidMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutTabPreviews.kt +++ b/shared/src/androidMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutTabPreviews.kt @@ -406,7 +406,7 @@ private fun WorkoutTabCountdownPreview() { exercise = Exercise( name = "Bench Press", muscleGroup = "Chest", - equipment = "Vitruvian", + equipment = "Cable", id = "bench-press", ), orderIndex = 0, @@ -520,7 +520,7 @@ private fun WorkoutTabRestingPreview() { /** * Preview of WorkoutTab in set summary state - shows enhanced stats after completing a set. - * Updated to showcase the new SetSummaryCard matching the official Vitruvian app design. + * Updated to showcase the new SetSummaryCard. */ @Preview( name = "WorkoutTab - Set Summary (Enhanced)", @@ -775,7 +775,7 @@ private fun WorkoutTabCompletedWithNextExercisePreview() { exercise = Exercise( name = "Bench Press", muscleGroup = "Chest", - equipment = "Vitruvian", + equipment = "Cable", id = "bench-press", ), orderIndex = 0, @@ -789,7 +789,7 @@ private fun WorkoutTabCompletedWithNextExercisePreview() { exercise = Exercise( name = "Bent Over Rows", muscleGroup = "Back", - equipment = "Vitruvian", + equipment = "Cable", id = "rows", ), orderIndex = 1, diff --git a/shared/src/androidMain/kotlin/com/devil/phoenixproject/util/CsvExporter.android.kt b/shared/src/androidMain/kotlin/com/devil/phoenixproject/util/CsvExporter.android.kt index 23fb1da78..0ec327a7a 100644 --- a/shared/src/androidMain/kotlin/com/devil/phoenixproject/util/CsvExporter.android.kt +++ b/shared/src/androidMain/kotlin/com/devil/phoenixproject/util/CsvExporter.android.kt @@ -81,7 +81,7 @@ class AndroidCsvExporter(private val context: Context) : CsvExporter { ?: exerciseNames[session.exerciseId] ?: "Unknown" val date = formatDate(session.timestamp) - // For Echo mode, use peak weight (matches official app behavior); otherwise use configured weight + // For Echo mode, peak weight is the meaningful load; otherwise use configured weight val isEchoMode = session.mode.contains("Echo", ignoreCase = true) val effectiveWeight = if (isEchoMode) { session.peakWeightKg ?: session.workingAvgWeightKg ?: session.weightPerCableKg @@ -174,7 +174,7 @@ class AndroidCsvExporter(private val context: Context) : CsvExporter { val shareIntent = Intent(Intent.ACTION_SEND).apply { type = "text/csv" putExtra(Intent.EXTRA_STREAM, uri) - putExtra(Intent.EXTRA_SUBJECT, "Vitruvian Export: $fileName") + putExtra(Intent.EXTRA_SUBJECT, "Project Phoenix Export: $fileName") addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } diff --git a/shared/src/androidMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.android.kt b/shared/src/androidMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.android.kt index a6fa44a6d..a1eb4a7f6 100644 --- a/shared/src/androidMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.android.kt +++ b/shared/src/androidMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.android.kt @@ -268,7 +268,7 @@ class AndroidDataBackupManager( .replace("-", "") + "_" + KmpUtils.formatTimestamp(KmpUtils.currentTimeMillis(), "HH:mm:ss") .replace(":", "") - val fileName = "vitruvian_backup_$timestamp.json" + val fileName = "phoenix_backup_$timestamp.json" return BackupJsonWriter(File(cacheDir, fileName).absolutePath) } @@ -292,7 +292,7 @@ class AndroidDataBackupManager( val contentValues = ContentValues().apply { put(MediaStore.Downloads.DISPLAY_NAME, fileName) put(MediaStore.Downloads.MIME_TYPE, "application/json") - put(MediaStore.Downloads.RELATIVE_PATH, "Download/VitruvianPhoenix") + put(MediaStore.Downloads.RELATIVE_PATH, "Download/ProjectPhoenix") } val resolver = context.contentResolver @@ -317,7 +317,7 @@ class AndroidDataBackupManager( @Suppress("DEPRECATION") val downloadsDir = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - "VitruvianPhoenix", + "ProjectPhoenix", ) downloadsDir.mkdirs() val destFile = File(downloadsDir, fileName) @@ -341,14 +341,14 @@ class AndroidDataBackupManager( .replace("-", "") + "_" + KmpUtils.formatTimestamp(KmpUtils.currentTimeMillis(), "HH:mm:ss") .replace(":", "") - val fileName = "vitruvian_backup_$timestamp.json" + val fileName = "phoenix_backup_$timestamp.json" val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Android 10+ use MediaStore val contentValues = ContentValues().apply { put(MediaStore.Downloads.DISPLAY_NAME, fileName) put(MediaStore.Downloads.MIME_TYPE, "application/json") - put(MediaStore.Downloads.RELATIVE_PATH, "Download/VitruvianPhoenix") + put(MediaStore.Downloads.RELATIVE_PATH, "Download/ProjectPhoenix") } val resolver = context.contentResolver @@ -365,7 +365,7 @@ class AndroidDataBackupManager( @Suppress("DEPRECATION") val downloadsDir = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - "VitruvianPhoenix", + "ProjectPhoenix", ) downloadsDir.mkdirs() @@ -457,7 +457,7 @@ class AndroidDataBackupManager( val shareIntent = Intent(Intent.ACTION_SEND).apply { type = "application/json" putExtra(Intent.EXTRA_STREAM, uri) - putExtra(Intent.EXTRA_SUBJECT, "Vitruvian Phoenix Backup") + putExtra(Intent.EXTRA_SUBJECT, "Project Phoenix Backup") addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } diff --git a/shared/src/androidMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.android.kt b/shared/src/androidMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.android.kt index 57292168e..60fa4eeff 100644 --- a/shared/src/androidMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.android.kt +++ b/shared/src/androidMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.android.kt @@ -74,7 +74,7 @@ actual object DeviceInfo { // ==================== Formatted Output ==================== actual fun getFormattedInfo(): String = buildString { - appendLine("App: VitruvianPhoenix v$appVersionName (build $appVersionCode)") + appendLine("App: Project Phoenix v$appVersionName (build $appVersionCode)") appendLine("Build Type: $buildType") appendLine() appendLine("Device: $manufacturer $model") diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml index b8832085f..74e420871 100644 --- a/shared/src/commonMain/composeResources/values-de/strings.xml +++ b/shared/src/commonMain/composeResources/values-de/strings.xml @@ -106,7 +106,7 @@ Verbindung fehlgeschlagen Trennen Trennen? - Bist du sicher, dass du die Verbindung zum Vitruvian-Gerät trennen möchtest? + Bist du sicher, dass du die Verbindung zum Gerät trennen möchtest? Einstellungen @@ -522,7 +522,6 @@ Previous exercise Next exercise Completed - Video thumbnail Thumbnail for %1$s Clear filters Alphabet navigation. Tap a letter to jump to exercises starting with that letter. @@ -769,10 +768,10 @@ Phoenix konnte nicht bestätigen, welches Profil aktiv ist. Versuche es erneut, bevor du fortfährst. Profilwiederherstellung ist weiterhin nicht verfügbar „%1$s“ löschen? Trainings, Routinen, Rekorde, Abzeichen, Tests und Fortschrittsdaten werden zu Default verschoben. Dies kann nicht rückgängig gemacht werden. - Videoverhalten - Übungsvideos anzeigen + Übungsdemos + Übungsdemos anzeigen Übungsdemonstrationen anzeigen; auf langsameren Geräten deaktivieren - Videowiedergabe deaktiviert + Übungsdemos deaktiviert Gewichtsschritt Körpergewicht Dauer der Satzzusammenfassung diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml index 74778afe1..22031cfaa 100644 --- a/shared/src/commonMain/composeResources/values-es/strings.xml +++ b/shared/src/commonMain/composeResources/values-es/strings.xml @@ -106,7 +106,7 @@ Conexión fallida Desconectar ¿Desconectar? - ¿Estás seguro de que deseas desconectarte de la máquina Vitruvian? + ¿Estás seguro de que deseas desconectarte de la máquina? Configuración @@ -522,7 +522,6 @@ Previous exercise Next exercise Completed - Video thumbnail Thumbnail for %1$s Clear filters Alphabet navigation. Tap a letter to jump to exercises starting with that letter. @@ -769,10 +768,10 @@ Phoenix no pudo confirmar qué perfil está activo. Vuelve a intentarlo antes de continuar. La recuperación del perfil sigue sin estar disponible ¿Eliminar "%1$s"? Sus entrenamientos, rutinas, récords, insignias, evaluaciones y datos de progreso pasarán a Default. Esta acción no se puede deshacer. - Comportamiento del vídeo - Mostrar vídeos de ejercicios + Demostraciones de ejercicios + Mostrar demostraciones de ejercicios Mostrar demostraciones; desactívalo en dispositivos más lentos - Reproducción de vídeo desactivada + Demostraciones desactivadas Incremento de peso Peso corporal Duración del resumen de serie diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml index ddf9ce53d..97d6e3651 100644 --- a/shared/src/commonMain/composeResources/values-fr/strings.xml +++ b/shared/src/commonMain/composeResources/values-fr/strings.xml @@ -106,7 +106,7 @@ Échec de la connexion Déconnecter Déconnecter ? - Êtes-vous sûr de vouloir vous déconnecter de la machine Vitruvian ? + Êtes-vous sûr de vouloir vous déconnecter de la machine ? Paramètres @@ -522,7 +522,6 @@ Previous exercise Next exercise Completed - Video thumbnail Thumbnail for %1$s Clear filters Alphabet navigation. Tap a letter to jump to exercises starting with that letter. @@ -769,10 +768,10 @@ Phoenix n’a pas pu confirmer quel profil est actif. Réessayez avant de continuer. La récupération du profil est toujours indisponible Supprimer « %1$s » ? Ses entraînements, routines, records, badges, évaluations et données de progression seront transférés vers Default. Cette action est irréversible. - Comportement vidéo - Afficher les vidéos d'exercice + Démonstrations d'exercices + Afficher les démonstrations d'exercices Afficher les démonstrations ; désactivez-les sur les appareils plus lents - Lecture vidéo désactivée + Démonstrations désactivées Incrément de poids Poids corporel Durée du résumé de série diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml index 5a9dfebcf..35f61fbc7 100644 --- a/shared/src/commonMain/composeResources/values-nl/strings.xml +++ b/shared/src/commonMain/composeResources/values-nl/strings.xml @@ -106,7 +106,7 @@ Verbinding mislukt Verbinding verbreken Verbinding verbreken? - Weet je zeker dat je de verbinding met de Vitruvian-machine wilt verbreken? + Weet je zeker dat je de verbinding met de machine wilt verbreken? Instellingen @@ -517,7 +517,6 @@ Vorige oefening Volgende oefening Voltooid - Video-miniatuur Miniatuur voor %1$s Filters wissen Alfabetnavigatie. Tik op een letter om naar oefeningen te springen die met die letter beginnen. @@ -748,10 +747,10 @@ Phoenix kan niet bevestigen welk profiel actief is. Probeer opnieuw voordat u doorgaat. Profielherstel is nog niet beschikbaar "%1$s" verwijderen? Trainingen, routines, records, badges, metingen en voortgang worden naar Default verplaatst. Dit kan niet ongedaan worden gemaakt. - Videogedrag - Oefeningsvideo's tonen + Oefendemonstraties + Oefendemonstraties tonen Toon oefendemonstraties; schakel dit uit op tragere apparaten - Videoweergave uitgeschakeld + Oefendemonstraties uitgeschakeld Gewichtsstap Lichaamsgewicht Duur setoverzicht diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index d3de25264..19b5941c1 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -120,7 +120,7 @@ Connection Failed Disconnect Disconnect? - Are you sure you want to disconnect from the Vitruvian machine? + Are you sure you want to disconnect from the trainer? Disconnect integration? Are you sure you want to disconnect %1$s? This will remove the integration and may interrupt a scheduled sync. Not connected to machine @@ -609,7 +609,6 @@ Previous exercise Next exercise Completed - Video thumbnail Thumbnail for %1$s Clear filters Alphabet navigation. Tap a letter to jump to exercises starting with that letter. @@ -870,8 +869,6 @@ Open Strength Assessment picker View exercise history and 1RM - - Video preview unavailable Connect to enable auto-start @@ -913,10 +910,10 @@ Phoenix could not confirm which profile is active. Retry before continuing. Profile recovery is still unavailable Delete "%1$s"? Its workouts, routines, records, badges, assessments, and progression data will move to Default. This cannot be undone. - Video Behavior + Exercise Demos Show Exercise Demos Display exercise demonstration stills; turn this off on slower devices - Video Playback Disabled + Exercise Demos Disabled Refresh extra exercises from wger Downloads openly licensed exercises from wger (CC-BY-SA). Does not replace the bundled catalogue. Added %1$d exercises from wger diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BleCompatibilityMode.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BleCompatibilityMode.kt index 71cdd3c3e..27deb9605 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BleCompatibilityMode.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BleCompatibilityMode.kt @@ -5,7 +5,7 @@ import com.devil.phoenixproject.util.DeviceInfo import kotlin.concurrent.Volatile /** - * Issue #333: BLE compatibility mode ("official small-MTU path"). + * Issue #333: BLE compatibility mode (small-MTU path). * * Root cause: on BCM4389 Pixels (Tensor G1/G2 generation — Pixel 6/7 phones, * Pixel Fold, Pixel Tablet — all with the Broadcom BCM4389 Bluetooth controller), @@ -14,10 +14,10 @@ import kotlin.concurrent.Volatile * the controller's write lane wedges (WriteRequestBusy forever), then surfaces * GATT_ERROR(133) and drops the link the moment a workout starts. * - * The official Vitruvian app never calls requestMtu(), so it stays at the default - * 23-byte ATT MTU and its 96/34-byte writes are automatically chunked by the - * ATT long-write procedure (Prepare Write + Execute Write), which those - * controllers handle fine. Compatibility mode reproduces that behavior: + * Leaving the ATT MTU at the default 23 bytes means the 96/34-byte writes are + * automatically chunked by the ATT long-write procedure (Prepare Write + + * Execute Write), which those controllers handle fine. Compatibility mode + * uses that path: * - no app-side requestMtu() call * - high connection priority after service discovery * - no GATT heartbeat loop diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BleExceptions.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BleExceptions.kt index 4aa3e70a4..fc18f698a 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BleExceptions.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BleExceptions.kt @@ -1,7 +1,7 @@ package com.devil.phoenixproject.data.ble /** - * Specific BLE exception types matching official Vitruvian app implementation + * Specific BLE exception types for granular connection error handling * These provide granular error handling for different BLE failure scenarios */ diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BlePacketCapture.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BlePacketCapture.kt index 3bc79a96a..b67eeabdf 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BlePacketCapture.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BlePacketCapture.kt @@ -10,7 +10,7 @@ import kotlinx.atomicfu.locks.withLock * BLE Packet Capture Utility for Hardware Validation * * Captures raw BLE bytes from the monitor characteristic and logs them - * in a format that can be pasted directly into HardwareValidationTest. + * in a copyable hex format for offline protocol analysis. * * Usage in debug builds: * ``` @@ -24,7 +24,7 @@ import kotlinx.atomicfu.locks.withLock * // Packets are also logged to Logcat with tag "BlePacketCapture" * ``` * - * Then grep Logcat for "CAPTURE_HEX" and paste into HardwareValidationTest. + * Then grep Logcat for "CAPTURE_HEX". * * To hook this into the polling loop, add ONE line in MetricPollingEngine.parseMonitorData(): * ``` @@ -89,7 +89,7 @@ object BlePacketCapture { log.i { "Packet sizes: $sizes" } // Log copyable test data - log.i { "=== COPY BELOW INTO HardwareValidationTest.CAPTURED_PACKETS ===" } + log.i { "=== CAPTURED PACKETS (copyable) ===" } for ((index, pkt) in result.withIndex()) { val relTime = pkt.timestampMs - startTime log.i { "CAPTURE_HEX[$index] t=${relTime}ms size=${pkt.size}: ${pkt.hex}" } diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoder.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoder.kt index 4db2897c0..b1fafa2fd 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoder.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoder.kt @@ -1,7 +1,7 @@ package com.devil.phoenixproject.data.ble enum class DiagnosticFaultCategory(val displayName: String) { - VITRUVIAN("Vee"), + CONTROLLER("Controller"), OTHER("Other"), MOTOR_A("Motor A"), MOTOR_B("Motor B"), @@ -19,7 +19,7 @@ data class DiagnosticFault( fun decodeDiagnosticFaults(packet: DiagnosticPacket): List { val words = packet.faultWords return listOf( - decodeDiagnosticFault(DiagnosticFaultCategory.VITRUVIAN, words.getOrElse(0) { 0 }), + decodeDiagnosticFault(DiagnosticFaultCategory.CONTROLLER, words.getOrElse(0) { 0 }), decodeDiagnosticFault(DiagnosticFaultCategory.OTHER, words.getOrElse(1) { 0 }), decodeDiagnosticFault(DiagnosticFaultCategory.MOTOR_A, words.getOrElse(2) { 0 }), decodeDiagnosticFault(DiagnosticFaultCategory.MOTOR_B, words.getOrElse(3) { 0 }), @@ -29,7 +29,7 @@ fun decodeDiagnosticFaults(packet: DiagnosticPacket): List { fun decodeDiagnosticFault(category: DiagnosticFaultCategory, code: Int): DiagnosticFault { val normalizedCode = code and 0xFFFF val label = when (category) { - DiagnosticFaultCategory.VITRUVIAN -> decodeVitruvianFault(normalizedCode) + DiagnosticFaultCategory.CONTROLLER -> decodeControllerFault(normalizedCode) DiagnosticFaultCategory.OTHER -> decodeOtherFault(normalizedCode) @@ -44,16 +44,16 @@ fun formatDiagnosticFaultCode(code: Int): String = "0x${(code and 0xFFFF).toStri fun formatDiagnosticUInt32(value: Long): String = "0x${(value and 0xFFFF_FFFFL).toString(16).uppercase().padStart(8, '0')}" -private fun decodeVitruvianFault(code: Int): String = decodeFlaggedFault( +private fun decodeControllerFault(code: Int): String = decodeFlaggedFault( code = code, flags = listOf( - 1 to "No comms", - 2 to "Init failure", - 4 to "TI restarted", - 8 to "Message failure (RX)", - 16 to "Message failure (TX)", + 1 to "Communication lost", + 2 to "Initialisation failure", + 4 to "Controller restarted", + 8 to "RX message failure", + 16 to "TX message failure", 32 to "Firmware update failure", - 64 to "Overtemp failure", + 64 to "Over-temperature", ), ) @@ -65,19 +65,19 @@ private fun decodeOtherFault(code: Int): String = when (code) { private fun decodeMotorFault(code: Int): String = decodeFlaggedFault( code = code, flags = listOf( - 1 to "HW Overcurrent", - 2 to "SW Overcurrent", - 4 to "Over voltage", - 8 to "Under voltage", - 16 to "PIM temp", - 32 to "Gate driver", - 64 to "Bord Temp", + 1 to "Hardware overcurrent", + 2 to "Software overcurrent", + 4 to "Overvoltage", + 8 to "Undervoltage", + 16 to "Power module temperature", + 32 to "Gate driver fault", + 64 to "Board temperature", 128 to "Kill switch", - 256 to "Alignment", - 512 to "Encoder", - 1024 to "HW/FW mismatch", - 2048 to "EEPROM", - 4096 to "Motor overtemp", + 256 to "Alignment fault", + 512 to "Encoder fault", + 1024 to "Hardware/firmware mismatch", + 2048 to "EEPROM fault", + 4096 to "Motor over-temperature", ), ) diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/KableBleConnectionManager.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/KableBleConnectionManager.kt index 8dd1108eb..a42223f61 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/KableBleConnectionManager.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/KableBleConnectionManager.kt @@ -366,7 +366,7 @@ class KableBleConnectionManager( ) // Use name if available, otherwise use identifier as placeholder - val name = advertisedName ?: "Vitruvian ($identifier)" + val name = advertisedName ?: "Trainer ($identifier)" // Skip devices without a real Vitruvian name if we already have one if (!hasRealName) { @@ -542,12 +542,12 @@ class KableBleConnectionManager( log.w { "scanAndConnect: No Vitruvian device found within timeout" } logRepo.error(LogEventType.SCAN_STOP, "No device found", details = "Timeout after ${timeoutMs}ms") reportConnectionState(ConnectionState.Disconnected) - return Result.failure(Exception("No Vitruvian device found")) + return Result.failure(Exception("No trainer found")) } @Suppress("REDUNDANT_CALL_OF_CONVERSION_METHOD") // Needed for iOS where identifier is Uuid val identifier = advertisement.identifier.toString() - val name = advertisement.name ?: "Vitruvian" + val name = advertisement.name ?: "Trainer" log.i { "scanAndConnect: Found device $name ($identifier), connecting..." } // Store for connection @@ -896,13 +896,13 @@ class KableBleConnectionManager( val useCompatibilityPath = compatibilityPathActive if (useCompatibilityPath) { - // Issue #333: official small-MTU path. Android 14+ coerces the FIRST + // Issue #333: small-MTU compatibility path. Android 14+ coerces the FIRST // app-side requestMtu() to ATT MTU 517, and at 517 the 96-byte workout // CONFIG write goes out as a single large ATT PDU that wedges the // BCM4389 controller on Pixel 6/7 (write lane stuck busy → GATT 133 → // disconnect). Making NO MTU request keeps the default 23-byte MTU, so // large writes are chunked by the ATT long-write procedure instead — - // the same path the official Vitruvian app uses. + // a path these controllers handle reliably. negotiatedMtu = null log.i { "[#333 compat] Small-MTU compatibility path active (${BleCompatibilityMode.summary()})" } logRepo.info( @@ -1607,8 +1607,8 @@ class KableBleConnectionManager( ", profileTail[0x4C]=${readFloatLE(command, 0x4C)}" + ", forceMin[0x50]=${readFloatLE(command, 0x50)}" + ", forceMax[0x54]=${readFloatLE(command, 0x54)}" + - ", officialSoftMax[0x58]=${readFloatLE(command, 0x58)}" + - ", officialIncrement[0x5C]=${readFloatLE(command, 0x5C)}" + ", softMax[0x58]=${readFloatLE(command, 0x58)}" + + ", increment[0x5C]=${readFloatLE(command, 0x5C)}" } private fun readFloatLE(data: ByteArray, offset: Int): Float { diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/MetricPollingEngine.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/MetricPollingEngine.kt index b0cc51dbd..e488d48eb 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/MetricPollingEngine.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/MetricPollingEngine.kt @@ -195,7 +195,7 @@ class MetricPollingEngine( * * Issue #333: on the small-MTU compatibility path the GATT heartbeat is * suppressed — BLE link-layer keepalive happens at the connection interval, - * and the official Vitruvian app runs no GATT heartbeat at all. + * and the firmware does not require a GATT-level heartbeat. */ fun startAll(peripheral: Peripheral, includeHeartbeat: Boolean = true) { log.i { "Starting polling loops (includeHeartbeat=$includeHeartbeat)" } diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/MonitorDataProcessor.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/MonitorDataProcessor.kt index 00e6d2606..104499007 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/MonitorDataProcessor.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/MonitorDataProcessor.kt @@ -126,7 +126,7 @@ class MonitorDataProcessor( // ===== STAGE 1: POSITION CLAMPING (last-good fallback) ===== // Replace out-of-range positions with last known good values (BLE noise recovery). - // Valid range: -1000 to +1000 mm per official app documentation. + // Valid range: -1000 to +1000 mm (observed device position envelope). if (posA !in MIN_POS..MAX_POS) { log.w { "Position A out of range: $posA, using last good: $lastGoodPosA" } posA = lastGoodPosA diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/ProtocolModels.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/ProtocolModels.kt index 324e9e582..7020af6e9 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/ProtocolModels.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/ProtocolModels.kt @@ -42,7 +42,7 @@ data class DiagnosticPacket( } /** - * Optional crash details from extended official diagnostics payloads. + * Optional crash details from extended diagnostics payloads. */ data class DiagnosticCrash( val seconds: Long, diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/ProtocolParser.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/ProtocolParser.kt index d4bbd81c0..4825b6818 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/ProtocolParser.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/ProtocolParser.kt @@ -129,7 +129,7 @@ private const val DIAGNOSTIC_WARNING_BYTES = 4 * - Bytes 22-23: repsSetTotal (u16 LE) * * Issue #388 / #174 / #187: V-Form ("Vee_*") firmware sends rep packets in the 6..23 byte - * range (most likely 16 bytes per official `Reps.read()` spec). Earlier Phoenix MP code + * range (16-byte payloads observed on V-Form firmware). Earlier Phoenix MP code * accepted only `==6` or `>=24` and returned null for everything else, regressing the * parent repo fix (VitruvianRedux PR #190 / commit 980df08) that timuh60/IshyEvenTrying * confirmed working in v0.6.2-beta. This restores the catch-all legacy branch. @@ -255,7 +255,7 @@ fun parseMonitorPacket(data: ByteArray): MonitorPacket? { /** * Parse diagnostic characteristic data into DiagnosticPacket. * - * Official app format (Little Endian): + * Diagnostic characteristic layout (Little Endian): * - Empty payload: default/zero diagnostic snapshot * - Bytes 0-3: uptime seconds (uint32) * - Bytes 4-11: 4 fault codes (uint16) diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/local/LegacyCatalogueIdMap.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/local/LegacyCatalogueIdMap.kt index 331ad137a..da9f93bde 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/local/LegacyCatalogueIdMap.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/local/LegacyCatalogueIdMap.kt @@ -1,10 +1,13 @@ package com.devil.phoenixproject.data.local /** - * Maps archived Vitruvian catalogue IDs onto the replacement free-exercise-db slugs - * used by cycle templates and 5/3/1 detection. Name matching covers remaining stock - * rows whose names are unchanged; [nameAliases] and [stemKey] cover reviewed - * singular/plural renames such as Rack Pull → Rack Pulls. + * Maps archived legacy catalogue row IDs onto the replacement free-exercise-db slugs + * used by cycle templates and 5/3/1 detection. The keys are opaque identifiers only — + * this object carries no catalogue content (names, descriptions, media) and exists + * solely so user-owned sessions, PRs, and routines created before migration 43 keep + * resolving. Name matching covers remaining archived rows whose names are unchanged; + * [nameAliases] and [stemKey] cover reviewed singular/plural renames such as + * Rack Pull → Rack Pulls. */ object LegacyCatalogueIdMap { val explicit: Map = mapOf( diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/local/MigrationStatements.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/local/MigrationStatements.kt index fbb3347aa..869fd9e1e 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/local/MigrationStatements.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/local/MigrationStatements.kt @@ -917,24 +917,16 @@ WHERE gs.rowid = ( 39 -> listOf( "ALTER TABLE Exercise ADD COLUMN isBodyweight INTEGER", "ALTER TABLE RoutineExercise ADD COLUMN isBodyweight INTEGER", - """UPDATE Exercise SET isBodyweight = 0 WHERE id IN ( - 'UjIGHxCav-lS9B2I', - 'enuJ_FgAzXDLAweK', - 'KoL_gx00nuf2wncV', - 'kSLyRg4bjLuzTeIM', - '2nTn2QR6MyezFYmK', - 'fAglxv8VMaisUTyo' + """UPDATE Exercise SET isBodyweight = 0 + WHERE isCustom = 0 AND TRIM(name) IN ( + 'Squat', + 'Good Morning', + 'Medial Delt Twist', + 'Kneeling 45 Degree Kickback', + 'Just Lift exercise' )""", """UPDATE RoutineExercise SET isBodyweight = 1, exerciseEquipment = '' WHERE exerciseEquipment = 'Bodyweight'""", - """UPDATE RoutineExercise SET isBodyweight = 0 WHERE exerciseId IN ( - 'UjIGHxCav-lS9B2I', - 'enuJ_FgAzXDLAweK', - 'KoL_gx00nuf2wncV', - 'kSLyRg4bjLuzTeIM', - '2nTn2QR6MyezFYmK', - 'fAglxv8VMaisUTyo' - )""", """UPDATE RoutineExercise SET isBodyweight = 0 WHERE isBodyweight IS NULL AND TRIM(exerciseName) IN ( 'Squat', diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/migration/MigrationManager.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/migration/MigrationManager.kt index 5ab98172b..f70d4781f 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/migration/MigrationManager.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/migration/MigrationManager.kt @@ -1067,7 +1067,7 @@ class MigrationManager( } /** - * Generic placeholder routine names set by external imports (e.g. Vitruvian cloud). + * Generic placeholder routine names set by external imports (e.g. legacy cloud exports). * These don't identify a real routine and should be treated as null/unknown. */ private val GARBAGE_ROUTINE_NAMES = setOf( diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/BleRepository.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/BleRepository.kt index 08fc5e8cd..5c7d26c52 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/BleRepository.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/BleRepository.kt @@ -57,7 +57,7 @@ enum class HandleState { * - isLegacyFormat = true * - Uses topCounter increments for rep counting (Beta 4 method) * - * OFFICIAL APP FORMAT (24 bytes): + * MODERN FIRMWARE FORMAT (24 bytes): * - topCounter (u32): Concentric/up phase completions * - completeCounter (u32): Eccentric/down phase completions * - rangeTop (float): Maximum ROM boundary @@ -148,7 +148,7 @@ interface BleRepository { // Heuristic/phase statistics from machine (for Echo mode force feedback) val heuristicData: StateFlow - // Machine diagnostics from the official diagnostic characteristic. + // Machine diagnostics from the diagnostic characteristic. val diagnostics: StateFlow suspend fun startScanning(): Result diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/ConnectionLogRepository.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/ConnectionLogRepository.kt index a81c4be96..1c8ea4661 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/ConnectionLogRepository.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/ConnectionLogRepository.kt @@ -157,7 +157,7 @@ class ConnectionLogRepository { */ fun exportAsText(): String { val sb = StringBuilder() - sb.appendLine("=== Vitruvian Connection Logs ===") + sb.appendLine("=== Connection Logs ===") sb.appendLine("Exported: ${formatTimestamp(currentTimeMillis())}") sb.appendLine("Total entries: ${_logs.value.size}") sb.appendLine() diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/KableBleRepository.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/KableBleRepository.kt index b3971b1bb..c4df6c73d 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/KableBleRepository.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/KableBleRepository.kt @@ -266,7 +266,7 @@ class KableBleRepository : BleRepository { override suspend fun sendStopCommand(): Result { log.i { "Sending stop command (polling continues)" } return try { - val stopPacket = BlePacketFactory.createOfficialStopPacket() + val stopPacket = BlePacketFactory.createSoftStopPacket() log.d { "Sending StopPacket (0x50)..." } sendWorkoutCommand(stopPacket) } catch (e: Exception) { @@ -400,7 +400,7 @@ class KableBleRepository : BleRepository { log.w { "REPS (LEGACY 6-byte format):" } log.w { " top=${notification.topCounter}, complete=${notification.completeCounter}" } } else { - log.i { "REPS (24-byte official format):" } + log.i { "REPS (24-byte modern format):" } log.i { " up=${notification.topCounter}, down=${notification.completeCounter}" } log.i { " repsRomCount=${notification.repsRomCount} (warmup done), repsRomTotal=${notification.repsRomTotal} (warmup target)" } log.i { " repsSetCount=${notification.repsSetCount} (working done), repsSetTotal=${notification.repsSetTotal} (working target)" } diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/Exercise.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/Exercise.kt index 1c8364408..e099f15d1 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/Exercise.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/Exercise.kt @@ -24,7 +24,7 @@ data class Exercise( val muscleGroup: String, val muscleGroups: String = muscleGroup, // Comma-separated list of primary muscle groups (defaults to muscleGroup for backward compatibility) val equipment: String = "", - val id: String? = null, // Optional exercise library ID for loading videos/thumbnails + val id: String? = null, // Optional exercise library ID (free-exercise-db slug, wger_, or custom_) used to load demo images val isFavorite: Boolean = false, // Whether exercise is marked as favorite val isCustom: Boolean = false, // Whether exercise was created by user val timesPerformed: Int = 0, // Number of times this exercise has been performed diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/Models.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/Models.kt index 7234326af..6fb081f5d 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/Models.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/Models.kt @@ -32,7 +32,7 @@ enum class WorkoutPhase { /** * Personal record for an exercise. * - * [weightPerCableKg] is the official-app display contract for PR load. [cableCount] + * [weightPerCableKg] is the per-cable display contract for PR load. [cableCount] * is retained for legacy metadata and analytics context; ordinary PR display must * not multiply by it. */ @@ -387,7 +387,7 @@ data class WorkoutMetric( val positionA: Float, // Position in mm (changed from Int in Issue #197) val positionB: Float, // Position in mm (changed from Int in Issue #197) val ticks: Long = 0L, - val velocityA: Double = 0.0, // Velocity for handle detection (official app protocol) + val velocityA: Double = 0.0, // Firmware-provided velocity used for handle detection val velocityB: Double = 0.0, // Velocity for right handle detection (for single-handle exercises) val status: Int = 0, // Machine status flags (0x8000=Deload Occurred, 0x0040=Deload Warn) ) { @@ -637,7 +637,7 @@ fun WorkoutSession.displayHeaviestKgPerCable(isBodyweight: Boolean = false): Flo /** * Legacy multiplier metadata for explicit total/compatibility paths. * - * Ordinary saved-session load display matches the official app and stays per-cable. + * Ordinary saved-session load display stays per-cable. * Do not use this helper to format primary selected/heaviest load values. */ fun WorkoutSession.displayLoadMultiplier(): Int = displayMultiplier ?: cableCount ?: 1 diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/UserPreferences.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/UserPreferences.kt index 547a18d6a..fbfea0cf4 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/UserPreferences.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/model/UserPreferences.kt @@ -10,7 +10,7 @@ data class UserPreferences( // Issue #167: autoplayEnabled removed - now derived from summaryCountdownSeconds // summaryCountdownSeconds == 0 (Unlimited) = autoplay OFF, != 0 = autoplay ON val stopAtTop: Boolean = false, // false = stop at bottom (extended), true = stop at top (contracted) - val enableVideoPlayback: Boolean = true, // true = show videos, false = hide videos to avoid slow loading + val enableVideoPlayback: Boolean = true, // true = show exercise demo images, false = hide them to avoid slow loading val beepsEnabled: Boolean = true, // true = play audio cues during workouts, false = haptic only val colorScheme: Int = 0, val discoModeUnlocked: Boolean = false, // Easter egg - unlocked by tapping LED header 7 times diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/usecase/RepCounterFromMachine.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/usecase/RepCounterFromMachine.kt index 8e5ba5278..e46824e89 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/usecase/RepCounterFromMachine.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/usecase/RepCounterFromMachine.kt @@ -9,7 +9,7 @@ import com.devil.phoenixproject.domain.model.RepType /** * Handles rep counting based on notifications emitted by the Vitruvian machine. * - * REP COUNTING APPROACH (Matches Official App): + * REP COUNTING APPROACH (trust the machine counters): * - warmupReps = repsRomCount (directly from machine) * - workingReps = down - repsRomCount (down counter minus warmup count) * diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/components/ConnectingOverlay.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/components/ConnectingOverlay.kt index ce9a4ab70..d22f78d58 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/components/ConnectingOverlay.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/components/ConnectingOverlay.kt @@ -51,7 +51,7 @@ fun ConnectingOverlay(onCancel: () -> Unit = {}) { style = MaterialTheme.typography.titleMedium, ) Text( - "Scanning for Vitruvian Trainer", + "Scanning for trainer", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/components/EnhancedCablePositionBar.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/components/EnhancedCablePositionBar.kt index 799cb5683..f146b532d 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/components/EnhancedCablePositionBar.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/components/EnhancedCablePositionBar.kt @@ -108,13 +108,13 @@ fun EnhancedCablePositionBar( ) // Position display normalization - ROM-relative scaling (matches parent repo) - // Per official app behavior observed in video: + // Display behaviour: // - During warmup, ROM is dynamically calibrated from actual movement // - Once ROM established, position is normalized relative to ROM range // - ROM markers appear at ~25% and ~90% of the bar, leaving headroom // - Going below ROM bottom triggers deload/spotter mode - // ROM padding: Based on official app screenshots, ROM boundaries appear at ~25% and ~90% + // ROM padding: ROM boundaries are drawn at 25% and 90% of the bar val romPaddingBottom = 0.25f // ROM bottom appears at 25% (leaves room for deload detection below) val romPaddingTop = 0.90f // ROM top appears at 90% (small headroom at top) val romDisplayRange = romPaddingTop - romPaddingBottom // 0.65 (65% of bar for ROM) @@ -130,7 +130,7 @@ fun EnhancedCablePositionBar( (romPaddingBottom + positionInRom * romDisplayRange).coerceIn(0f, 1f) } else { // ROM not yet established: use wide range for initial display - // Start with large range so initial movements appear small (like official app) + // Start with large range so initial movements appear small val wideRangeMax = 1000f // Full validation range (currentPosition / wideRangeMax).coerceIn(0f, 1f) }, diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/WorkoutCoordinator.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/WorkoutCoordinator.kt index 6858feac6..6b0419cf3 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/WorkoutCoordinator.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/WorkoutCoordinator.kt @@ -71,7 +71,7 @@ class WorkoutCoordinator( /** * Two-tier velocity hysteresis for stall detection (Issue #204, #216) - * Matches official app behavior to prevent timer toggling near threshold: + * Hysteresis prevents the stall timer toggling near the threshold: * - Below LOW (<2.5): start/continue stall timer (user is stopped) * - Above HIGH (>10): reset stall timer (user is clearly moving) * - Between LOW and HIGH (>=2.5 and <=10): maintain current state (hysteresis band) diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/navigation/NavGraph.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/navigation/NavGraph.kt index ddcba2107..19b2d6aa4 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/navigation/NavGraph.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/navigation/NavGraph.kt @@ -564,7 +564,7 @@ fun NavGraph( ) } - // Diagnostics screen - official-style machine diagnostics + // Diagnostics screen - machine diagnostics composable( route = NavigationRoutes.Diagnostics.route, enterTransition = { diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/JustLiftScreen.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/JustLiftScreen.kt index f7514b88b..3ecd2ea99 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/JustLiftScreen.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/JustLiftScreen.kt @@ -224,7 +224,7 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme } } - // Enable handle detection for auto-start when connected (matches official app) + // Enable handle detection for auto-start when connected val connectionState by viewModel.connectionState.collectAsState() // Single consolidated effect for handle detection (Issue: iOS autostart race condition fix) @@ -238,7 +238,7 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme } } - // Reset workout state if entering Just Lift with any non-Idle state (matches official app) + // Reset workout state if entering Just Lift with any non-Idle state LaunchedEffect(workoutState) { if (workoutState !is WorkoutState.Idle && workoutState !is WorkoutState.Active) { viewModel.prepareForJustLift() diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SetSummaryCard.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SetSummaryCard.kt index ce88d7053..d2a5e6da3 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SetSummaryCard.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SetSummaryCard.kt @@ -46,7 +46,7 @@ import vitruvianprojectphoenix.shared.generated.resources.* import vitruvianprojectphoenix.shared.generated.resources.Res /** - * Enhanced Set Summary Card - matches official Vitruvian app design + * Enhanced Set Summary Card * Shows detailed metrics: reps, volume, mode, peak/avg forces, duration, energy */ @Composable @@ -99,7 +99,7 @@ fun SetSummaryCard( } // Calculate display values. - // Official app primary load display is per-cable. Total volume is already a total value, + // Primary load display is per-cable. Total volume is already a total value, // so it only needs unit conversion. val displayReps = summary.repCount val totalVolumeDisplay = kgToDisplay(summary.totalVolumeKg, weightUnit) diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SplashScreen.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SplashScreen.kt index 2a6350cca..e12a928c3 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SplashScreen.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SplashScreen.kt @@ -43,7 +43,7 @@ private val DarkSlate = Color(0xFF0F172A) private val DeepNavy = Color(0xFF1E293B) /** - * Animated splash screen with the Vitruvian Phoenix logo. + * Animated splash screen with the Project Phoenix logo. * Features: * - Dramatic logo entrance with scale and bounce * - Animated fire glow behind the logo diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutHud.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutHud.kt index 0d562229c..130f1d16e 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutHud.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutHud.kt @@ -138,7 +138,7 @@ fun WorkoutHud( onUpdateParameters = onUpdateParameters, onNextExercise = onStartNextExercise, // Issue #125: Never show Next button during Active state - exercise navigation - // should only be allowed when the machine is not engaged. Official app behavior. + // should only be allowed when the machine is not engaged. showNextButton = false, isCurrentExerciseBodyweight = isCurrentExerciseBodyweight, rackLoadAdjustment = rackLoadAdjustment, @@ -685,7 +685,7 @@ private fun ExecutionPage( if (metric != null && !isCurrentExerciseBodyweight) { // Current Load - default to per-cable resistance, or show total only for // explicit unified-accessory exercises. - // Always use max(loadA, loadB) to show peak force (matches official app) + // Always use max(loadA, loadB) to show peak force (peak of both cables) // For Echo mode: use heuristic kgMax (actual measured force) // // The heuristic data provides actual measured force via the machine's diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutUiState.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutUiState.kt index 18d5dcbd3..3a9fc6492 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutUiState.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutUiState.kt @@ -33,7 +33,7 @@ import com.devil.phoenixproject.presentation.manager.RestTransitionPlan * @property autoStopState Auto-stop countdown state for Just Lift mode * @property autoStartCountdown Countdown seconds when auto-starting * @property weightUnit User's preferred weight display unit - * @property enableVideoPlayback Whether to show exercise demo videos + * @property enableVideoPlayback Whether to show exercise demonstration images * @property loadedRoutine Currently loaded routine (null for single exercise) * @property currentExerciseIndex Index in routine's exercise list * @property currentSetIndex Index of current set (0-based) within current exercise diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/util/WeightDisplayFormatter.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/util/WeightDisplayFormatter.kt index 27e4f3c61..5dd28aa1d 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/util/WeightDisplayFormatter.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/util/WeightDisplayFormatter.kt @@ -8,7 +8,7 @@ import com.devil.phoenixproject.util.format * Centralized weight display formatter. * * Phoenix stores, syncs, recommends, and commands machine load as per-cable kg. - * That matches the official app: cable count metadata must not change ordinary + * Cable count metadata must not change ordinary * selected-load display. Total two-cable text is available only through the * explicitly named total helper methods below. */ diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/viewmodel/DiagnosticsViewModel.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/viewmodel/DiagnosticsViewModel.kt index 0195aadce..fabcbd000 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/viewmodel/DiagnosticsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/viewmodel/DiagnosticsViewModel.kt @@ -68,7 +68,7 @@ fun buildDiagnosticsExportText( faults: List, exportedAtMillis: Long, ): String = buildString { - appendLine("=== Vitruvian Machine Diagnostics ===") + appendLine("=== Machine Diagnostics ===") appendLine("App version: ${Constants.APP_VERSION}") appendLine("Exported: ${formatTimestamp(exportedAtMillis)}") appendLine("Classification: REDACTED_DIAGNOSTICS") diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BleConstants.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BleConstants.kt index 251c9720c..750162c38 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BleConstants.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BleConstants.kt @@ -5,8 +5,8 @@ import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid /** - * BLE Constants - UUIDs and configuration values for Vitruvian device communication - * Nordic UART service UUIDs and Vitruvian protocol constants. + * BLE Constants - UUIDs and configuration values for trainer device communication + * Nordic UART service UUIDs and trainer protocol constants. */ @Suppress("unused") // Protocol reference constants - many are kept for documentation @OptIn(ExperimentalUuidApi::class) @@ -32,7 +32,7 @@ object BleConstants { const val DIAGNOSTIC_CHAR_UUID_STRING = "5fa538ec-d041-42f6-bbd6-c30d475387b7" // Variable const val PROPERTY_CHAR_UUID_STRING = DIAGNOSTIC_CHAR_UUID_STRING // Alias - // Unknown/Auth characteristic - present in web apps notification list + // Unknown/Auth characteristic - observed in the device's notification set // Purpose unclear but may be needed for proper device communication const val UNKNOWN_AUTH_CHAR_UUID_STRING = "36e6c2ee-21c7-404e-aa9b-f74ca4728ad4" @@ -43,17 +43,16 @@ object BleConstants { REPS_CHAR_UUID_STRING, HEURISTIC_CHAR_UUID_STRING, BLE_UPDATE_REQUEST_CHAR_UUID_STRING, - UNKNOWN_AUTH_CHAR_UUID_STRING, // Web apps subscribe to this + UNKNOWN_AUTH_CHAR_UUID_STRING, // Subscribed for parity with observed device behavior ) // Device name pattern for filtering - matches "Vitruvian*" devices const val DEVICE_NAME_PREFIX = "Vee" - const val DEVICE_NAME_PATTERN = "^Vitruvian.*$" // Command IDs (machine protocol) object Commands { const val STOP_COMMAND: Byte = 0x50 // Stop/halt - const val RESET_COMMAND: Byte = 0x0A // Reset/init (web app stop) - recovery fallback + const val RESET_COMMAND: Byte = 0x0A // Reset/init - accepted by the device as a recovery stop const val REGULAR_COMMAND: Byte = 0x4F // 25-byte packet (79 decimal) const val ECHO_COMMAND: Byte = 0x4E // 32-byte packet (78 decimal) const val ACTIVATION_COMMAND: Byte = 0x04 // 96-byte packet diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt index 1aa8c3430..c3bef52b3 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt @@ -7,8 +7,8 @@ import com.devil.phoenixproject.domain.model.WorkoutParameters import kotlin.concurrent.Volatile /** - * BLE Packet Factory - Builds binary protocol frames for Vitruvian device communication - * Ported from protocol.js and modes.js in the reference web application + * BLE Packet Factory - Builds the binary protocol frames the trainer firmware accepts. + * Byte layouts are documented inline and pinned by BlePacketFactoryTest. * * KMP-compatible version using manual byte manipulation (no java.nio.ByteBuffer) */ @@ -67,7 +67,7 @@ object BlePacketFactory { /** * Creates the legacy Phoenix START command (4 bytes). - * Official activation-mode starts do not send this after the configuration packet. + * Activation-mode starts do not need this after the CONFIG packet. */ fun createStartCommand(): ByteArray = byteArrayOf(0x03, 0x00, 0x00, 0x00) @@ -78,16 +78,16 @@ object BlePacketFactory { fun createStopCommand(): ByteArray = byteArrayOf(0x05, 0x00, 0x00, 0x00) /** - * Creates the Official App STOP_PACKET command (2 bytes). + * Creates the soft-stop command (0x50 0x00, 2 bytes). * Observed machine packet layout: * - Uses StopPacket (0x50 0x00) to end sessions and CLEAR FAULTS * - This is a "soft stop" that releases tension and clears the blinking red light fault state */ - fun createOfficialStopPacket(): ByteArray = byteArrayOf(0x50, 0x00) + fun createSoftStopPacket(): ByteArray = byteArrayOf(0x50, 0x00) /** * Creates the RESET command (4 bytes). - * This is what web apps use for stop (0x0A) - same as init command. + * The device accepts 0x0A as reset/init; usable as a recovery stop. * Use for recovery if device gets stuck. */ fun createResetCommand(): ByteArray = byteArrayOf(0x0A, 0x00, 0x00, 0x00) @@ -123,7 +123,7 @@ object BlePacketFactory { * separate: 0x48-0x4F remains the mode profile's eccentric-up ramp, while * selected force/progression live at 0x58/0x5C. * - * The default [ForceConfigVariant.NON_OVERLAP] preserves that official layout. + * The default [ForceConfigVariant.NON_OVERLAP] preserves that firmware layout. * [ForceConfigVariant.OVERLAP] is retained only to reproduce the legacy Phoenix * behavior that overwrote 0x48/0x4C after copying the profile. */ @@ -196,7 +196,7 @@ object BlePacketFactory { frame[0x2a] = 0x1E frame[0x2b] = 0x00 - // Eccentric-specific RepConfig override (eccentric mode profile) + // Eccentric-specific rep-config override (eccentric mode profile) // Eccentric mode uses bottom.inner.mmPerM = 50 (vs default 250) for // more sensitive bottom-of-rep detection during eccentric-focused training. if (params.programMode is ProgramMode.EccentricOnly) { @@ -212,13 +212,13 @@ object BlePacketFactory { val profile = getActivationPhases(profileMode) profile.copyInto(frame, 0x30) - // Official activation force config keeps the selected force separate + // The activation force config block keeps the selected force separate // from per-rep progression. The increment field controls progression; // targetWeight and forceMax stay anchored to the selected force. val targetWeightPerCable = params.weightPerCableKg val effectiveKg = targetWeightPerCable + 10.0f - // Official normal force modes keep softMax tied to the selected force + // Normal force modes keep softMax tied to the selected force // per cable. Unlimited-rep behavior is controlled by the reps field // (0xFF), not by raising softMax to the machine maximum. val softMax = params.weightPerCableKg @@ -277,7 +277,7 @@ object BlePacketFactory { } } else { Logger.d("BlePacket") { - "official non-overlap layout active: " + + "non-overlap layout active: " + "ecc.up.minMmS[0x48]=${readShortLE(frame, BleConstants.ActivationPacket.OFFSET_ECC_UP_MIN_MMS)}, " + "ecc.up.maxMmS[0x4A]=${readShortLE(frame, BleConstants.ActivationPacket.OFFSET_ECC_UP_MAX_MMS)}, " + "ecc.up.ramp[0x4C]=${readFloatLE(frame, BleConstants.ActivationPacket.OFFSET_ECC_UP_RAMP)}" diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/ColorScheme.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/ColorScheme.kt index 6309c156b..dbb765b1e 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/ColorScheme.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/ColorScheme.kt @@ -81,7 +81,7 @@ object ColorSchemes { /** * "None" turns off the LED lights by sending black (0,0,0) colors. - * This matches the official app's implementation. + * Sending black to all three slots turns the LEDs off. */ val NONE = ColorScheme( name = "None", diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/Constants.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/Constants.kt index ca45d9b07..8c7eeeaca 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/Constants.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/Constants.kt @@ -5,7 +5,7 @@ package com.devil.phoenixproject.util */ object Constants { // App version - const val APP_VERSION = "0.9.6" + const val APP_VERSION = "1.0.0" // EULA version - increment when EULA text changes materially // Users must re-accept when this version increases @@ -132,7 +132,7 @@ object OneRepMaxCalculator { /** * Protocol constants for machine BLE frames. - * NOTE: Legacy web app used different sizes and commands + * NOTE: older protocol revisions used different frame sizes and commands */ @Suppress("unused") // Protocol reference constants object ProtocolConstants { @@ -140,7 +140,7 @@ object ProtocolConstants { // Frame sizes const val STOP_PACKET_SIZE = 2 - const val REGULAR_PACKET_SIZE = 25 // Was 96 in web app + const val REGULAR_PACKET_SIZE = 25 const val ECHO_PACKET_SIZE = 32 // F308: matches BlePacketFactory.createEchoControl() const val ACTIVATION_PACKET_SIZE = 96 // F308: matches BlePacketFactory.createProgramParams()/ActivationPacket.SIZE const val COLOR_SCHEME_SIZE = 34 diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt index aa0fd0604..ecbc5e818 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt @@ -2506,7 +2506,7 @@ abstract class BaseDataBackupManager( } /** - * Generic placeholder routine names set by external imports (e.g. Vitruvian cloud). + * Generic placeholder routine names set by external imports (e.g. legacy cloud exports). * These don't identify a real routine and should be treated as null/unknown. */ private val GARBAGE_ROUTINE_NAMES = setOf( diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/HardwareDetection.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/HardwareDetection.kt index 12e8f49c4..3aa9b88cb 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/HardwareDetection.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/util/HardwareDetection.kt @@ -3,9 +3,9 @@ package com.devil.phoenixproject.util import com.devil.phoenixproject.domain.model.VitruvianModel /** - * Vitruvian Hardware Detection + * Trainer Hardware Detection * - * Previously attempted to identify hardware models (Euclid, Trainer+) from device name prefixes, + * Previously attempted to identify hardware models (V-Form, Trainer+) from device name prefixes, * but this approach was flawed - device name patterns don't reliably indicate hardware capabilities. * * Current approach: Report only what we can actually detect (device name) and avoid making @@ -31,7 +31,7 @@ object HardwareDetection { /** * Get device display info without making capability assumptions */ - fun getDeviceDisplayInfo(deviceName: String): String = "Vitruvian Trainer ($deviceName)" + fun getDeviceDisplayInfo(deviceName: String): String = "Trainer ($deviceName)" /** * Get hardware capabilities - currently returns defaults since we can't @@ -44,7 +44,7 @@ object HardwareDetection { } /** - * Hardware capabilities for Vitruvian trainers + * Hardware capabilities for supported trainers * * Note: Without firmware version detection, we assume all features are available. * This is safer than incorrectly disabling features based on flawed model detection. diff --git a/shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/migrations/39.sqm b/shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/migrations/39.sqm index d26ef170b..746399328 100644 --- a/shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/migrations/39.sqm +++ b/shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/migrations/39.sqm @@ -8,14 +8,16 @@ ALTER TABLE Exercise ADD COLUMN isBodyweight INTEGER; ALTER TABLE RoutineExercise ADD COLUMN isBodyweight INTEGER; --- Catalog fix: cable lifts that shipped with an empty equipment list. -UPDATE Exercise SET isBodyweight = 0 WHERE id IN ( - 'UjIGHxCav-lS9B2I', -- Squat - 'enuJ_FgAzXDLAweK', -- Good Morning - 'KoL_gx00nuf2wncV', -- Medial Delt Twist - 'kSLyRg4bjLuzTeIM', -- Medial Delt Twist (duplicate catalog entry) - '2nTn2QR6MyezFYmK', -- Kneeling 45 Degree Kickback - 'fAglxv8VMaisUTyo' -- Just Lift exercise +-- Catalog fix: bundled cable lifts that shipped with an empty equipment list. +-- Matched by catalogue name on non-custom rows; TRIM handles a trailing-space +-- variant of 'Kneeling 45 Degree Kickback' present in older installs. +UPDATE Exercise SET isBodyweight = 0 +WHERE isCustom = 0 AND TRIM(name) IN ( + 'Squat', + 'Good Morning', + 'Medial Delt Twist', + 'Kneeling 45 Degree Kickback', + 'Just Lift exercise' ); -- Convert the legacy pull-sync sentinel to the new column and clear the fake @@ -24,18 +26,9 @@ UPDATE RoutineExercise SET isBodyweight = 1, exerciseEquipment = '' WHERE exerciseEquipment = 'Bodyweight'; -- Force-correct routine exercises referencing the misclassified cable lifts. -UPDATE RoutineExercise SET isBodyweight = 0 WHERE exerciseId IN ( - 'UjIGHxCav-lS9B2I', - 'enuJ_FgAzXDLAweK', - 'KoL_gx00nuf2wncV', - 'kSLyRg4bjLuzTeIM', - '2nTn2QR6MyezFYmK', - 'fAglxv8VMaisUTyo' -); - -- Legacy rows can carry a NULL or stale exerciseId (the loader heals them by --- name at load time), so also match the known cable lifts by snapshot name. --- TRIM handles the catalog's trailing-space 'Kneeling 45 Degree Kickback '. +-- name at load time), so match the known cable lifts by snapshot name. +-- TRIM handles a trailing-space variant of 'Kneeling 45 Degree Kickback'. -- The IS NULL guard preserves explicit flags set above (e.g. a portal -- bodyweight toggle converted from the sentinel). UPDATE RoutineExercise SET isBodyweight = 0 diff --git a/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoderTest.kt b/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoderTest.kt index 2e9dc7585..306becc9a 100644 --- a/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoderTest.kt +++ b/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoderTest.kt @@ -8,53 +8,53 @@ import kotlin.test.assertTrue class DiagnosticFaultDecoderTest { @Test - fun `decodes official Vitruvian fault labels`() { + fun `decodes controller fault labels`() { val expected = mapOf( 0 to "None", - 1 to "No comms", - 2 to "Init failure", - 4 to "TI restarted", - 8 to "Message failure (RX)", - 16 to "Message failure (TX)", + 1 to "Communication lost", + 2 to "Initialisation failure", + 4 to "Controller restarted", + 8 to "RX message failure", + 16 to "TX message failure", 32 to "Firmware update failure", - 64 to "Overtemp failure", + 64 to "Over-temperature", ) expected.forEach { (code, label) -> - val decoded = decodeDiagnosticFault(DiagnosticFaultCategory.VITRUVIAN, code) + val decoded = decodeDiagnosticFault(DiagnosticFaultCategory.CONTROLLER, code) assertEquals(label, decoded.label, "code=$code") assertEquals(formatDiagnosticFaultCode(code), decoded.rawHex) } } @Test - fun `decodes combined Vitruvian fault bit flags`() { - val decoded = decodeDiagnosticFault(DiagnosticFaultCategory.VITRUVIAN, 0x0043) - val duplicateMessageFailureBits = decodeDiagnosticFault(DiagnosticFaultCategory.VITRUVIAN, 0x0018) + fun `decodes combined controller fault bit flags`() { + val decoded = decodeDiagnosticFault(DiagnosticFaultCategory.CONTROLLER, 0x0043) + val duplicateMessageFailureBits = decodeDiagnosticFault(DiagnosticFaultCategory.CONTROLLER, 0x0018) - assertEquals("No comms, Init failure, Overtemp failure", decoded.label) + assertEquals("Communication lost, Initialisation failure, Over-temperature", decoded.label) assertEquals("0x0043", decoded.rawHex) assertTrue(decoded.hasFault) - assertEquals("Message failure (RX), Message failure (TX)", duplicateMessageFailureBits.label) + assertEquals("RX message failure, TX message failure", duplicateMessageFailureBits.label) } @Test - fun `decodes official motor fault labels`() { + fun `decodes motor fault labels`() { val expected = mapOf( 0 to "None", - 1 to "HW Overcurrent", - 2 to "SW Overcurrent", - 4 to "Over voltage", - 8 to "Under voltage", - 16 to "PIM temp", - 32 to "Gate driver", - 64 to "Bord Temp", + 1 to "Hardware overcurrent", + 2 to "Software overcurrent", + 4 to "Overvoltage", + 8 to "Undervoltage", + 16 to "Power module temperature", + 32 to "Gate driver fault", + 64 to "Board temperature", 128 to "Kill switch", - 256 to "Alignment", - 512 to "Encoder", - 1024 to "HW/FW mismatch", - 2048 to "EEPROM", - 4096 to "Motor overtemp", + 256 to "Alignment fault", + 512 to "Encoder fault", + 1024 to "Hardware/firmware mismatch", + 2048 to "EEPROM fault", + 4096 to "Motor over-temperature", ) expected.forEach { (code, label) -> @@ -68,13 +68,13 @@ class DiagnosticFaultDecoderTest { fun `decodes combined motor fault bit flags`() { val decoded = decodeDiagnosticFault(DiagnosticFaultCategory.MOTOR_A, 0x1005) - assertEquals("HW Overcurrent, Over voltage, Motor overtemp", decoded.label) + assertEquals("Hardware overcurrent, Overvoltage, Motor over-temperature", decoded.label) assertEquals("0x1005", decoded.rawHex) assertTrue(decoded.hasFault) } @Test - fun `decodes official other fault labels`() { + fun `decodes other fault labels`() { val none = decodeDiagnosticFault(DiagnosticFaultCategory.OTHER, 0) val other = decodeDiagnosticFault(DiagnosticFaultCategory.OTHER, 7) @@ -85,7 +85,7 @@ class DiagnosticFaultDecoderTest { } @Test - fun `decodeDiagnosticFaults assigns official categories by word index`() { + fun `decodeDiagnosticFaults assigns categories by word index`() { val packet = DiagnosticPacket( runtimeSeconds = 1L, faultWords = listOf(4, 7, 4, 64), @@ -95,13 +95,13 @@ class DiagnosticFaultDecoderTest { val faults = decodeDiagnosticFaults(packet) - assertEquals(DiagnosticFaultCategory.VITRUVIAN, faults[0].category) - assertEquals("TI restarted", faults[0].label) + assertEquals(DiagnosticFaultCategory.CONTROLLER, faults[0].category) + assertEquals("Controller restarted", faults[0].label) assertEquals(DiagnosticFaultCategory.OTHER, faults[1].category) assertEquals("Other", faults[1].label) assertEquals(DiagnosticFaultCategory.MOTOR_A, faults[2].category) - assertEquals("Over voltage", faults[2].label) + assertEquals("Overvoltage", faults[2].label) assertEquals(DiagnosticFaultCategory.MOTOR_B, faults[3].category) - assertEquals("Bord Temp", faults[3].label) + assertEquals("Board temperature", faults[3].label) } } diff --git a/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/HardwareValidationTest.kt b/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/HardwareValidationTest.kt deleted file mode 100644 index f48f4cf6c..000000000 --- a/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/HardwareValidationTest.kt +++ /dev/null @@ -1,444 +0,0 @@ -package com.devil.phoenixproject.data.ble - -import kotlin.math.abs -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -/** - * Hardware Validation Test Suite - * - * Captures and validates the 3 BLE protocol discrepancies identified in the - * comparison review (Phase 1) that require real hardware verification: - * - * 1a. Packet Layout: Official (28-byte) vs Phoenix/Parent (18-byte) interpretation - * 1b. Force Units: Percentage (0-100%) vs kg (raw/100) - * 1c. Velocity Source: Firmware-provided vs client-calculated EMA - * - * HOW TO USE: - * 1. Enable raw packet logging by calling BlePacketCapture.enable() in a debug build - * 2. Connect to trainer and perform a few reps at a KNOWN weight (e.g., 50kg) - * 3. Copy captured hex strings into the CAPTURED_PACKETS list below - * 4. Run this test suite: ./gradlew :shared:testDebugUnitTest --tests "*.HardwareValidationTest" - * 5. Review output to determine which interpretation matches reality - * - * See also: BlePacketCapture.kt for the runtime capture utility. - */ -class HardwareValidationTest { - - // ========================================================================= - // PASTE CAPTURED PACKETS HERE - // ========================================================================= - // Each entry: hex string from BlePacketCapture log, known weight in kg, description - // Example: CapturedSample("0A001400E803...1200", 50.0f, "50kg static hold mid-cable") - // - // Capture at least: - // - 3-5 packets during a STATIC HOLD at a known weight (validates force/load units) - // - 5-10 packets during SLOW MOVEMENT (validates velocity field presence) - // - 3-5 packets at REST with cable retracted (validates zero/idle state) - - data class CapturedSample( - val hex: String, - val knownWeightKg: Float, - val description: String, - ) - - private val CAPTURED_PACKETS: List = listOf( - // TODO: Replace with real captured packets from BlePacketCapture - // CapturedSample("0A001400E803...", 50.0f, "50kg static hold"), - ) - - // ========================================================================= - // INTERPRETATION A: Official App (28-byte Sample format) - // ========================================================================= - // [posA_s16/10, velA_s16/10, forceA_s16/100, posB_s16/10, velB_s16/10, forceB_s16/100, time_i32, status_s16] - // 6 shorts (12 bytes) + 1 int (4 bytes) + 1 short (2 bytes) = 18 bytes minimum - - data class OfficialInterpretation( - val posA: Float, // mm (short / 10.0) - val velA: Float, // mm/s (short / 10.0) - val forceA: Float, // percentage 0-100 (short / 100.0) - val posB: Float, // mm (short / 10.0) - val velB: Float, // mm/s (short / 10.0) - val forceB: Float, // percentage 0-100 (short / 100.0) - val timeMs: Int, // firmware timestamp (4 bytes) - val status: Int?, // status flags (2 bytes, optional) - ) - - private fun parseAsOfficial(data: ByteArray): OfficialInterpretation? { - if (data.size < 12) return null // Minimum: 6 shorts - return OfficialInterpretation( - posA = getInt16LE(data, 0) / 10.0f, - velA = getInt16LE(data, 2) / 10.0f, - forceA = getInt16LE(data, 4) / 100.0f, - posB = getInt16LE(data, 6) / 10.0f, - velB = getInt16LE(data, 8) / 10.0f, - forceB = getInt16LE(data, 10) / 100.0f, - timeMs = if (data.size >= 16) getInt32LE(data, 12) else 0, - status = if (data.size >= 18) getUInt16LE(data, 16) else null, - ) - } - - // ========================================================================= - // INTERPRETATION B: Phoenix/Parent (18-byte Monitor format) - // ========================================================================= - // [ticksLo_u16, ticksHi_u16, posA_s16/10, (skip 2), loadA_u16/100, posB_s16/10, (skip 2), loadB_u16/100, status_u16] - // This is what parseMonitorPacket() currently does. - - data class PhoenixInterpretation( - val ticksLow: Int, // unsigned 16-bit - val ticksHigh: Int, // unsigned 16-bit - val ticks: Long, // combined 32-bit tick counter - val posA: Float, // mm (signed short / 10.0) - val skippedA: Int, // bytes 6-7 (velocity in official interpretation) - val loadA: Float, // kg (unsigned short / 100.0) - val posB: Float, // mm (signed short / 10.0) - val skippedB: Int, // bytes 12-13 (velocity in official interpretation) - val loadB: Float, // kg (unsigned short / 100.0) - val status: Int?, // status flags (optional) - ) - - private fun parseAsPhoenix(data: ByteArray): PhoenixInterpretation? { - if (data.size < 16) return null - val ticksLow = getUInt16LE(data, 0) - val ticksHigh = getUInt16LE(data, 2) - return PhoenixInterpretation( - ticksLow = ticksLow, - ticksHigh = ticksHigh, - ticks = ticksLow.toLong() or (ticksHigh.toLong() shl 16), - posA = getInt16LE(data, 4) / 10.0f, - skippedA = getInt16LE(data, 6), // What Phoenix skips (potential velA) - loadA = getUInt16LE(data, 8) / 100.0f, - posB = getInt16LE(data, 10) / 10.0f, - skippedB = getInt16LE(data, 12), // What Phoenix skips (potential velB) - loadB = getUInt16LE(data, 14) / 100.0f, - status = if (data.size >= 18) getUInt16LE(data, 16) else null, - ) - } - - // ========================================================================= - // Helper: hex string to ByteArray - // ========================================================================= - - private fun hexToBytes(hex: String): ByteArray { - val clean = hex.replace(" ", "").replace(":", "") - require(clean.length % 2 == 0) { "Hex string must have even length" } - return ByteArray(clean.length / 2) { i -> - clean.substring(i * 2, i * 2 + 2).toInt(16).toByte() - } - } - - private fun ByteArray.toHexDump(): String = joinToString(" ") { - (it.toInt() and 0xFF).toString(16).padStart(2, '0').uppercase() - } - - // ========================================================================= - // TEST: Dual Interpretation of Captured Packets - // ========================================================================= - - @Test - fun `dual interpretation comparison - populate CAPTURED_PACKETS to run`() { - if (CAPTURED_PACKETS.isEmpty()) { - println( - """ - |======================================================== - | HARDWARE VALIDATION: No captured packets yet - |======================================================== - | To use this test: - | 1. Add BlePacketCapture.startCapture() to your debug build - | 2. Connect to trainer, perform reps at known weight - | 3. Copy hex strings into CAPTURED_PACKETS list above - | 4. Re-run this test - |======================================================== - """.trimMargin(), - ) - return - } - - println("=".repeat(80)) - println("HARDWARE VALIDATION: Dual Packet Interpretation") - println("=".repeat(80)) - - for ((index, sample) in CAPTURED_PACKETS.withIndex()) { - val data = hexToBytes(sample.hex) - val official = parseAsOfficial(data) - val phoenix = parseAsPhoenix(data) - - println("\n--- Packet #$index: ${sample.description} (${data.size} bytes) ---") - println("Raw: ${data.toHexDump()}") - println("Known weight: ${sample.knownWeightKg} kg") - - if (official != null) { - println("\n OFFICIAL interpretation (28-byte Sample):") - println(" posA=${official.posA}mm velA=${official.velA}mm/s forceA=${official.forceA}%") - println(" posB=${official.posB}mm velB=${official.velB}mm/s forceB=${official.forceB}%") - println(" time=${official.timeMs}ms status=${official.status?.let { "0x${it.toString(16).padStart(4, '0').uppercase()}" }}") - - // Force-as-percentage check: does forceA map to known weight? - val forceAsKgFromPercent = official.forceA / 100.0f * 200.0f // Assuming 200kg max V-Form - println(" -> If force is %: ${official.forceA}% of 200kg = ${forceAsKgFromPercent}kg (expected ~${sample.knownWeightKg}kg)") - } - - if (phoenix != null) { - println("\n PHOENIX interpretation (18-byte Monitor):") - println(" ticks=${phoenix.ticks} (lo=${phoenix.ticksLow} hi=${phoenix.ticksHigh})") - println(" posA=${phoenix.posA}mm loadA=${phoenix.loadA}kg") - println(" posB=${phoenix.posB}mm loadB=${phoenix.loadB}kg") - println(" skippedA=${phoenix.skippedA} (raw) skippedB=${phoenix.skippedB} (raw)") - println(" status=${phoenix.status?.let { "0x${it.toString(16).padStart(4, '0').uppercase()}" }}") - - // Load-as-kg check: does loadA match known weight? - println(" -> If load is kg: ${phoenix.loadA}kg (expected ~${sample.knownWeightKg}kg)") - - // What if skipped bytes are velocity? - val velFromSkippedA = phoenix.skippedA / 10.0f - val velFromSkippedB = phoenix.skippedB / 10.0f - println(" -> If skipped bytes are velocity: velA=${velFromSkippedA}mm/s velB=${velFromSkippedB}mm/s") - } - - println("\n VERDICT CHECKLIST:") - println(" [ ] Does official.forceA (${official?.forceA}%) make sense as percentage?") - println(" [ ] Does phoenix.loadA (${phoenix?.loadA}kg) match known weight (${sample.knownWeightKg}kg)?") - println(" [ ] Does official.posA (${official?.posA}mm) match phoenix.posA (${phoenix?.posA}mm)?") - println(" [ ] Are skipped bytes non-zero during movement? (velA=${phoenix?.skippedA}, velB=${phoenix?.skippedB})") - println(" [ ] Does official.timeMs (${official?.timeMs}) look like a timestamp?") - println(" [ ] Do phoenix.ticks (${phoenix?.ticks}) look like a counter?") - } - - println("\n" + "=".repeat(80)) - println("Review output above. The interpretation where force/load matches") - println("the known weight is correct. Non-zero skipped bytes during movement") - println("confirm firmware velocity is present.") - println("=".repeat(80)) - } - - // ========================================================================= - // TEST 1a: Packet Layout - Position field alignment - // ========================================================================= - - @Test - fun `1a - packet layout - positions should be plausible in at least one interpretation`() { - if (CAPTURED_PACKETS.isEmpty()) return - - for (sample in CAPTURED_PACKETS) { - val data = hexToBytes(sample.hex) - val official = parseAsOfficial(data)!! - val phoenix = parseAsPhoenix(data)!! - - // In at least one interpretation, positions should be in valid range (-1000 to 1000 mm) - val officialPosValid = official.posA in -1000f..1000f && official.posB in -1000f..1000f - val phoenixPosValid = phoenix.posA in -1000f..1000f && phoenix.posB in -1000f..1000f - - assertTrue( - officialPosValid || phoenixPosValid, - "Packet '${sample.description}': Neither interpretation has plausible positions. " + - "Official: posA=${official.posA}, posB=${official.posB}; " + - "Phoenix: posA=${phoenix.posA}, posB=${phoenix.posB}", - ) - - // Note: In OFFICIAL layout, bytes 0-1 are posA. In PHOENIX, bytes 0-1 are ticksLow. - // If official.posA is always a small number AND phoenix.ticksLow is incrementing, - // that's evidence FOR the Phoenix interpretation (it's a counter, not position). - } - } - - // ========================================================================= - // TEST 1b: Force Units - Compare against known weight - // ========================================================================= - - @Test - fun `1b - force units - determine if field represents percentage or kg`() { - val staticHolds = CAPTURED_PACKETS.filter { it.knownWeightKg > 0 } - if (staticHolds.isEmpty()) { - println("No static hold packets with known weight. Capture packets while holding a known weight.") - return - } - - var percentageMatchCount = 0 - var kgMatchCount = 0 - val tolerance = 0.15f // 15% tolerance for BLE noise - - for (sample in staticHolds) { - val data = hexToBytes(sample.hex) - val official = parseAsOfficial(data)!! - val phoenix = parseAsPhoenix(data)!! - - // Hypothesis A: Official is correct, field is percentage - // forceA% of 200kg max should approximate known weight - val vFormMaxKg = 200.0f - val trainerPlusMaxKg = 220.0f - val kgFromPercent200 = official.forceA / 100.0f * vFormMaxKg - val kgFromPercent220 = official.forceA / 100.0f * trainerPlusMaxKg - - val percentMatch200 = abs(kgFromPercent200 - sample.knownWeightKg) / sample.knownWeightKg < tolerance - val percentMatch220 = abs(kgFromPercent220 - sample.knownWeightKg) / sample.knownWeightKg < tolerance - - // Hypothesis B: Phoenix is correct, field is kg directly - val kgMatch = abs(phoenix.loadA - sample.knownWeightKg) / sample.knownWeightKg < tolerance - - if (percentMatch200 || percentMatch220) percentageMatchCount++ - if (kgMatch) kgMatchCount++ - - println("Sample '${sample.description}':") - println(" Official forceA=${official.forceA}% -> ${kgFromPercent200}kg (200max) or ${kgFromPercent220}kg (220max)") - println(" Phoenix loadA=${phoenix.loadA}kg") - println(" Known weight: ${sample.knownWeightKg}kg") - println(" Percentage match: ${percentMatch200 || percentMatch220}, Kg match: $kgMatch") - } - - println("\nSUMMARY: Percentage interpretation matched $percentageMatchCount/${staticHolds.size}") - println(" Kg interpretation matched $kgMatchCount/${staticHolds.size}") - - if (kgMatchCount > percentageMatchCount) { - println("RESULT: Force field appears to be KG (Phoenix interpretation correct)") - } else if (percentageMatchCount > kgMatchCount) { - println("RESULT: Force field appears to be PERCENTAGE (Official interpretation correct)") - } else { - println("RESULT: INCONCLUSIVE - need more samples at different weights") - } - } - - // ========================================================================= - // TEST 1c: Velocity Source - Check if skipped bytes contain velocity - // ========================================================================= - - @Test - fun `1c - velocity source - skipped bytes should be non-zero during movement`() { - if (CAPTURED_PACKETS.isEmpty()) return - - var nonZeroSkippedCount = 0 - var movementSampleCount = 0 - - for (sample in CAPTURED_PACKETS) { - val data = hexToBytes(sample.hex) - val phoenix = parseAsPhoenix(data)!! - - // During movement, if bytes 6-7 and 12-13 contain firmware velocity, - // they should be non-zero - val hasSkippedData = phoenix.skippedA != 0 || phoenix.skippedB != 0 - if (hasSkippedData) nonZeroSkippedCount++ - - // Try to identify movement packets (crude: non-zero position) - if (abs(phoenix.posA) > 5.0f || abs(phoenix.posB) > 5.0f) { - movementSampleCount++ - } - - if (hasSkippedData) { - val velA = phoenix.skippedA / 10.0f - val velB = phoenix.skippedB / 10.0f - println("Packet '${sample.description}': skipped bytes -> velA=${velA}mm/s, velB=${velB}mm/s") - // Sanity: velocity should be reasonable (-500 to +500 mm/s for cable exercises) - assertTrue( - abs(velA) < 500f && abs(velB) < 500f, - "Implausible velocity from skipped bytes: velA=$velA, velB=$velB", - ) - } - } - - println("\nVelocity source summary:") - println(" Packets with non-zero skipped bytes: $nonZeroSkippedCount/${CAPTURED_PACKETS.size}") - println(" Movement packets (pos > 5mm): $movementSampleCount/${CAPTURED_PACKETS.size}") - - if (nonZeroSkippedCount > 0) { - println("RESULT: Skipped bytes contain data - likely firmware velocity") - println(" Recommend reading firmware velocity instead of client-side EMA") - } else if (movementSampleCount > 0) { - println("RESULT: Movement detected but skipped bytes are zero - firmware may not provide velocity") - } else { - println("RESULT: No movement packets captured - re-test with cable movement") - } - } - - // ========================================================================= - // TEST: Tick Counter vs Position in bytes 0-3 - // ========================================================================= - - @Test - fun `1a - supplemental - bytes 0-3 should be counter OR position but not both`() { - if (CAPTURED_PACKETS.size < 3) { - println("Need 3+ sequential packets to test counter behavior. Skipping.") - return - } - - // If bytes 0-3 are a tick counter (Phoenix), they should INCREASE monotonically - // If bytes 0-1 are posA (Official), they should correlate with cable position - val ticks = CAPTURED_PACKETS.map { parseAsPhoenix(hexToBytes(it.hex))!!.ticks } - val officialPositions = CAPTURED_PACKETS.map { parseAsOfficial(hexToBytes(it.hex))!!.posA } - - // Check if ticks are monotonically increasing - var ticksMonotonic = true - for (i in 1 until ticks.size) { - if (ticks[i] <= ticks[i - 1]) { - ticksMonotonic = false - break - } - } - - // Check if official positions stay in a plausible range - val positionsPlausible = officialPositions.all { it in -1000f..1000f } - - println("Tick counter test (bytes 0-3):") - println(" Values: ${ticks.take(5)}") - println(" Monotonically increasing: $ticksMonotonic") - println("\nPosition test (bytes 0-1 as posA):") - println(" Values: ${officialPositions.take(5)}") - println(" All in plausible range: $positionsPlausible") - - if (ticksMonotonic && !positionsPlausible) { - println("RESULT: Bytes 0-3 are a tick counter (Phoenix interpretation)") - } else if (positionsPlausible && !ticksMonotonic) { - println("RESULT: Bytes 0-1 are position (Official interpretation)") - } else { - println("RESULT: AMBIGUOUS - both interpretations plausible. Need more diverse packets.") - } - } - - // ========================================================================= - // TEST: Validate parseMonitorPacket() against known good packet - // ========================================================================= - - @Test - fun `existing parseMonitorPacket produces valid output for synthetic packet`() { - // Synthetic 18-byte packet with known values - // Ticks: 1000 (0xE803) lo=0x03E8, hi=0x0000 - // PosA: 150.0mm -> raw 1500 = 0xDC05 - // Gap: 0x0000 - // LoadA: 50.00kg -> raw 5000 = 0x8813 - // PosB: 148.0mm -> raw 1480 = 0xC805 - // Gap: 0x0000 - // LoadB: 50.00kg -> raw 5000 = 0x8813 - // Status: 0x0000 - val data = byteArrayOf( - 0xE8.toByte(), 0x03, // ticksLo = 1000 - 0x00, 0x00, // ticksHi = 0 - 0xDC.toByte(), 0x05, // posA raw = 1500 -> 150.0mm - 0x00, 0x00, // gap (or velA in official) - 0x88.toByte(), 0x13, // loadA raw = 5000 -> 50.0kg - 0xC8.toByte(), 0x05, // posB raw = 1480 -> 148.0mm - 0x00, 0x00, // gap (or velB in official) - 0x88.toByte(), 0x13, // loadB raw = 5000 -> 50.0kg - 0x00, 0x00, // status = 0 - ) - - val packet = parseMonitorPacket(data) - assertNotNull(packet, "parseMonitorPacket should handle 18-byte packet") - assertEquals(1000L, packet.ticks) - assertEquals(150.0f, packet.posA, 0.1f) - assertEquals(50.0f, packet.loadA, 0.01f) - assertEquals(148.0f, packet.posB, 0.1f) - assertEquals(50.0f, packet.loadB, 0.01f) - assertEquals(0, packet.status) - } - - @Test - fun `parseMonitorPacket rejects packets shorter than 16 bytes`() { - val tooShort = ByteArray(15) - val result = parseMonitorPacket(tooShort) - assertNull(result) - } - - private fun assertNull(value: Any?) { - assertEquals(null, value) - } -} diff --git a/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/ProtocolParserTest.kt b/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/ProtocolParserTest.kt index 064113ac0..4c86552f1 100644 --- a/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/ProtocolParserTest.kt +++ b/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/ProtocolParserTest.kt @@ -354,7 +354,7 @@ class ProtocolParserTest { @Test fun `parseDiagnosticPacket returns null for short data`() { - val data = ByteArray(17) // Need 18 bytes minimum for non-empty official payloads + val data = ByteArray(17) // Need 18 bytes minimum for non-empty diagnostic payloads assertNull(parseDiagnosticPacket(data)) } @@ -535,12 +535,12 @@ class ProtocolParserTest { @Test fun `decodeDiagnosticFault maps 0x0004 by category`() { - val vitruvian = decodeDiagnosticFault(DiagnosticFaultCategory.VITRUVIAN, 0x0004) + val controller = decodeDiagnosticFault(DiagnosticFaultCategory.CONTROLLER, 0x0004) val motor = decodeDiagnosticFault(DiagnosticFaultCategory.MOTOR_A, 0x0004) - assertEquals("TI restarted", vitruvian.label) - assertEquals("Over voltage", motor.label) - assertEquals("0x0004", vitruvian.rawHex) + assertEquals("Controller restarted", controller.label) + assertEquals("Overvoltage", motor.label) + assertEquals("0x0004", controller.rawHex) assertEquals("0x0004", motor.rawHex) } diff --git a/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/integration/HealthDataMappingTest.kt b/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/integration/HealthDataMappingTest.kt index fb2abc309..baf435327 100644 --- a/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/integration/HealthDataMappingTest.kt +++ b/shared/src/commonTest/kotlin/com/devil/phoenixproject/data/integration/HealthDataMappingTest.kt @@ -77,7 +77,7 @@ class HealthDataMappingTest { @Test fun titleWeightDefaultsToSingleWhenCableCountNull() { // Legacy sessions without cableCount default to single cable (safer assumption) - // This matches effectiveTotalVolumeKg, InsightCards, and official Vitruvian app behavior + // This matches effectiveTotalVolumeKg and InsightCards val title = buildTitle(exerciseName = "Bench Press", weightPerCableKg = 50f, cableCount = null) assertEquals("Bench Press \u2014 50.0kg", title) } diff --git a/shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/util/WeightDisplayFormatterTest.kt b/shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/util/WeightDisplayFormatterTest.kt index 30edd15d9..c91de599a 100644 --- a/shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/util/WeightDisplayFormatterTest.kt +++ b/shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/util/WeightDisplayFormatterTest.kt @@ -21,7 +21,7 @@ class WeightDisplayFormatterTest { unit = WeightUnit.KG, ) - assertEquals(50f, result, "Official app displays selected load as per-cable weight") + assertEquals(50f, result, "Selected load is displayed as per-cable weight") } @Test diff --git a/shared/src/commonTest/kotlin/com/devil/phoenixproject/util/BleConstantsTest.kt b/shared/src/commonTest/kotlin/com/devil/phoenixproject/util/BleConstantsTest.kt index 0b389d630..4bb749b27 100644 --- a/shared/src/commonTest/kotlin/com/devil/phoenixproject/util/BleConstantsTest.kt +++ b/shared/src/commonTest/kotlin/com/devil/phoenixproject/util/BleConstantsTest.kt @@ -61,7 +61,7 @@ class BleConstantsTest { @Test fun `activation packet force config offsets match firmware layout`() { - // Official eccentric-up profile tail. + // Eccentric-up profile tail (firmware layout). assertEquals(0x48, BleConstants.ActivationPacket.OFFSET_ECC_UP_MIN_MMS) assertEquals(0x4A, BleConstants.ActivationPacket.OFFSET_ECC_UP_MAX_MMS) assertEquals(0x4C, BleConstants.ActivationPacket.OFFSET_ECC_UP_RAMP) @@ -70,7 +70,7 @@ class BleConstantsTest { assertEquals(0x48, BleConstants.ActivationPacket.OFFSET_SOFT_MAX) assertEquals(0x4C, BleConstants.ActivationPacket.OFFSET_INCREMENT) - // Official force config block. + // Force config block (firmware layout). assertEquals(0x50, BleConstants.ActivationPacket.OFFSET_FORCE_MIN) assertEquals(0x54, BleConstants.ActivationPacket.OFFSET_FORCE_MAX) assertEquals(0x58, BleConstants.ActivationPacket.OFFSET_TARGET_WEIGHT) diff --git a/shared/src/commonTest/kotlin/com/devil/phoenixproject/util/BlePacketFactoryTest.kt b/shared/src/commonTest/kotlin/com/devil/phoenixproject/util/BlePacketFactoryTest.kt index 3850c5bb5..7c30ab562 100644 --- a/shared/src/commonTest/kotlin/com/devil/phoenixproject/util/BlePacketFactoryTest.kt +++ b/shared/src/commonTest/kotlin/com/devil/phoenixproject/util/BlePacketFactoryTest.kt @@ -77,8 +77,8 @@ class BlePacketFactoryTest { } @Test - fun `createOfficialStopPacket returns 2-byte soft stop`() { - val packet = BlePacketFactory.createOfficialStopPacket() + fun `createSoftStopPacket returns 2-byte soft stop`() { + val packet = BlePacketFactory.createSoftStopPacket() assertEquals(2, packet.size) assertEquals(0x50.toByte(), packet[0]) @@ -743,7 +743,7 @@ class BlePacketFactoryTest { assertEquals(3, packet[0x04].toInt(), "warmupReps / romRepCount") assertEquals(2, packet[0x05].toInt(), "targetReps / repCount") - // EchoForceConfig fields (matching Echo force-config serialization order) + // Echo force-config fields (serialization order) assertEquals(0, readUShortLE(packet, 0x06), "spotter (always 0)") assertEquals(75, readUShortLE(packet, 0x08), "eccentricOverload") assertEquals(50, readUShortLE(packet, 0x0A), "referenceMapBlend (always 50)") @@ -755,7 +755,7 @@ class BlePacketFactoryTest { // Eccentric EchoPhase: fixed on the machine assertEquals(0.0f, readFloatLE(packet, 0x18), "eccentricDurationSeconds (always 0.0)") - assertEquals(-200.0f, readFloatLE(packet, 0x1C), "eccentricMaxVelocity (official=-200.0)") + assertEquals(-200.0f, readFloatLE(packet, 0x1C), "eccentricMaxVelocity (firmware default=-200.0)") } @Test @@ -811,7 +811,7 @@ class BlePacketFactoryTest { } // ========== Old School Mode: Expected Byte Layout Tests ========== - // Official mode mapping: Phoenix OldSchool = Official STATIC + // Firmware profile mapping: OldSchool uses the STATIC activation profile @Test fun `Old School packet matches expected byte layout RepConfig header`() { @@ -1000,7 +1000,7 @@ class BlePacketFactoryTest { } // ========== TUT Mode: Expected Byte Layout Tests ========== - // Phoenix TUT maps to official FOCUSED mode — identical activation profile values + // TUT uses the FOCUSED firmware activation profile — identical activation profile values @Test fun `TUT packet matches expected byte layout RepConfig header`() { @@ -1122,7 +1122,7 @@ class BlePacketFactoryTest { // bottom RepBound — ECCENTRIC-SPECIFIC OVERRIDE assertEquals(5.0f, readFloatLE(packet, 0x1C), "bottom.threshold") assertEquals(0.0f, readFloatLE(packet, 0x20), "bottom.drift") - // Official: bottom.inner = L(50, 250) — NOT the default L(250, 250) + // Eccentric profile: bottom.inner = L(50, 250) — NOT the default L(250, 250) assertEquals(50.toShort(), readShortLE(packet, 0x24), "bottom.inner.mmPerM (ECCENTRIC=50)") assertEquals(250.toShort(), readShortLE(packet, 0x26), "bottom.inner.mmMax") assertEquals(200.toShort(), readShortLE(packet, 0x28), "bottom.outer.mmPerM") @@ -1628,7 +1628,7 @@ class BlePacketFactoryTest { @Test fun `issue390 AMRAP mode keeps target weight at selected per-cable weight`() { // AMRAP uses reps=0xFF for unlimited reps. The force controller still - // receives the selected per-cable force in the official trailing force block. + // receives the selected per-cable force in the trailing force config block. val params = WorkoutParameters( programMode = ProgramMode.OldSchool, reps = 10, diff --git a/shared/src/iosMain/kotlin/com/devil/phoenixproject/util/CsvExporter.ios.kt b/shared/src/iosMain/kotlin/com/devil/phoenixproject/util/CsvExporter.ios.kt index d08fe0f0f..258038f27 100644 --- a/shared/src/iosMain/kotlin/com/devil/phoenixproject/util/CsvExporter.ios.kt +++ b/shared/src/iosMain/kotlin/com/devil/phoenixproject/util/CsvExporter.ios.kt @@ -64,7 +64,7 @@ class IosCsvExporter : CsvExporter { val exerciseName = exerciseNames[session.exerciseId] ?: session.exerciseId ?: "Unknown" val date = KmpUtils.formatTimestamp(session.timestamp, "yyyy-MM-dd") val time = KmpUtils.formatTimestamp(session.timestamp, "HH:mm") - // For Echo mode, use peak weight (matches official app behavior); otherwise use configured weight + // For Echo mode, peak weight is the meaningful load; otherwise use configured weight val isEchoMode = session.mode.contains("Echo", ignoreCase = true) val effectiveWeight = if (isEchoMode) { session.peakWeightKg ?: session.workingAvgWeightKg ?: session.weightPerCableKg diff --git a/shared/src/iosMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.ios.kt b/shared/src/iosMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.ios.kt index 49d1e1c1e..cdd493d3d 100644 --- a/shared/src/iosMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.ios.kt +++ b/shared/src/iosMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.ios.kt @@ -272,7 +272,7 @@ class IosDataBackupManager( .replace("-", "") + "_" + KmpUtils.formatTimestamp(KmpUtils.currentTimeMillis(), "HH:mm:ss") .replace(":", "") - val fileName = "vitruvian_backup_$timestamp.json" + val fileName = "phoenix_backup_$timestamp.json" val tempDir = NSTemporaryDirectory() return BackupJsonWriter("$tempDir$fileName") } @@ -317,7 +317,7 @@ class IosDataBackupManager( .replace("-", "") + "_" + KmpUtils.formatTimestamp(KmpUtils.currentTimeMillis(), "HH:mm:ss") .replace(":", "") - val fileName = "vitruvian_backup_$timestamp.json" + val fileName = "phoenix_backup_$timestamp.json" val filePath = "$backupDirectory/$fileName" val data = NSString.create(string = jsonString).dataUsingEncoding(NSUTF8StringEncoding) diff --git a/shared/src/iosMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.ios.kt b/shared/src/iosMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.ios.kt index c4b49c7f9..11fc06792 100644 --- a/shared/src/iosMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.ios.kt +++ b/shared/src/iosMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.ios.kt @@ -57,7 +57,7 @@ actual object DeviceInfo { // ==================== Formatted Output ==================== actual fun getFormattedInfo(): String = buildString { - appendLine("App: VitruvianPhoenix v$appVersionName (build $appVersionCode)") + appendLine("App: Project Phoenix v$appVersionName (build $appVersionCode)") appendLine("Build Type: $buildType") appendLine() appendLine("Device: $manufacturer $model") diff --git a/third_party/kable-core-android-patched/README.md b/third_party/kable-core-android-patched/README.md index aa0f6633e..21aaa4cd1 100644 --- a/third_party/kable-core-android-patched/README.md +++ b/third_party/kable-core-android-patched/README.md @@ -6,5 +6,5 @@ Phoenix substitutes only the Android variant of Kable with this project. The com metadata and non-Android variants still come from the upstream `kable-core` dependency. The local patch is intentionally narrow: Android characteristic writes use the legacy one-argument `BluetoothGatt.writeCharacteristic(...)` -path on all API levels, matching the official Vitruvian app while preserving +path on all API levels, which is the write shape the trainer firmware reliably accepts, while preserving Kable's operation guard and callback handling. diff --git a/third_party/kable-core-android-patched/src/androidMain/BluetoothGatt.kt b/third_party/kable-core-android-patched/src/androidMain/BluetoothGatt.kt index f0b22aaca..57224c6f3 100644 --- a/third_party/kable-core-android-patched/src/androidMain/BluetoothGatt.kt +++ b/third_party/kable-core-android-patched/src/androidMain/BluetoothGatt.kt @@ -55,7 +55,7 @@ internal fun BluetoothGatt.writeCharacteristicOrThrow( // Phoenix #333: Pixel 6/7 BCM4389 devices wedge the acknowledged-write // completion path at workout CONFIG start when Kable uses Android 13's // three-argument write API. Keep Kable's guard/callback pipeline, but use - // the same legacy one-argument write shape as the official Vitruvian app. + // the legacy one-argument write shape that the trainer firmware reliably accepts. characteristic.value = data characteristic.writeType = writeType if (!writeCharacteristic(characteristic)) {