Now I'll append a finalized pool-length plan section to the IMPLEMENTATION_PLAN.md. I'll replace the "Document status" line with the new section plus an updated status.

I'll add a new section "11. Appendix D — Pool-Length Horizontal Distance Plan (finalized)" with all the decisions incorporated.

---

## 11. Appendix D — Pool-Length Horizontal Distance Plan (finalized)

User decisions applied:

1. **Button mapping:** **UP = length**, **DOWN = contraction**.
2. **Pool-length presets:** **25 m**, **50 m**, **custom**.
3. **Settings access:** **Long-press BACK** from idle state.
4. **FIT field:** **Custom developer field** only; do not write Garmin standard `distance`.
5. **Counting model:** User **presses manually** when to count a length.

---

### D.1 Goal

Add an optional **manual pool-length / lane-based horizontal distance tracker** to the existing fēnix 3 breath-hold app, letting the user enter a pool length and press UP during a session to increment completed lengths, producing a total horizontal distance stored in a custom FIT field.

### D.2 Constraints

- Target hardware remains **fēnix 3 / fēnix 3 HR** (CIQ API Level 1.4.0).
- No GPS or accelerometer dead-reckoning will be used; distance is **length count × pool length**.
- No raw ambient pressure / depth on fēnix 3, so vertical position is not computed.
- Existing prep → hold → recovery → save flow and HRM‑Tri support must remain intact.
- All input is via **5 physical buttons**; no touchscreen.
- Sideload-only distribution; no Connect IQ Store.
- Must compile with existing `manifest.xml` (no new permissions expected).

### D.3 Approach

Mirror Garmin’s native **Pool Swim** model of *known pool length + length count*, but count lengths **manually** because third-party apps cannot access the native stroke/turn-detection accelerometer pipeline.

- Long-press BACK from idle opens a **pool-length picker**.
- Presets are **25 m / 50 m / custom**; custom value is edited with UP/DOWN in 1 m steps.
- During a session, **UP increments the length counter**; DOWN continues to count contractions.
- Total distance = `lengthCount × poolLengthM`.
- Distance and length count are written to custom FIT session fields on save.

### D.4 Steps

1. **Add pool-length storage**
   - Variables: `poolLengthM`, `poolLengthIndex` (0=25, 1=50, 2=custom).
   - Use `Application.Storage` if available; otherwise keep in-memory with a sensible default.
   - Guard storage access with `Application has :Storage`.

2. **Create pool-length picker view**
   - New files: `source/PoolLengthView.mc`, `source/PoolLengthDelegate.mc`.
   - Opened from idle via **long-press BACK**.
   - Shows current preset or custom value.
   - UP/DOWN cycles 25 m → 50 m → custom.
   - When custom is selected, START toggles into custom-edit mode; UP/DOWN changes value; START confirms.
   - BACK/ESC cancels; START saves and returns to idle.

3. **Update button routing in `BreathHoldDelegate.mc`**
   - Short UP during idle opens pool-length picker.
   - During hold/recovery: **UP → `onLengthButton()`**, **DOWN → `onContractionButton()`**.
   - Keep START and long-press BACK behavior from existing delegate.

4. **Add length/distance state to `BreathHoldView.mc`**
   - Fields: `lengthCount`, `horizontalDistanceM`.
   - `onLengthButton()` increments `lengthCount` and recalculates `horizontalDistanceM`.
   - Update `onUpdate(dc)` to show `LAPS`/`DIST` line during hold and recovery.

5. **Add custom FIT fields**
   - In `startSession()`, create:
     - `horizontal_distance_m` (`UINT16`, units `"m"`, `MESG_TYPE_SESSION`).
     - `length_count` (`UINT8`, units `"laps"`, `MESG_TYPE_SESSION`).
   - In `finishSession()`, write final values before `session.save()`.
   - Do not touch Garmin’s built-in `distance` field.

6. **Update idle screen**
   - Display configured pool length when a session is not running, e.g. `POOL 25m`.
   - Show hint: `HOLD BACK = settings`.

7. **Update README usage and customization sections**
   - Document UP = length, DOWN = contraction.
   - Explain how to set pool length.
   - Note that distance is manual and approximate.

8. **Build and simulator verification**
   - Build for `fenix3` and `fenix3_hr`.
   - In simulator: open settings, change presets, count lengths, save, inspect FIT.

9. **Real-device shallow-water test**
   - Sideload to fēnix 3.
   - In a 25 m or 50 m pool, press UP each time you complete a length.
   - Verify displayed distance and exported FIT custom fields match.

### D.5 Verification

| Step | Verification |
|---|---|
| 1 | App launches with default 25 m; storage guard compiles cleanly. |
| 2 | Long-press BACK from idle opens picker; UP/DOWN cycles 25/50/custom; custom edit works. |
| 3 | Button routing tested in simulator: UP counts length, DOWN counts contraction, START advances state. |
| 4 | Display shows correct lap/distance during hold and on done screen. |
| 5 | FIT custom fields `horizontal_distance_m` and `length_count` appear in exported session. |
| 6 | Idle screen shows selected pool length and settings hint. |
| 7 | README matches observed behavior. |
| 8 | `build.sh` completes without errors or memory-limit warnings. |
| 9 | Real pool test: known pool length × manual count matches FIT output. |

### D.6 Risks & Rollbacks

| Risk | Impact | Mitigation / Rollback |
|---|---|---|
| `Application.Storage` unavailable on fēnix 3 API 1.4.0 | Pool length not persisted between launches | Guard with `Application has :Storage`; fall back to in-memory default. |
| Long-press BACK conflicts with existing short-press exit | Unexpected behavior | Implement distinct timer thresholds; test both short and long press. |
| UP previously counted contractions | Users may press wrong button during hold | Clear on-screen labels: `UP=LAP  DOWN=CONT`. |
| Manual counting at 1–2 m depth is hard with wet buttons | Distance count may be missed | Document limitation; app is a log, not a safety instrument. |
| Added views exceed fēnix 3 app memory limit | Build or runtime failure | Keep picker minimal; reuse strings; build with memory reporting. |
| Custom FIT fields not visible in Garmin Connect | User cannot see distance | Mention that developer fields require Connect IQ-compatible FIT viewers; standard distance field is intentionally not used. |

### D.7 Open Questions

None remaining; all user decisions captured above.

---

**Document status:** Draft — pool-length plan finalized in §11; awaiting implementation go-ahead.

---

**Document status:** Draft — awaiting user decisions in §7 before moving to code.


Successfully replaced 1 block(s) in ~/code/garmin-freedive-app/IMPLEMENTATION_PLAN.md.

     ...
 360 - Garmin App Review Guidelines: diving apps are not listed in the Connect IQ Store.
 361
 362 ---
 363
-364 **Document status:** Draft — awaiting user decisions in §7 before moving to code.
+364 ## 11. Appendix D — Pool-Length Horizontal Distance Plan (finalized)
+365
+366 User decisions applied:
+367
+368 1. **Button mapping:** **UP = length**, **DOWN = contraction**.
+369 2. **Pool-length presets:** **25 m**, **50 m**, **custom**.
+370 3. **Settings access:** **Long-press BACK** from idle state.
+371 4. **FIT field:** **Custom developer field** only; do not write Garmin standard `distance`.
+372 5. **Counting model:** User **presses manually** when to count a length.
+373
+374 ---
+375
+376 ### D.1 Goal
+377
+378 Add an optional **manual pool-length / lane-based horizontal distance tracker** to the existing fēnix 3 breath-hold app, letting the user enter a pool length and press UP during a session to increment completed lengths, producing a total horizontal distance stored in a custom FIT field.
+379
+380 ### D.2 Constraints
+381
+382 - Target hardware remains **fēnix 3 / fēnix 3 HR** (CIQ API Level 1.4.0).
+383 - No GPS or accelerometer dead-reckoning will be used; distance is **length count × pool length**.
+384 - No raw ambient pressure / depth on fēnix 3, so vertical position is not computed.
+385 - Existing prep → hold → recovery → save flow and HRM‑Tri support must remain intact.
+386 - All input is via **5 physical buttons**; no touchscreen.
+387 - Sideload-only distribution; no Connect IQ Store.
+388 - Must compile with existing `manifest.xml` (no new permissions expected).
+389
+390 ### D.3 Approach
+391
+392 Mirror Garmin’s native **Pool Swim** model of *known pool length + length count*, but count lengths **manually** because third-party apps cannot access the native stroke/turn-detection accelerometer pipeline.
+393
+394 - Long-press BACK from idle opens a **pool-length picker**.
+395 - Presets are **25 m / 50 m / custom**; custom value is edited with UP/DOWN in 1 m steps.
+396 - During a session, **UP increments the length counter**; DOWN continues to count contractions.
+397 - Total distance = `lengthCount × poolLengthM`.
+398 - Distance and length count are written to custom FIT session fields on save.
+399
+400 ### D.4 Steps
+401
+402 1. **Add pool-length storage**
+403    - Variables: `poolLengthM`, `poolLengthIndex` (0=25, 1=50, 2=custom).
+404    - Use `Application.Storage` if available; otherwise keep in-memory with a sensible default.
+405    - Guard storage access with `Application has :Storage`.
+406
+407 2. **Create pool-length picker view**
+408    - New files: `source/PoolLengthView.mc`, `source/PoolLengthDelegate.mc`.
+409    - Opened from idle via **long-press BACK**.
+410    - Shows current preset or custom value.
+411    - UP/DOWN cycles 25 m → 50 m → custom.
+412    - When custom is selected, START toggles into custom-edit mode; UP/DOWN changes value; START confirms.
+413    - BACK/ESC cancels; START saves and returns to idle.
+414
+415 3. **Update button routing in `BreathHoldDelegate.mc`**
+416    - Short UP during idle opens pool-length picker.
+417    - During hold/recovery: **UP → `onLengthButton()`**, **DOWN → `onContractionButton()`**.
+418    - Keep START and long-press BACK behavior from existing delegate.
+419
+420 4. **Add length/distance state to `BreathHoldView.mc`**
+421    - Fields: `lengthCount`, `horizontalDistanceM`.
+422    - `onLengthButton()` increments `lengthCount` and recalculates `horizontalDistanceM`.
+423    - Update `onUpdate(dc)` to show `LAPS`/`DIST` line during hold and recovery.
+424
+425 5. **Add custom FIT fields**
+426    - In `startSession()`, create:
+427      - `horizontal_distance_m` (`UINT16`, units `"m"`, `MESG_TYPE_SESSION`).
+428      - `length_count` (`UINT8`, units `"laps"`, `MESG_TYPE_SESSION`).
+429    - In `finishSession()`, write final values before `session.save()`.
+430    - Do not touch Garmin’s built-in `distance` field.
+431
+432 6. **Update idle screen**
+433    - Display configured pool length when a session is not running, e.g. `POOL 25m`.
+434    - Show hint: `HOLD BACK = settings`.
+435
+436 7. **Update README usage and customization sections**
+437    - Document UP = length, DOWN = contraction.
+438    - Explain how to set pool length.
+439    - Note that distance is manual and approximate.
+440
+441 8. **Build and simulator verification**
+442    - Build for `fenix3` and `fenix3_hr`.
+443    - In simulator: open settings, change presets, count lengths, save, inspect FIT.
+444
+445 9. **Real-device shallow-water test**
+446    - Sideload to fēnix 3.
+447    - In a 25 m or 50 m pool, press UP each time you complete a length.
+448    - Verify displayed distance and exported FIT custom fields match.
+449
+450 ### D.5 Verification
+451
+452 | Step | Verification |
+453 |---|---|
+454 | 1 | App launches with default 25 m; storage guard compiles cleanly. |
+455 | 2 | Long-press BACK from idle opens picker; UP/DOWN cycles 25/50/custom; custom edit works. |
+456 | 3 | Button routing tested in simulator: UP counts length, DOWN counts contraction, START advances state. |
+457 | 4 | Display shows correct lap/distance during hold and on done screen. |
+458 | 5 | FIT custom fields `horizontal_distance_m` and `length_count` appear in exported session. |
+459 | 6 | Idle screen shows selected pool length and settings hint. |
+460 | 7 | README matches observed behavior. |
+461 | 8 | `build.sh` completes without errors or memory-limit warnings. |
+462 | 9 | Real pool test: known pool length × manual count matches FIT output. |
+463
+464 ### D.6 Risks & Rollbacks
+465
+466 | Risk | Impact | Mitigation / Rollback |
+467 |---|---|---|
+468 | `Application.Storage` unavailable on fēnix 3 API 1.4.0 | Pool length not persisted between launches | Guard with `Application has :Storage`; fall back to in-memory default. |
+469 | Long-press BACK conflicts with existing short-press exit | Unexpected behavior | Implement distinct timer thresholds; test both short and long press. |
+470 | UP previously counted contractions | Users may press wrong button during hold | Clear on-screen labels: `UP=LAP  DOWN=CONT`. |
+471 | Manual counting at 1–2 m depth is hard with wet buttons | Distance count may be missed | Document limitation; app is a log, not a safety instrument. |
+472 | Added views exceed fēnix 3 app memory limit | Build or runtime failure | Keep picker minimal; reuse strings; build with memory reporting. |
+473 | Custom FIT fields not visible in Garmin Connect | User cannot see distance | Mention that developer fields require Connect IQ-compatible FIT viewers; standard distance field is intentionally not used. |
+474
+475 ### D.7 Open Questions
+476
+477 None remaining; all user decisions captured above.
+478
+479 ---
+480
+481 **Document status:** Draft — pool-length plan finalized in §11; awaiting implementation go-ahead.

--- ~/code/garmin-freedive-app/IMPLEMENTATION_PLAN.md
+++ ~/code/garmin-freedive-app/IMPLEMENTATION_PLAN.md
@@ -360,5 +360,122 @@
 - Garmin App Review Guidelines: diving apps are not listed in the Connect IQ Store.

 ---

-**Document status:** Draft — awaiting user decisions in §7 before moving to code.
+## 11. Appendix D — Pool-Length Horizontal Distance Plan (finalized)
+
+User decisions applied:
+
+1. **Button mapping:** **UP = length**, **DOWN = contraction**.
+2. **Pool-length presets:** **25 m**, **50 m**, **custom**.
+3. **Settings access:** **Long-press BACK** from idle state.
+4. **FIT field:** **Custom developer field** only; do not write Garmin standard `distance`.
+5. **Counting model:** User **presses manually** when to count a length.
+
+---
+
+### D.1 Goal
+
+Add an optional **manual pool-length / lane-based horizontal distance tracker** to the existing fēnix 3 breath-hold app, letting the user enter a pool length and press UP during a session to increment completed lengths, producing a total horizontal distance stored in a custom FIT field.
+
+### D.2 Constraints
+
+- Target hardware remains **fēnix 3 / fēnix 3 HR** (CIQ API Level 1.4.0).
+- No GPS or accelerometer dead-reckoning will be used; distance is **length count × pool length**.
+- No raw ambient pressure / depth on fēnix 3, so vertical position is not computed.
+- Existing prep → hold → recovery → save flow and HRM‑Tri support must remain intact.
+- All input is via **5 physical buttons**; no touchscreen.
+- Sideload-only distribution; no Connect IQ Store.
+- Must compile with existing `manifest.xml` (no new permissions expected).
+
+### D.3 Approach
+
+Mirror Garmin’s native **Pool Swim** model of *known pool length + length count*, but count lengths **manually** because third-party apps cannot access the native stroke/turn-detection accelerometer pipeline.
+
+- Long-press BACK from idle opens a **pool-length picker**.
+- Presets are **25 m / 50 m / custom**; custom value is edited with UP/DOWN in 1 m steps.
+- During a session, **UP increments the length counter**; DOWN continues to count contractions.
+- Total distance = `lengthCount × poolLengthM`.
+- Distance and length count are written to custom FIT session fields on save.
+
+### D.4 Steps
+
+1. **Add pool-length storage**
+   - Variables: `poolLengthM`, `poolLengthIndex` (0=25, 1=50, 2=custom).
+   - Use `Application.Storage` if available; otherwise keep in-memory with a sensible default.
+   - Guard storage access with `Application has :Storage`.
+
+2. **Create pool-length picker view**
+   - New files: `source/PoolLengthView.mc`, `source/PoolLengthDelegate.mc`.
+   - Opened from idle via **long-press BACK**.
+   - Shows current preset or custom value.
+   - UP/DOWN cycles 25 m → 50 m → custom.
+   - When custom is selected, START toggles into custom-edit mode; UP/DOWN changes value; START confirms.
+   - BACK/ESC cancels; START saves and returns to idle.
+
+3. **Update button routing in `BreathHoldDelegate.mc`**
+   - Short UP during idle opens pool-length picker.
+   - During hold/recovery: **UP → `onLengthButton()`**, **DOWN → `onContractionButton()`**.
+   - Keep START and long-press BACK behavior from existing delegate.
+
+4. **Add length/distance state to `BreathHoldView.mc`**
+   - Fields: `lengthCount`, `horizontalDistanceM`.
+   - `onLengthButton()` increments `lengthCount` and recalculates `horizontalDistanceM`.
+   - Update `onUpdate(dc)` to show `LAPS`/`DIST` line during hold and recovery.
+
+5. **Add custom FIT fields**
+   - In `startSession()`, create:
+     - `horizontal_distance_m` (`UINT16`, units `"m"`, `MESG_TYPE_SESSION`).
+     - `length_count` (`UINT8`, units `"laps"`, `MESG_TYPE_SESSION`).
+   - In `finishSession()`, write final values before `session.save()`.
+   - Do not touch Garmin’s built-in `distance` field.
+
+6. **Update idle screen**
+   - Display configured pool length when a session is not running, e.g. `POOL 25m`.
+   - Show hint: `HOLD BACK = settings`.
+
+7. **Update README usage and customization sections**
+   - Document UP = length, DOWN = contraction.
+   - Explain how to set pool length.
+   - Note that distance is manual and approximate.
+
+8. **Build and simulator verification**
+   - Build for `fenix3` and `fenix3_hr`.
+   - In simulator: open settings, change presets, count lengths, save, inspect FIT.
+
+9. **Real-device shallow-water test**
+   - Sideload to fēnix 3.
+   - In a 25 m or 50 m pool, press UP each time you complete a length.
+   - Verify displayed distance and exported FIT custom fields match.
+
+### D.5 Verification
+
+| Step | Verification |
+|---|---|
+| 1 | App launches with default 25 m; storage guard compiles cleanly. |
+| 2 | Long-press BACK from idle opens picker; UP/DOWN cycles 25/50/custom; custom edit works. |
+| 3 | Button routing tested in simulator: UP counts length, DOWN counts contraction, START advances state. |
+| 4 | Display shows correct lap/distance during hold and on done screen. |
+| 5 | FIT custom fields `horizontal_distance_m` and `length_count` appear in exported session. |
+| 6 | Idle screen shows selected pool length and settings hint. |
+| 7 | README matches observed behavior. |
+| 8 | `build.sh` completes without errors or memory-limit warnings. |
+| 9 | Real pool test: known pool length × manual count matches FIT output. |
+
+### D.6 Risks & Rollbacks
+
+| Risk | Impact | Mitigation / Rollback |
+|---|---|---|
+| `Application.Storage` unavailable on fēnix 3 API 1.4.0 | Pool length not persisted between launches | Guard with `Application has :Storage`; fall back to in-memory default. |
+| Long-press BACK conflicts with existing short-press exit | Unexpected behavior | Implement distinct timer thresholds; test both short and long press. |
+| UP previously counted contractions | Users may press wrong button during hold | Clear on-screen labels: `UP=LAP  DOWN=CONT`. |
+| Manual counting at 1–2 m depth is hard with wet buttons | Distance count may be missed | Document limitation; app is a log, not a safety instrument. |
+| Added views exceed fēnix 3 app memory limit | Build or runtime failure | Keep picker minimal; reuse strings; build with memory reporting. |
+| Custom FIT fields not visible in Garmin Connect | User cannot see distance | Mention that developer fields require Connect IQ-compatible FIT viewers; standard distance field is intentionally not used. |
+
+### D.7 Open Questions
+
+None remaining; all user decisions captured above.
+
+---
+
+**Document status:** Draft — pool-length plan finalized in §11; awaiting implementation go-ahead.


The user provided decisions and I updated the implementation plan. I should now confirm the update and summarize the finalized plan concisely. No further tool calls are needed unless I want to verify the edit. Let me verify the section was added correctly.


- Garmin App Review Guidelines: diving apps are not listed in the Connect IQ Store.

---

## 11. Appendix D — Pool-Length Horizontal Distance Plan (finalized)

User decisions applied:

1. **Button mapping:** **UP = length**, **DOWN = contraction**.
2. **Pool-length presets:** **25 m**, **50 m**, **custom**.
3. **Settings access:** **Long-press BACK** from idle state.
4. **FIT field:** **Custom developer field** only; do not write Garmin standard `distance`.
5. **Counting model:** User **presses manually** when to count a length.

---

### D.1 Goal

Add an optional **manual pool-length / lane-based horizontal distance tracker** to the existing fēnix 3 breath-hold app, letting the user enter a pool length and press UP during a session to increment completed lengths, producing a total horizontal distance stored in a custom FIT field.

### D.2 Constraints

- Target hardware remains **fēnix 3 / fēnix 3 HR** (CIQ API Level 1.4.0).
- No GPS or accelerometer dead-reckoning will be used; distance is **length count × pool length**.
- No raw ambient pressure / depth on fēnix 3, so vertical position is not computed.
- Existing prep → hold → recovery → save flow and HRM‑Tri support must remain intact.
- All input is via **5 physical buttons**; no touchscreen.
- Sideload-only distribution; no Connect IQ Store.
- Must compile with existing `manifest.xml` (no new permissions expected).

### D.3 Approach

Mirror Garmin’s native **Pool Swim** model of *known pool length + length count*, but count lengths **manually** because third-party apps cannot access the native stroke/turn-detection accelerometer pipeline.

- Long-press BACK from idle opens a **pool-length picker**.
- Presets are **25 m / 50 m / custom**; custom value is edited with UP/DOWN in 1 m steps.
- During a session, **UP increments the length counter**; DOWN continues to count contractions.
- Total distance = `lengthCount × poolLengthM`.
- Distance and length count are written to custom FIT session fields on save.

### D.4 Steps

1. **Add pool-length storage**
   - Variables: `poolLengthM`, `poolLengthIndex` (0=25, 1=50, 2=custom).
   - Use `Application.Storage` if available; otherwise keep in-memory with a sensible default.
   - Guard storage access with `Application has :Storage`.

2. **Create pool-length picker view**
   - New files: `source/PoolLengthView.mc`, `source/PoolLengthDelegate.mc`.
   - Opened from idle via **long-press BACK**.
   - Shows current preset or custom value.
   - UP/DOWN cycles 25 m → 50 m → custom.
   - When custom is selected, START toggles into custom-edit mode; UP/DOWN changes value; START confirms.
   - BACK/ESC cancels; START saves and returns to idle.

3. **Update button routing in `BreathHoldDelegate.mc`**
   - Short UP during idle opens pool-length picker.
   - During hold/recovery: **UP → `onLengthButton()`**, **DOWN → `onContractionButton()`**.
   - Keep START and long-press BACK behavior from existing delegate.

4. **Add length/distance state to `BreathHoldView.mc`**
   - Fields: `lengthCount`, `horizontalDistanceM`.
   - `onLengthButton()` increments `lengthCount` and recalculates `horizontalDistanceM`.
   - Update `onUpdate(dc)` to show `LAPS`/`DIST` line during hold and recovery.

5. **Add custom FIT fields**
   - In `startSession()`, create:
     - `horizontal_distance_m` (`UINT16`, units `"m"`, `MESG_TYPE_SESSION`).
     - `length_count` (`UINT8`, units `"laps"`, `MESG_TYPE_SESSION`).
   - In `finishSession()`, write final values before `session.save()`.
   - Do not touch Garmin’s built-in `distance` field.

6. **Update idle screen**
   - Display configured pool length when a session is not running, e.g. `POOL 25m`.
   - Show hint: `HOLD BACK = settings`.

7. **Update README usage and customization sections**
   - Document UP = length, DOWN = contraction.
   - Explain how to set pool length.
   - Note that distance is manual and approximate.

8. **Build and simulator verification**
   - Build for `fenix3` and `fenix3_hr`.
   - In simulator: open settings, change presets, count lengths, save, inspect FIT.

9. **Real-device shallow-water test**
   - Sideload to fēnix 3.
   - In a 25 m or 50 m pool, press UP each time you complete a length.
   - Verify displayed distance and exported FIT custom fields match.

### D.5 Verification

| Step | Verification |
|---|---|
| 1 | App launches with default 25 m; storage guard compiles cleanly. |
| 2 | Long-press BACK from idle opens picker; UP/DOWN cycles 25/50/custom; custom edit works. |
| 3 | Button routing tested in simulator: UP counts length, DOWN counts contraction, START advances state. |
| 4 | Display shows correct lap/distance during hold and on done screen. |
| 5 | FIT custom fields `horizontal_distance_m` and `length_count` appear in exported session. |
| 6 | Idle screen shows selected pool length and settings hint. |
| 7 | README matches observed behavior. |
| 8 | `build.sh` completes without errors or memory-limit warnings. |
| 9 | Real pool test: known pool length × manual count matches FIT output. |

### D.6 Risks & Rollbacks

| Risk | Impact | Mitigation / Rollback |
|---|---|---|
| `Application.Storage` unavailable on fēnix 3 API 1.4.0 | Pool length not persisted between launches | Guard with `Application has :Storage`; fall back to in-memory default. |
| Long-press BACK conflicts with existing short-press exit | Unexpected behavior | Implement distinct timer thresholds; test both short and long press. |
| UP previously counted contractions | Users may press wrong button during hold | Clear on-screen labels: `UP=LAP  DOWN=CONT`. |
| Manual counting at 1–2 m depth is hard with wet buttons | Distance count may be missed | Document limitation; app is a log, not a safety instrument. |
| Added views exceed fēnix 3 app memory limit | Build or runtime failure | Keep picker minimal; reuse strings; build with memory reporting. |
| Custom FIT fields not visible in Garmin Connect | User cannot see distance | Mention that developer fields require Connect IQ-compatible FIT viewers; standard distance field is intentionally not used. |

### D.7 Open Questions

None remaining; all user decisions captured above.

---

[3 more lines in file. Use offset=480 to continue.]


The plan is now in the file. I should summarize for the user that the finalized plan has been added to IMPLEMENTATION_PLAN.md section 11, and list the key decisions.

Done. I updated `code/garmin-freedive-app/IMPLEMENTATION_PLAN.md` with the finalized pool-length horizontal-distance plan in **§11 Appendix D**.

Your decisions applied:

| Decision | Result |
|---|---|
| Button mapping | **UP = length/lap**, **DOWN = contraction** |
| Pool-length presets | **25 m**, **50 m**, **custom** |
| Settings access | **Long-press BACK** from idle |
| FIT distance field | **Custom developer field** only |
| Counting | **Manual** — you press UP each length |

Key points from the plan:

- Distance is computed as **length count × pool length** — the same principle Garmin Pool Swim uses, but counted manually because third-party apps cannot access the native stroke/turn detection.
- No GPS or accelerometer dead-reckoning is involved.
- New files: `PoolLengthView.mc` + `PoolLengthDelegate.mc`.
- New custom FIT fields: `horizontal_distance_m` and `length_count`.
- fēnix 3 memory limits and wet-button reliability are flagged as the main risks.

Open questions: none remaining.

Say the word when you want me to start implementing it.


start implementing please


Context-mode is active, so for analyzing/build output/logs I should prefer ctx_execute / ctx_batch_execute. But actual file edits still use Read/Edit/Write. The user wants me to implement the pool-length horizontal distance feature.

I need to:
1. Create PoolLengthView.mc and PoolLengthDelegate.mc
2. Modify BreathHoldApp.mc to handle view switching
3. Modify BreathHoldDelegate.mc for long-press BACK and button routing
4. Modify BreathHoldView.mc for pool length state, length counting, display updates, FIT fields
5. Update README.md with usage info
6. Update IMPLEMENTATION_PLAN.md status

I already read the key files. Let me implement now.

Plan:
- PoolLengthView: simple view showing pool length setting. It has presets 25, 50, custom. Custom edit mode.
- PoolLengthDelegate: handles buttons. UP/DOWN cycles presets and custom value. START confirms. BACK cancels.
- Need to get pool length into BreathHoldView. Options:
  - Store in Application.Storage
  - Pass via Application properties? In Monkey C there's no easy global state except static class vars.
  - Use Application.Storage if available, else static vars in BreathHoldApp or a separate settings class.
  - Simpler: add static vars to BreathHoldApp that both views can access. But static class vars in Monkey C are per-class and accessible.

Actually, in Monkey C, we can use the AppBase instance as a singleton. We can add getter/setter in BreathHoldApp and access via `Application.getApp()`. That returns the current AppBase instance cast to Object, then we can cast to BreathHoldApp.

But for fēnix 3 simplicity, I'll use `Application.Storage` if available. The guard is `Application has :Storage`. Actually for API 1.4.0, Storage might not be available. Let me check: Application.Storage was introduced in API 2.4.0 I think. On fēnix 3 it's likely not available. Better to use static properties or pass through Application.getApp().

Let me use static class variables in a dedicated Settings class or in BreathHoldApp itself. Actually, `Application.getApp()` returns an Object, and we can cast to BreathHoldApp. Add methods like `getPoolLengthM()` and `setPoolLengthM()`.

Wait - BreathHoldApp already extends Application.AppBase. We can do:

```monkey-c
function getApp() as BreathHoldApp {
    return Application.getApp() as BreathHoldApp;
}
```

But casting might be tricky. We can just use `Application.getApp()` and call methods with proper `has` checks, or store as a property. Actually in Monkey C you can cast: `var app = Application.getApp() as BreathHoldApp;`.

But I recall that in Monkey C, you can access static fields across classes. Let me use a simple `Settings` class with static vars. That avoids cast issues.

```monkey-c
class Settings {
    static var poolLengthM = 25;
    static var poolLengthIndex = 0; // 0=25, 1=50, 2=custom
}
```

Then both BreathHoldView and PoolLengthView can read/write `Settings.poolLengthM`.

For persistence between launches, we could try Application.Storage on newer devices, but since fēnix 3 doesn't have it, static vars reset each launch. That's acceptable per the risk table.

Actually, let me check if Application.Storage is available in API 1.4.0. Looking it up: In Connect IQ, `Toybox.Application.Storage` was introduced in API 2.4.0. So fēnix 3 won't have it. We can still guard with `Application has :Storage` for forward compatibility, but primary storage is static vars.

Now implementation:

### PoolLengthView.mc
```monkey-c
using Toybox.WatchUi;
using Toybox.Graphics;
using Toybox.System;

class PoolLengthView extends WatchUi.View {

    const PRESET_25 = 0;
    const PRESET_50 = 1;
    const PRESET_CUSTOM = 2;

    const PRESET_VALUES = [25, 50];

    var selectedIndex;
    var customValue;
    var inCustomEdit = false;

    function initialize() {
        View.initialize();
        selectedIndex = Settings.poolLengthIndex;
        customValue = Settings.poolLengthM;
    }

    function onUpdate(dc) {
        dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK);
        dc.clear();
        dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);

        var cx = dc.getWidth() / 2;
        var y = 16;

        dc.drawText(cx, y, Graphics.FONT_SMALL, "POOL LENGTH", Graphics.TEXT_JUSTIFY_CENTER);

        var valueStr = "";
        if (selectedIndex == PRESET_CUSTOM) {
            valueStr = customValue.format("%d") + "m";
        } else {
            valueStr = PRESET_VALUES[selectedIndex].format("%d") + "m";
        }

        dc.drawText(cx, y + 35, Graphics.FONT_NUMBER_MEDIUM, valueStr, Graphics.TEXT_JUSTIFY_CENTER);

        var hint = "";
        if (selectedIndex == PRESET_CUSTOM && inCustomEdit) {
            hint = "UP/DOWN adjust  START save";
        } else if (selectedIndex == PRESET_CUSTOM) {
            hint = "START edit custom";
        } else {
            hint = "UP/DOWN preset  START save";
        }
        dc.drawText(cx, y + 95, Graphics.FONT_TINY, hint, Graphics.TEXT_JUSTIFY_CENTER);

        dc.drawText(cx, y + 120, Graphics.FONT_TINY, "BACK cancel", Graphics.TEXT_JUSTIFY_CENTER);
    }

    function saveAndExit() {
        if (selectedIndex == PRESET_CUSTOM) {
            Settings.poolLengthM = customValue;
        } else {
            Settings.poolLengthM = PRESET_VALUES[selectedIndex];
        }
        Settings.poolLengthIndex = selectedIndex;
        WatchUi.popView(WatchUi.SLIDE_RIGHT);
    }

    function onUp() {
        if (selectedIndex == PRESET_CUSTOM && inCustomEdit) {
            customValue++;
            if (customValue > 100) { customValue = 100; }
        } else {
            selectedIndex++;
            if (selectedIndex > PRESET_CUSTOM) { selectedIndex = PRESET_CUSTOM; }
        }
        WatchUi.requestUpdate();
    }

    function onDown() {
        if (selectedIndex == PRESET_CUSTOM && inCustomEdit) {
            customValue--;
            if (customValue < 10) { customValue = 10; }
        } else {
            selectedIndex--;
            if (selectedIndex < 0) { selectedIndex = 0; }
        }
        WatchUi.requestUpdate();
    }

    function onStart() {
        if (selectedIndex == PRESET_CUSTOM && !inCustomEdit) {
            inCustomEdit = true;
            WatchUi.requestUpdate();
        } else {
            saveAndExit();
        }
    }
}
```

### PoolLengthDelegate.mc
```monkey-c
using Toybox.WatchUi;

class PoolLengthDelegate extends WatchUi.InputDelegate {

    var view;

    function initialize(v) {
        InputDelegate.initialize();
        view = v;
    }

    function onKey(keyEvent) {
        var key = keyEvent.getKey();

        if (key == WatchUi.KEY_UP) {
            view.onUp();
            return true;
        }

        if (key == WatchUi.KEY_DOWN) {
            view.onDown();
            return true;
        }

        if (key == WatchUi.KEY_START || key == WatchUi.KEY_ENTER) {
            view.onStart();
            return true;
        }

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            WatchUi.popView(WatchUi.SLIDE_RIGHT);
            return true;
        }

        return false;
    }
}
```

### Settings.mc
```monkey-c
class Settings {
    static var poolLengthM = 25;
    static var poolLengthIndex = 0; // 0=25, 1=50, 2=custom
}
```

### BreathHoldApp.mc
Need to handle initial view. Actually we can keep getInitialView returning [view, delegate]. The delegate handles long-press BACK. To push a new view, use `WatchUi.pushView(new PoolLengthView(), new PoolLengthDelegate(new PoolLengthView()), WatchUi.SLIDE_LEFT);`

But the initial delegate is BreathHoldDelegate. It can call pushView.

### BreathHoldDelegate.mc
Need to handle:
- Long-press BACK from idle -> open settings
- Short UP during idle -> also open settings? Wait, the plan says long-press BACK from idle opens settings. But I also wrote "Short UP during idle opens pool-length picker" in step 3. That contradicts the user decision. The user said long-press BACK for settings. Let me fix: only long-press BACK opens settings. UP during idle could be unused or open settings too? Better to only use long-press BACK for settings as requested.

But wait, during idle the user might press UP accidentally. We can make it do nothing or also open settings. Given limited buttons, let's make long-press BACK open settings, and leave UP unused in idle to avoid accidental changes.

Actually re-reading: "3. long press back from idle" - so settings access is long-press BACK. Good.

Button routing:
- During idle:
  - START -> start session
  - BACK short -> System.exit()
  - BACK long -> open pool length settings
- During prep/hold/recovery:
  - START -> state transitions
  - UP -> onLengthButton (during hold/recovery maybe also prep?)
  - DOWN -> onContractionButton (during hold)
  - BACK -> discard session and exit
- During done:
  - START -> reset
  - BACK -> exit

Wait, user decision 5: "i press manually when to count". This means UP to count lengths should work during the hold phase (maybe also recovery?). The user said "when to count" - likely during the breath-hold. Let's allow length counting during hold and recovery. Prep is a countdown before hold, probably not swimming.

But the original BreathHoldDelegate routes UP/DOWN to onContractionButton regardless of state. I need to change so that:
- In STATE_HOLD and STATE_RECOVERY: UP -> onLengthButton, DOWN -> onContractionButton
- In STATE_PREP: UP/DOWN perhaps nothing? Or still length/contraction? Probably nothing; user is breathing.

Actually, to be flexible, during prep maybe UP/DOWN could adjust pool length? No, that would complicate. Better: UP/DOWN do nothing during prep. During hold: UP length, DOWN contraction. During recovery: UP length (if user forgot to count), DOWN maybe nothing.

But actually recovery is after the breath-hold, so counting lengths then doesn't make sense. Let's only allow UP length during hold. But the user might surface and remember they swam a length during recovery. Hmm. Let's allow during hold and recovery for safety.

For the long-press detection: Monkey C doesn't have a built-in long-press event in InputDelegate.onKey. We need to implement it ourselves by tracking key down time and detecting when key is held.

InputDelegate has `onKey(keyEvent)` which gives `KEY_DOWN` and `KEY_UP`? Actually `keyEvent.getType()` returns KEY_PRESSED, KEY_RELEASED, etc. We can track when BACK is pressed and start a timer, then if released before threshold treat as short, if timer fires treat as long.

Let me check: in Monkey C, `WatchUi.InputDelegate.onKey` receives a `KeyEvent` which has `getKey()` and `getType()`. Types are `KEY_PRESSED`, `KEY_RELEASED`, maybe `KEY_PING`? Actually I think `InputDelegate.onKey` is only called on press, not release. Hmm.

Let me verify: The API docs say:
- `onKey(keyEvent)` - Called when a key is pressed.

Actually I recall that `KeyEvent.getType()` can be `KEY_PRESSED`, `KEY_RELEASED`, or `KEY_PING`. Yes, it can receive release events. But I'm not 100% sure for fēnix 3 API 1.4.0.

Alternative: Use a Timer in the delegate. When BACK is pressed, start a timer. If START or other key is pressed before timer fires, cancel. If timer fires, trigger long-press action.

But there's no easy way to know if BACK is still held without release events. However, for a simple long-press detection, we can just start a timer on KEY_PRESSED, and if no KEY_RELEASED arrives within e.g. 800ms, treat it as long press. But if the user holds it for 800ms and releases at 900ms, we'd get a release event after long press triggered. We need to ignore the release after long press.

Let me implement with timer and release tracking:

```monkey-c
class BreathHoldDelegate extends WatchUi.InputDelegate {

    var view;
    var backTimer = null;
    var backPressedTime = 0;
    const LONG_PRESS_MS = 800;

    function initialize(v) {
        InputDelegate.initialize();
        view = v;
    }

    function onKey(keyEvent) {
        var key = keyEvent.getKey();
        var type = keyEvent.getType();

        if (key == WatchUi.KEY_START || key == WatchUi.KEY_ENTER) {
            if (type == KEY_PRESSED) { view.onStartButton(); }
            return true;
        }

        if (key == WatchUi.KEY_UP) {
            if (type == KEY_PRESSED) {
                if (view.state == view.STATE_HOLD || view.state == view.STATE_RECOVERY) {
                    view.onLengthButton();
                }
            }
            return true;
        }

        if (key == WatchUi.KEY_DOWN) {
            if (type == KEY_PRESSED) {
                if (view.state == view.STATE_HOLD) {
                    view.onContractionButton();
                }
            }
            return true;
        }

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            if (type == KEY_PRESSED) {
                backPressedTime = System.getTimer();
                backTimer = new Timer.Timer();
                backTimer.start(method(:onBackLongPress), LONG_PRESS_MS, false);
            } else if (type == KEY_RELEASED) {
                if (backTimer != null) {
                    backTimer.stop();
                    backTimer = null;
                }
                var elapsed = System.getTimer() - backPressedTime;
                if (elapsed < LONG_PRESS_MS) {
                    // short press
                    if (view.state == view.STATE_IDLE) {
                        System.exit();
                    } else {
                        view.discardSession();
                        view.state = view.STATE_IDLE;
                        WatchUi.requestUpdate();
                    }
                }
                // else long press already handled
            }
            return true;
        }

        return false;
    }

    function onBackLongPress() {
        backTimer = null;
        if (view.state == view.STATE_IDLE) {
            WatchUi.pushView(new PoolLengthView(), new PoolLengthDelegate(new PoolLengthView()), WatchUi.SLIDE_LEFT);
        }
    }
}
```

But I'm not sure if `System.getTimer()` is available. It's `System.getTimer()` returning ms since device start. Yes.

However, I need to be careful: if KEY_RELEASED isn't delivered, then after long press we won't do anything bad because long press handler already pushed settings view. But if we then release, we get KEY_RELEASED, elapsed will be > LONG_PRESS_MS, and we do nothing. Good.

But wait, `KEY_PRESSED` for BACK during idle triggers the timer. If the user holds it long, onBackLongPress fires and pushes settings. Then on KEY_RELEASED, we check elapsed and do nothing. Good.

Potential issue: when settings view is pushed, the delegate's backTimer might still exist. But the settings delegate handles its own BACK. The BreathHoldDelegate is still alive but not active. When returning, we should reset backTimer. Actually the settings view is pushed, and when popped, we return to BreathHoldView. The BreathHoldDelegate may still have backTimer null because onBackLongPress set it to null. OK.

But what if user short-presses BACK during idle to exit? Timer starts, then KEY_RELEASED fires at <800ms, timer stopped, elapsed < LONG_PRESS_MS, System.exit(). Good.

What about during session: short BACK discards. Long BACK currently does nothing during session (only opens settings from idle). That's fine per plan.

Now, do we need to import Timer in delegate? Yes.

But wait, I used `KEY_PRESSED` and `KEY_RELEASED` constants. In Monkey C, these are likely `WatchUi.KEY_EVENT_TYPE_PRESSED` and `WatchUi.KEY_EVENT_TYPE_RELEASED`. Let me verify the actual names.

Looking at the API: `KeyEvent` has `getType()` returning a Number. Constants are likely `WatchUi.KEY_EVENT_TYPE_PRESSED` and `WatchUi.KEY_EVENT_TYPE_RELEASED`. Actually I think they're in `WatchUi` as `KEY_PRESSED` and `KEY_RELEASED`? Let me check known code.

Actually in Monkey C, the constants are `WatchUi.KEY_EVENT_TYPE_PRESSED` and `WatchUi.KEY_EVENT_TYPE_RELEASED` or maybe `WatchUi.KEY_PRESSED`. Let me search memory... I recall seeing code like:

```monkey-c
if (keyEvent.getType() == WatchUi.KEY_EVENT_TYPE_PRESSED) { ... }
```

Yes, I believe it's `WatchUi.KEY_EVENT_TYPE_PRESSED` and `WatchUi.KEY_EVENT_TYPE_RELEASED`. But for compatibility I can just check `getType() == 0` for pressed? No, better to use named constants.

Actually, the original code didn't check type at all. The existing `onKey` is only called on press in many cases. If we add release handling, we need the constants.

Let me use `WatchUi.KEY_EVENT_TYPE_PRESSED` and `WatchUi.KEY_EVENT_TYPE_RELEASED`. If those don't exist in API 1.4.0, we'll need alternatives. Let me verify via web search or docs. But I can't web search right now with tools? Actually I can use web_explore.

Wait, I should just use a simpler approach: only handle press events and don't do long-press at all. Instead, open settings with a different button combination. But the user specifically asked for long-press BACK.

Alternatively, use `KEY_PING` events? Hmm.

Let me search the Connect IQ docs quickly.

Question: Monkey C WatchUi KeyEvent KEY_EVENT_TYPE_PRESSED KEY_EVENT_TYPE_RELEASED