The KeyPressType constants are `WatchUi.PRESS_TYPE_DOWN`, `WatchUi.PRESS_TYPE_UP`, `WatchUi.PRESS_TYPE_ACTION`. These are available since API 1.1.2, so fēnix 3 API 1.4.0 supports them.

However, I notice that `onKey` is described as "A physical button has been pressed and released." So `onKey` might only be called on action (press+release), while `onKeyPressed` and `onKeyReleased` are separate methods. If I override `onKey`, I might not get release events.

Better approach: override `onKeyPressed` and `onKeyReleased` separately. That way I don't need to check event types.

Let me implement with `onKeyPressed` and `onKeyReleased`. The delegate class already overrides `onKey`. I'll switch to overriding press/release.

Actually, the existing code overrides `onKey`. If I switch to `onKeyPressed`/`onKeyReleased`, I need to remove `onKey` or keep it. Let me use `onKeyPressed` and `onKeyReleased` for the long-press detection.

Implementation in BreathHoldDelegate:

```monkey-c
using Toybox.WatchUi;
using Toybox.System;
using Toybox.Timer;

class BreathHoldDelegate extends WatchUi.InputDelegate {

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

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

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

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

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

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

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            if (view.state == view.STATE_IDLE) {
                backTimer = new Timer.Timer();
                backTimer.start(method(:onBackLongPress), LONG_PRESS_MS, false);
            } else {
                view.discardSession();
                view.state = view.STATE_IDLE;
                WatchUi.requestUpdate();
            }
            return true;
        }

        return false;
    }

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

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            if (backTimer != null) {
                backTimer.stop();
                backTimer = null;
                // Short press: do nothing, because long-press handler would have done something if held long enough
                // But if released before long press, the user just tapped BACK in idle. Original behavior was System.exit().
                System.exit();
            }
            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);
        }
    }
}
```

Wait, there's a subtle issue: in `onKeyPressed` for BACK during idle, we start a timer but don't exit the app. Then on release, if timer still exists, it's a short press and we exit. If timer fired (long press), it pushed settings view. Then on release, `backTimer` is null, so we don't exit. But the settings view's delegate handles BACK.

But wait, when long press fires and pushes settings, the BreathHoldView is still underneath. The `backTimer` was set to null. When user releases BACK, `onKeyReleased` sees backTimer null and does nothing. Good.

But what if the user accidentally presses BACK during idle intending to exit? With long press they now have to hold it. Short press exits. That's acceptable, though slightly different from original behavior. Original: short BACK in idle exited. New: short BACK in idle exits; long BACK opens settings.

However, I need to be careful: when settings is open and user presses BACK, the PoolLengthDelegate pops the view. After pop, the BreathHoldDelegate's `onKeyReleased` might still receive the release event? Actually, when settings is pushed, the settings delegate handles input. The BreathHoldDelegate is not active. When settings is popped, we return to BreathHoldView with its delegate. The release event already happened while settings was active, so it won't be delivered to BreathHoldDelegate. Good.

But there's a concern: when long press fires and pushes settings, the BACK button is still physically held. The settings view might immediately receive the same BACK press/release and pop itself. That could be a problem. To avoid this, maybe after long-press triggers settings, we should ignore the subsequent release.

Actually, since settings delegate will handle its own BACK, and the user is still holding BACK, the settings delegate will get KEY_PRESSED for BACK (because the key is still down when settings becomes active). Hmm, actually the press event was already consumed by BreathHoldDelegate. The release event will be delivered to whichever delegate is active at release time. If settings is active, it will get the release and might treat it as a short press and pop. That's bad.

To mitigate, in the long-press handler we could set a flag that the next BACK release should be ignored. But we can't easily communicate that to the settings delegate. Alternatively, we could require a minimum delay or just accept this edge case.

Maybe a simpler approach: don't use long-press for settings. Instead use a different button. But the user explicitly asked for long-press BACK.

Alternative: Use `WatchUi.BehaviorDelegate` which has `onBack()` and maybe `onMenu()`. But BehaviorDelegate requires API 1.2.0+ and may have different semantics. Actually BehaviorDelegate might handle BACK specially. But for watch apps, InputDelegate is fine.

Let me think differently. The long-press detection can be done by counting repeat events. Actually `onKeyPressed` gets called repeatedly when a key is held (with repeat count). We could detect long press by checking `keyEvent.getRepeatCount()`? No, repeat count is for auto-repeat, not for initial press duration.

Actually, there might be a simpler way: use `onKey` which is called on press+release. If we handle BACK in `onKey` only when it's a short press, and use a timer for long press... same problem.

OK, maybe long-press is tricky. Let me reconsider the user requirement. The user said "long press back from idle". But perhaps we can interpret this as: when in idle, BACK short = exit, BACK long = settings. Implementation with timer is the right approach. The edge case of settings popping on release is real but maybe manageable if the settings delegate ignores a release that happens too quickly after view appears.

Actually, a better approach: in `onBackLongPress`, we could stop the timer and push settings. We can pass a flag to PoolLengthDelegate telling it to ignore the next BACK release. But that's complex.

Alternatively, use `switchToView` instead of `pushView` for settings. That replaces the BreathHoldView entirely. Then BACK release goes to settings delegate, which can pop back to BreathHoldView. But then we need to recreate BreathHoldView state. That's also complex.

Maybe simplest: keep `pushView`, and in PoolLengthDelegate, ignore any BACK event that occurs within e.g. 500ms of view creation. This gives the user time to release the button before settings responds.

```monkey-c
class PoolLengthDelegate extends WatchUi.InputDelegate {
    var view;
    var createdTime;

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

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

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            if (System.getTimer() - createdTime < 500) {
                return true; // ignore quick BACK after view appears
            }
            WatchUi.popView(WatchUi.SLIDE_RIGHT);
            return true;
        }
        ...
    }
}
```

This is a reasonable workaround.

Actually, looking more carefully: when we push a new view while the key is held, does the new delegate receive a new PRESSED event? I think the input system delivers events to the active delegate at the time of the event. Since the press event was already handled by BreathHoldDelegate, the settings delegate won't get a duplicate press. It will only get the release. So ignoring the first 500ms of release events in settings should work.

Let me implement this way.

Now for the Settings class and persistence. Since fēnix 3 doesn't have Application.Storage, I'll use static vars in a Settings class. Actually, I can also try to use `Application.Properties` if available, but that's also API 2.4.0+. So static vars it is.

Wait, I recall `Application.getApp().setProperty()` / `getProperty()` are part of `AppBase` and might be available in API 1.4.0? The ObjectStore sample shows `app.getProperty()` but that's the old object store, deprecated in 4.0. In API 1.4.0, there might be `Application.Properties`? Let me check.

Actually, `Application.Properties` was introduced in API 2.4.0 to replace ObjectStore. The old `Application.getApp().getProperty()` might still exist in 1.4.0 as part of AppBase.

Looking at the ObjectStore sample from the forum (which is a device app), it uses `app.getProperty($.PROP_NUMBER)` in API 1.4.0-ish. The sample says object store is deprecated in 4.0. So in 1.4.0, `getProperty`/`setProperty` should work.

Let me use `Application.getApp().getProperty()` and `setProperty()` for persistence. If available, it persists between launches. The object store uses string keys.

Actually, I should verify: `AppBase.getProperty()` and `setProperty()` are documented as part of Application module. In API 1.4.0, they exist.

Let me use them with a fallback if null. In BreathHoldApp, I can load on start and save on change.

Actually, using static Settings class is simpler and avoids dealing with object store. But persistence is a nice-to-have. The plan says "Use Application.Storage if available; otherwise keep in-memory". Since 1.4.0 doesn't have Storage, but it might have ObjectStore (getProperty/setProperty), I can use that.

Let me check: in Monkey C, `Application.AppBase` has `getProperty()` and `setProperty()` which access the object store. These are available from API 1.0.0? I believe yes.

OK, I'll use a `Settings` class with static vars and methods that read/write to `Application.getApp()` properties when available.

Actually, to keep it simple and minimize risk, I'll just use static class vars in `Settings.mc`. The plan says in-memory fallback is acceptable. We can add persistence later.

But wait, the user might want pool length to persist. It's a small nice-to-have. Let me use `Application.getApp()` properties.

```monkey-c
class Settings {
    static var poolLengthM = 25;
    static var poolLengthIndex = 0;

    static function load() {
        var app = Application.getApp();
        var storedLength = app.getProperty("poolLengthM");
        var storedIndex = app.getProperty("poolLengthIndex");
        if (storedLength != null && storedIndex != null) {
            poolLengthM = storedLength;
            poolLengthIndex = storedIndex;
        }
    }

    static function save() {
        var app = Application.getApp();
        app.setProperty("poolLengthM", poolLengthM);
        app.setProperty("poolLengthIndex", poolLengthIndex);
    }
}
```

Then in BreathHoldApp.onStart, call `Settings.load()`.

Now for the FIT fields. Need to add in `startSession()`:
- `horizontal_distance_m` as UINT16, units "m"
- `length_count` as UINT8, units "laps"

In `finishSession()`, set data.

Note: The current code uses developer field IDs 0 and 1. I need to add new fields with IDs 2 and 3.

Also, in `resetRunState()`, reset lengthCount and horizontalDistanceM. But wait, the done screen shows maxHoldTime. After a new session starts, we reset.

For display: I need to show LAPS/DIST. The screen is 218x218 on fēnix 3. Current layout has title, timer, HR, info. I can add another line. Maybe replace HR line during hold with DIST? Or add below. Let me add a line between timer and HR, or combine HR and DIST.

Current y positions: title at y=16, timer at y+28=44, HR at y+88=104, info at y+115=131.

218 height, so bottom is around 200. We have room for one more line. Let me put LAPS/DIST at y+62 = 78, between timer and HR.

Actually, for hold phase, show:
- Title: HOLD
- Timer
- LAPS/DIST
- Contractions
- HR maybe small

The fēnix 3 screen is small. Let me keep it minimal:
- Title (small)
- Timer (medium number)
- Info line: "LAPS 3  DIST 75m  CONT 5" or similar

I'll add a dedicated `lapInfoText` variable and draw it.

Let me implement the code now.

Files to create:
1. `source/Settings.mc`
2. `source/PoolLengthView.mc`
3. `source/PoolLengthDelegate.mc`

Files to modify:
1. `source/BreathHoldApp.mc` - add Settings.load() in onStart
2. `source/BreathHoldDelegate.mc` - add long-press BACK, button routing
3. `source/BreathHoldView.mc` - add pool length state, length counting, display, FIT fields
4. `README.md` - update usage
5. `IMPLEMENTATION_PLAN.md` - mark status implemented

Let me write the code.

First, Settings.mc:

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

    static function load() {
        var app = Application.getApp();
        var storedLength = app.getProperty("poolLengthM");
        var storedIndex = app.getProperty("poolLengthIndex");
        if (storedLength != null && storedIndex != null) {
            poolLengthM = storedLength;
            poolLengthIndex = storedIndex;
        }
    }

    static function save() {
        var app = Application.getApp();
        app.setProperty("poolLengthM", poolLengthM);
        app.setProperty("poolLengthIndex", poolLengthIndex);
    }
}
```

Wait, Application.getApp() returns Object. We need to import Toybox.Application. Actually since we call app.getProperty, and AppBase has that method, it should work at runtime. But the compiler might complain because Object doesn't have getProperty. In Monkey C, method lookup is dynamic? Actually, Monkey C uses optional typing. If we don't declare a type, it might work. But with `var app = Application.getApp();` and then `app.getProperty(...)`, the compiler may not know the type. However, in Monkey C, untyped variables allow any method call? I think so, but it may generate warnings or errors.

Better: cast or use `Application.getApp() as Application.AppBase`. Actually in Monkey C, you can write:
```monkey-c
var app = Application.getApp() as Application.AppBase;
```

Or simply not type it and rely on dynamic dispatch.

Actually, looking at the ObjectStore sample: `var app = Application.getApp();` and then `app.getProperty(...)` is used. So this works.

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;
    var createdTime;

    function initialize() {
        View.initialize();
        createdTime = System.getTimer();
        selectedIndex = Settings.poolLengthIndex;
        customValue = Settings.poolLengthM;
        if (selectedIndex < PRESET_25 || selectedIndex > PRESET_CUSTOM) {
            selectedIndex = PRESET_25;
        }
    }

    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 onUp() {
        if (selectedIndex == PRESET_CUSTOM && inCustomEdit) {
            customValue++;
            if (customValue > 200) { customValue = 200; }
        } else {
            selectedIndex--;
            if (selectedIndex < PRESET_25) { selectedIndex = PRESET_25; }
        }
        WatchUi.requestUpdate();
    }

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

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

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

Wait, user said UP/DOWN cycles presets. With my implementation, UP decreases index (25 -> 25), DOWN increases (25 -> 50 -> custom). That's a bit unintuitive because UP usually means next/previous. Let me make it simpler:
- UP: previous preset (25 -> 50 -> custom? or reverse)
- DOWN: next preset

Actually, it doesn't matter much as long as consistent. Let me use:
- UP: move to larger value? No, on a list displayed vertically, UP usually moves up in the list. We don't display a list, just a value.
- Let me use DOWN to go 25 -> 50 -> custom, UP to go back. That feels natural: pressing DOWN cycles forward through options.

Wait, in my code above, `onUp` decreases index and `onDown` increases index. So DOWN cycles 25 -> 50 -> custom. Good. But if user is at custom and in edit mode, UP increases value and DOWN decreases value. That's also natural (UP = more, DOWN = less).

Actually no: in edit mode, I had `onUp` increment customValue and `onDown` decrement. That means UP = larger pool, DOWN = smaller pool. That's fine.

But for preset cycling, I had `onUp` decrease index (so from 50 to 25), and `onDown` increase index (25 to 50 to custom). So DOWN moves forward. That might be fine.

Wait, I think I made a mistake. In `onUp` for preset: `selectedIndex--;` then clamp to min. So from 25 -> 25, from 50 -> 25, from custom -> 50. In `onDown`: `selectedIndex++;` clamp to max. So 25 -> 50, 50 -> custom, custom -> custom.

That means UP goes to previous (lower index), DOWN goes to next (higher index). With values 25, 50, custom, DOWN increases value. That's OK.

PoolLengthDelegate.mc:

```monkey-c
using Toybox.WatchUi;
using Toybox.System;

class PoolLengthDelegate extends WatchUi.InputDelegate {

    var view;

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

    function onKeyPressed(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;
        }

        return false;
    }

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

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            // Ignore a release that happens immediately after the view appears,
            // because the user may still be releasing a long-press BACK from idle.
            if (System.getTimer() - view.createdTime < 500) {
                return true;
            }
            WatchUi.popView(WatchUi.SLIDE_RIGHT);
            return true;
        }

        return false;
    }
}
```

Wait, I need to import System in this file too.

BreathHoldApp.mc changes:
- Add `Settings.load();` in `onStart`.

```monkey-c
using Toybox.Application;
using Toybox.WatchUi;
using Toybox.Sensor;

class BreathHoldApp extends Application.AppBase {

    function initialize() {
        AppBase.initialize();
    }

    function onStart(state) {
        Settings.load();
        if (Sensor has :setEnabledSensors) {
            Sensor.setEnabledSensors([Sensor.SENSOR_HEARTRATE]);
        }
    }
    ...
}
```

Wait, need to make sure Settings class is visible. Since all classes are in the default package, they're visible to each other.

BreathHoldDelegate.mc:

```monkey-c
using Toybox.WatchUi;
using Toybox.System;
using Toybox.Timer;

class BreathHoldDelegate extends WatchUi.InputDelegate {

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

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

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

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

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

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

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            if (view.state == view.STATE_IDLE) {
                backTimer = new Timer.Timer();
                backTimer.start(method(:onBackLongPress), LONG_PRESS_MS, false);
            } else {
                view.discardSession();
                view.state = view.STATE_IDLE;
                WatchUi.requestUpdate();
            }
            return true;
        }

        return false;
    }

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

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            if (backTimer != null) {
                backTimer.stop();
                backTimer = null;
                // Short press on BACK from idle: exit the app.
                System.exit();
            }
            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);
        }
    }
}
```

Wait, there's a problem: `onKeyPressed` returns Boolean. In Monkey C, method signatures matter. `onKeyPressed(keyEvent)` should match `onKeyPressed(keyEvent as WatchUi.KeyEvent) as Lang.Boolean`. The return type should be Boolean. My returns are `true`/`false` which are Boolean literals. OK.

But the method parameter type: if I don't specify `as WatchUi.KeyEvent`, it might be `Object` or `Dictionary`. Better to specify type? Actually in Monkey C, the override just needs to match by name and parameter count; types are checked loosely. But to be safe, I can write `function onKeyPressed(keyEvent as WatchUi.KeyEvent) as Lang.Boolean`.

Actually, looking at existing code: `function onKey(keyEvent) { ... return true; }` without types. So I'll keep it consistent without types.

But wait, `InputDelegate.onKey` returns Boolean. The existing method returns `true`/`false`. OK.

Now BreathHoldView.mc changes:
- Add imports? Already imports WatchUi, Graphics, System, Lang, Timer, ActivityRecording, FitContributor, Sensor.
- Add fields: `lengthCount`, `horizontalDistanceM`, `distanceField`, `lengthField`.
- Initialize `lengthCount = 0` etc in initialize and resetRunState.
- In `onUpdate`, display pool length in idle, and laps/distance during session.
- Add `onLengthButton()`.
- In `startSession()`, create distance and length fields.
- In `finishSession()`, set their data.
- In `resetRunState()`, reset lengthCount and horizontalDistanceM.

Display layout:
- Idle: show "POOL 25m" and "HOLD BACK = settings"
- Prep: maybe just timer
- Hold: show LAPS/DIST/CONT info
- Recovery: show final LAPS/DIST
- Done: show max hold + distance

Let me design the info lines carefully. fēnix 3 screen is 218x218. FONT_TINY is small.

For idle:
```
statusText = "FREEDIVE"
timeText = "Press START"
infoText = "POOL " + Settings.poolLengthM + "m  HOLD BACK"
```

For prep:
```
statusText = "PREP BREATHE"
timeText = formatTime(prepTime - currentTime)
infoText = "START = skip"
```

For hold:
```
statusText = "HOLD"
timeText = formatTime(currentTime)
infoText = "UP=LAP " + lengthCount + "  DOWN=CONT " + contractionCount
```
Or show distance too:
```
infoText = "LAPS " + lengthCount + " DIST " + horizontalDistanceM + "m"
```
And where to put contractions? Maybe combine:
```
infoText = "L" + lengthCount + " C" + contractionCount + " D" + horizontalDistanceM + "m"
```
That's compact.

For recovery:
```
statusText = "RECOVERY"
timeText = formatTime(recoveryTime - currentTime)
infoText = "L" + lengthCount + " C" + contractionCount + " D" + horizontalDistanceM + "m"
```

For done:
```
statusText = "SAVED"
timeText = formatTime(maxHoldTime)
infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m"
```

HR line stays.

For FIT fields:
In `startSession()`:
```monkey-c
distanceField = session.createField(
    "horizontal_distance_m",
    2,
    FitContributor.DATA_TYPE_UINT16,
    {:mesgType => Fit.MESG_TYPE_SESSION, :units => "m"}
);

lengthField = session.createField(
    "length_count",
    3,
    FitContributor.DATA_TYPE_UINT8,
    {:mesgType => Fit.MESG_TYPE_SESSION, :units => "laps"}
);
```

In `finishSession()`:
```monkey-c
if (distanceField != null) {
    distanceField.setData(horizontalDistanceM);
}
if (lengthField != null) {
    lengthField.setData(lengthCount);
}
```

Wait, `horizontalDistanceM` could exceed UINT16 (65535) if pool length 200m and 327 laps. But realistically for a session, no. Still, use UINT16 and cap if needed. Actually max UINT16 is 65535m which is fine.

Also, `horizontalDistanceM` is calculated from `lengthCount * Settings.poolLengthM`. Need to make sure it's int.

Reset:
```monkey-c
function resetRunState() {
    state = STATE_IDLE;
    currentTime = 0;
    maxHoldTime = 0;
    contractionCount = 0;
    lengthCount = 0;
    horizontalDistanceM = 0;
    lastHeartRate = null;
}
```

But I should not reset `lengthCount` in `onStartButton` for new session? Yes, `resetRunState` is called when starting a new session from done state. So it resets. Good.

`onLengthButton()`:
```monkey-c
function onLengthButton() {
    if (state == STATE_HOLD || state == STATE_RECOVERY) {
        lengthCount++;
        horizontalDistanceM = lengthCount * Settings.poolLengthM;
        WatchUi.requestUpdate();
    }
}
```

Hmm, but if we're in recovery and count a length, the distance updates. Fine.

Wait, should UP count length during recovery? The plan says "during hold/recovery". OK.

But actually, counting lengths during recovery doesn't make sense practically. Let me restrict to hold only? The user said "i press manually when to count" — that implies during the breath-hold when swimming. Let's keep it during hold only to avoid accidental counts during recovery.

Actually, in recovery the user is breathing, not swimming. So UP during recovery should not count. Let me change to hold only.

But what if the user surfaces and is in recovery, then remembers to count? They would have to count during the hold. That's fine.

OK, UP counts length only during hold. DOWN counts contractions only during hold.

Now for idle display of pool length. Need to import Settings in BreathHoldView. It's already in default namespace.

Actually, `Settings.poolLengthM` will be accessible.

Let me write the edits.

One more thing: the `Settings` class uses `Application.getApp()` which returns `Object`. In Monkey C, this works for getProperty/setProperty.

Actually, I realize I should check if `Application.getApp()` is accessible from a non-Application class. Yes, it's a static method on Application module.

Now let me also consider: when BreathHoldApp.onStart calls Settings.load(), if this is the first start, it loads defaults. Good.

Now, potential compiler issue: using arrays in PoolLengthView: `const PRESET_VALUES = [25, 50];` In Monkey C, array literals are fine.

Also, `PRESET_VALUES[selectedIndex]` where selectedIndex is Number. OK.

Now, I should also think about the `createdTime` field. I declared it in PoolLengthView. The delegate accesses `view.createdTime`. OK.

Let me now write all the files.

Actually, one more concern: `WatchUi.KEY_ESC` vs `WatchUi.KEY_LAP`. The original code uses both. The BACK button on fēnix 3 is KEY_ESC (or KEY_LAP?). The original code checks both. I'll keep both.

Also, `WatchUi.KEY_ENTER` is the same as START? The original code checks both KEY_START and KEY_ENTER. I'll keep.

Now, I need to be careful with `function onKeyPressed` and `function onKeyReleased` overriding. The InputDelegate has these methods. When overriding, should I specify return type? The existing `onKey` doesn't. Let me check if Monkey C requires matching signatures exactly or just by name.

In Monkey C, method override uses duck typing mostly. But for delegate callbacks, the runtime looks for the method by name. So `function onKeyPressed(keyEvent)` should be called.

Actually, I realize the existing code overrides `onKey`. If I also override `onKeyPressed`/`onKeyReleased`, will both be called? `onKey` is called for press+release action, `onKeyPressed` for down, `onKeyReleased` for up. I think they're independent. If I remove `onKey` and only use press/release, I need to handle START/UP/DOWN in press.

But wait, the original code used `onKey` and it worked. Why change? Because I need separate press/release for BACK long press. For START/UP/DOWN, I can use `onKey` (action) or `onKeyPressed`. Using `onKeyPressed` for all is fine.

However, I need to be careful: if I override both `onKey` and `onKeyPressed`/`onKeyReleased`, the runtime might call `onKey` for action and also `onKeyPressed`/`onKeyReleased`. To avoid double handling, I should only use `onKeyPressed`/`onKeyReleased`.

So I'll replace `onKey` with `onKeyPressed` and `onKeyReleased`.

But the START button action: does `onKeyPressed` fire once when pressed? Yes. That means the user gets immediate response on press, not on release. That's fine.

Actually, I just realized: the existing code uses `onKey`, which fires on action (release typically). If I switch to `onKeyPressed`, the app becomes more responsive. That's probably fine.

But for the pool length picker, I also use `onKeyPressed`/`onKeyReleased`.

Let me write it.

One issue: In Monkey C, if a method doesn't return a value but the base method returns Boolean, it might return null which is falsy. I should return true/false explicitly.

OK, now implementing.

I'll create the new files with write tool and edit existing files.

Let me also check the README to update.

And update IMPLEMENTATION_PLAN status.

Let me proceed.

write

using Toybox.Application;

class Settings {
    static var poolLengthM = 25;
    static var poolLengthIndex = 0; // 0=25m, 1=50m, 2=custom

    function load() {
        var app = Application.getApp();
        var storedLength = app.getProperty("poolLengthM");
        var storedIndex = app.getProperty("poolLengthIndex");
        if (storedLength != null && storedIndex != null) {
            poolLengthM = storedLength;
            poolLengthIndex = storedIndex;
        }
    }

    function save() {
        var app = Application.getApp();
        app.setProperty("poolLengthM", poolLengthM);
        app.setProperty("poolLengthIndex", poolLengthIndex);
    }
}


Successfully wrote 666 bytes to ~/code/garmin-freedive-app/source/Settings.mc


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;
    var createdTime;

    function initialize() {
        View.initialize();
        createdTime = System.getTimer();
        selectedIndex = Settings.poolLengthIndex;
        customValue = Settings.poolLengthM;
        if (selectedIndex < PRESET_25 || selectedIndex > PRESET_CUSTOM) {
            selectedIndex = PRESET_25;
        }
    }

    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 onUp() {
        if (selectedIndex == PRESET_CUSTOM && inCustomEdit) {
            customValue++;
            if (customValue > 200) { customValue = 200; }
        } else {
            selectedIndex--;
            if (selectedIndex < PRESET_25) { selectedIndex = PRESET_25; }
        }
        WatchUi.requestUpdate();
    }

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

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

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


Successfully wrote 3051 bytes to ~/code/garmin-freedive-app/source/PoolLengthView.mc


using Toybox.WatchUi;
using Toybox.System;

class PoolLengthDelegate extends WatchUi.InputDelegate {

    var view;

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

    function onKeyPressed(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;
        }

        return false;
    }

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

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            // Ignore a release that happens immediately after the view appears,
            // because the user may still be releasing a long-press BACK from idle.
            if (System.getTimer() - view.createdTime < 500) {
                return true;
            }
            WatchUi.popView(WatchUi.SLIDE_RIGHT);
            return true;
        }

        return false;
    }
}


Successfully wrote 1171 bytes to ~/code/garmin-freedive-app/source/PoolLengthDelegate.mc


edit

    function onStart(state) {
        Settings.load();

        // Enable the ANT+/on-board heart rate sensor.

    function onStart(state) {
        // Enable the ANT+/on-board heart rate sensor.


Successfully replaced 1 block(s) in ~/code/garmin-freedive-app/source/BreathHoldApp.mc.

    ...
  8         AppBase.initialize();
  9     }
 10
 11     function onStart(state) {
+12         Settings.load();
+13
 12         // Enable the ANT+/on-board heart rate sensor.
 13         if (Sensor has :setEnabledSensors) {
 14             Sensor.setEnabledSensors([Sensor.SENSOR_HEARTRATE]);
 15         }
    ...

--- ~/code/garmin-freedive-app/source/BreathHoldApp.mc
+++ ~/code/garmin-freedive-app/source/BreathHoldApp.mc
@@ -8,8 +8,10 @@
         AppBase.initialize();
     }

     function onStart(state) {
+        Settings.load();
+
         // Enable the ANT+/on-board heart rate sensor.
         if (Sensor has :setEnabledSensors) {
             Sensor.setEnabledSensors([Sensor.SENSOR_HEARTRATE]);
         }


using Toybox.WatchUi;
using Toybox.System;
using Toybox.Timer;

class BreathHoldDelegate extends WatchUi.InputDelegate {

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

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

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

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

        if (key == WatchUi.KEY_UP) {
            // UP counts a completed pool length during the hold.
            if (view.state == view.STATE_HOLD) {
                view.onLengthButton();
            }
            return true;
        }

        if (key == WatchUi.KEY_DOWN) {
            // DOWN counts a contraction during the hold.
            if (view.state == view.STATE_HOLD) {
                view.onContractionButton();
            }
            return true;
        }

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            if (view.state == view.STATE_IDLE) {
                // Start long-press detection; if released early we exit the app.
                backTimer = new Timer.Timer();
                backTimer.start(method(:onBackLongPress), LONG_PRESS_MS, false);
            } else {
                view.discardSession();
                view.state = view.STATE_IDLE;
                WatchUi.requestUpdate();
            }
            return true;
        }

        return false;
    }

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

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            if (backTimer != null) {
                // Released before the long-press threshold: short press, exit the app.
                backTimer.stop();
                backTimer = null;
                System.exit();
            }
            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);
        }
    }
}

using Toybox.WatchUi;
using Toybox.System;

class BreathHoldDelegate extends WatchUi.InputDelegate {

    var view;

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

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

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

        if (key == WatchUi.KEY_UP || key == WatchUi.KEY_DOWN) {
            view.onContractionButton();
            return true;
        }

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            if (view.state == view.STATE_IDLE) {
                System.exit();
            } else {
                view.discardSession();
                view.state = view.STATE_IDLE;
                WatchUi.requestUpdate();
            }
            return true;
        }

        return false;
    }
}


Successfully replaced 1 block(s) in ~/code/garmin-freedive-app/source/BreathHoldDelegate.mc.

  1 using Toybox.WatchUi;
  2 using Toybox.System;
+ 3 using Toybox.Timer;
  3
  4 class BreathHoldDelegate extends WatchUi.InputDelegate {
  5
  6     var view;
+ 8     var backTimer = null;
+ 9     const LONG_PRESS_MS = 800;
  7
  8     function initialize(v) {
  9         InputDelegate.initialize();
 10         view = v;
 11     }
 12
-13     function onKey(keyEvent) {
+16     function onKeyPressed(keyEvent) {
 14         var key = keyEvent.getKey();
 15
 16         if (key == WatchUi.KEY_START || key == WatchUi.KEY_ENTER) {
 17             view.onStartButton();
 18             return true;
 19         }
 20
-21         if (key == WatchUi.KEY_UP || key == WatchUi.KEY_DOWN) {
-22             view.onContractionButton();
+24         if (key == WatchUi.KEY_UP) {
+25             // UP counts a completed pool length during the hold.
+26             if (view.state == view.STATE_HOLD) {
+27                 view.onLengthButton();
+28             }
 23             return true;
 24         }
 25
+32         if (key == WatchUi.KEY_DOWN) {
+33             // DOWN counts a contraction during the hold.
+34             if (view.state == view.STATE_HOLD) {
+35                 view.onContractionButton();
+36             }
+37             return true;
+38         }
+39
 26         if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
 27             if (view.state == view.STATE_IDLE) {
-28                 System.exit();
+42                 // Start long-press detection; if released early we exit the app.
+43                 backTimer = new Timer.Timer();
+44                 backTimer.start(method(:onBackLongPress), LONG_PRESS_MS, false);
 29             } else {
 30                 view.discardSession();
 31                 view.state = view.STATE_IDLE;
 32                 WatchUi.requestUpdate();
    ...
 35         }
 36
 37         return false;
 38     }
+55
+56     function onKeyReleased(keyEvent) {
+57         var key = keyEvent.getKey();
+58
+59         if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
+60             if (backTimer != null) {
+61                 // Released before the long-press threshold: short press, exit the app.
+62                 backTimer.stop();
+63                 backTimer = null;
+64                 System.exit();
+65             }
+66             return true;
+67         }
+68
+69         return false;
+70     }
+71
+72     function onBackLongPress() {
+73         backTimer = null;
+74         if (view.state == view.STATE_IDLE) {
+75             WatchUi.pushView(new PoolLengthView(), new PoolLengthDelegate(new PoolLengthView()), WatchUi.SLIDE_LEFT);
+76         }
+77     }
 39 }

--- ~/code/garmin-freedive-app/source/BreathHoldDelegate.mc
+++ ~/code/garmin-freedive-app/source/BreathHoldDelegate.mc
@@ -1,32 +1,48 @@
 using Toybox.WatchUi;
 using Toybox.System;
+using Toybox.Timer;

 class BreathHoldDelegate extends WatchUi.InputDelegate {

     var view;
+    var backTimer = null;
+    const LONG_PRESS_MS = 800;

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

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

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

-        if (key == WatchUi.KEY_UP || key == WatchUi.KEY_DOWN) {
-            view.onContractionButton();
+        if (key == WatchUi.KEY_UP) {
+            // UP counts a completed pool length during the hold.
+            if (view.state == view.STATE_HOLD) {
+                view.onLengthButton();
+            }
             return true;
         }

+        if (key == WatchUi.KEY_DOWN) {
+            // DOWN counts a contraction during the hold.
+            if (view.state == view.STATE_HOLD) {
+                view.onContractionButton();
+            }
+            return true;
+        }
+
         if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
             if (view.state == view.STATE_IDLE) {
-                System.exit();
+                // Start long-press detection; if released early we exit the app.
+                backTimer = new Timer.Timer();
+                backTimer.start(method(:onBackLongPress), LONG_PRESS_MS, false);
             } else {
                 view.discardSession();
                 view.state = view.STATE_IDLE;
                 WatchUi.requestUpdate();
@@ -35,5 +51,28 @@
         }

         return false;
     }
+
+    function onKeyReleased(keyEvent) {
+        var key = keyEvent.getKey();
+
+        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
+            if (backTimer != null) {
+                // Released before the long-press threshold: short press, exit the app.
+                backTimer.stop();
+                backTimer = null;
+                System.exit();
+            }
+            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);
+        }
+    }
 }


using Toybox.WatchUi;
using Toybox.Graphics;
using Toybox.System;
using Toybox.Lang;
using Toybox.Timer;
using Toybox.ActivityRecording;
using Toybox.FitContributor;
using Toybox.Sensor;

class BreathHoldView extends WatchUi.View {

    // App states
    const STATE_IDLE     = 0;
    const STATE_PREP     = 1;
    const STATE_HOLD     = 2;
    const STATE_RECOVERY = 3;
    const STATE_DONE     = 4;

    var state = STATE_IDLE;
    var timer = null;

    // FIT session
    var session = null;
    var holdField = null;
    var hrField = null;

    // Breath-hold configuration (seconds)
    var prepTime = 120;
    var recoveryTime = 120;

    // Runtime counters
    var currentTime = 0;
    var maxHoldTime = 0;
    var contractionCount = 0;
    var lastHeartRate = null;

    function initialize() {
        View.initialize();
    }

    function onShow() {
        timer = new Timer.Timer();
    }

    function onHide() {
        if (timer != null) {
            timer.stop();
        }
    }

    function onUpdate(dc) {
        // Clear screen
        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;

        var statusText = "";
        var timeText = "";
        var infoText = "";

        if (state == STATE_IDLE) {
            statusText = "FREEDIVE";
            timeText = "Press START";
            infoText = "Back = exit";
        } else if (state == STATE_PREP) {
            statusText = "PREP BREATHE";
            timeText = formatTime(prepTime - currentTime);
            infoText = "START = skip to hold";
        } else if (state == STATE_HOLD) {
            statusText = "HOLD";
            timeText = formatTime(currentTime);
            infoText = "Contractions: " + contractionCount;
        } else if (state == STATE_RECOVERY) {
            statusText = "RECOVERY";
            timeText = formatTime(recoveryTime - currentTime);
            infoText = "START = finish";
        } else if (state == STATE_DONE) {
            statusText = "SAVED";
            timeText = formatTime(maxHoldTime);
            infoText = "START = new session";
        }

        // Title
        dc.drawText(cx, y, Graphics.FONT_SMALL, statusText, Graphics.TEXT_JUSTIFY_CENTER);

        // Main timer
        dc.drawText(cx, y + 28, Graphics.FONT_NUMBER_MEDIUM, timeText, Graphics.TEXT_JUSTIFY_CENTER);

        // Heart rate
        var hrStr = "--";
        if (lastHeartRate != null) {
            hrStr = lastHeartRate.toString();
        }
        dc.drawText(cx, y + 88, Graphics.FONT_SMALL, "HR " + hrStr, Graphics.TEXT_JUSTIFY_CENTER);

        // Info / instruction line
        dc.drawText(cx, y + 115, Graphics.FONT_TINY, infoText, Graphics.TEXT_JUSTIFY_CENTER);
    }

    function formatTime(seconds) {
        if (seconds < 0) {
            seconds = 0;
        }
        var m = seconds / 60;
        var s = seconds % 60;
        return m.format("%02d") + ":" + s.format("%02d");
    }

    // Called once per second by the Timer
    function onTimer() {
        if (state == STATE_PREP) {
            currentTime++;
            if (currentTime >= prepTime) {
                state = STATE_HOLD;
                currentTime = 0;
            }
        } else if (state == STATE_HOLD) {

[127 more lines in file. Use offset=121 to continue.]


    // FIT session
    var session = null;
    var holdField = null;
    var hrField = null;
    var distanceField = null;
    var lengthField = null;

    // Breath-hold configuration (seconds)
    var prepTime = 120;
    var recoveryTime = 120;

    // Runtime counters
    var currentTime = 0;
    var maxHoldTime = 0;
    var contractionCount = 0;
    var lengthCount = 0;
    var horizontalDistanceM = 0;
    var lastHeartRate = null;

    function initialize() {
        View.initialize();
        lengthCount = 0;
        horizontalDistanceM = 0;
    }

    // FIT session
    var session = null;
    var holdField = null;
    var hrField = null;

    // Breath-hold configuration (seconds)
    var prepTime = 120;
    var recoveryTime = 120;

    // Runtime counters
    var currentTime = 0;
    var maxHoldTime = 0;
    var contractionCount = 0;
    var lastHeartRate = null;

    function initialize() {
        View.initialize();
    }


Successfully replaced 1 block(s) in ~/code/garmin-freedive-app/source/BreathHoldView.mc.

     ...
  22     // FIT session
  23     var session = null;
  24     var holdField = null;
  25     var hrField = null;
+ 26     var distanceField = null;
+ 27     var lengthField = null;
  26
  27     // Breath-hold configuration (seconds)
  28     var prepTime = 120;
  29     var recoveryTime = 120;
     ...
  31     // Runtime counters
  32     var currentTime = 0;
  33     var maxHoldTime = 0;
  34     var contractionCount = 0;
+ 37     var lengthCount = 0;
+ 38     var horizontalDistanceM = 0;
  35     var lastHeartRate = null;
  36
  37     function initialize() {
  38         View.initialize();
+ 43         lengthCount = 0;
+ 44         horizontalDistanceM = 0;
  39     }
  40
  41     function onShow() {
  42         timer = new Timer.Timer();
     ...

--- ~/code/garmin-freedive-app/source/BreathHoldView.mc
+++ ~/code/garmin-freedive-app/source/BreathHoldView.mc
@@ -22,8 +22,10 @@
     // FIT session
     var session = null;
     var holdField = null;
     var hrField = null;
+    var distanceField = null;
+    var lengthField = null;

     // Breath-hold configuration (seconds)
     var prepTime = 120;
     var recoveryTime = 120;
@@ -31,12 +33,16 @@
     // Runtime counters
     var currentTime = 0;
     var maxHoldTime = 0;
     var contractionCount = 0;
+    var lengthCount = 0;
+    var horizontalDistanceM = 0;
     var lastHeartRate = null;

     function initialize() {
         View.initialize();
+        lengthCount = 0;
+        horizontalDistanceM = 0;
     }

     function onShow() {
         timer = new Timer.Timer();


        if (state == STATE_IDLE) {
            statusText = "FREEDIVE";
            timeText = "Press START";
            infoText = "POOL " + Settings.poolLengthM + "m  HOLD BACK";
        } else if (state == STATE_PREP) {
            statusText = "PREP BREATHE";
            timeText = formatTime(prepTime - currentTime);
            infoText = "START = skip to hold";
        } else if (state == STATE_HOLD) {
            statusText = "HOLD";
            timeText = formatTime(currentTime);
            infoText = "UP=LAP " + lengthCount + "  DOWN=CONT " + contractionCount;
        } else if (state == STATE_RECOVERY) {
            statusText = "RECOVERY";
            timeText = formatTime(recoveryTime - currentTime);
            infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m  START=finish";
        } else if (state == STATE_DONE) {
            statusText = "SAVED";
            timeText = formatTime(maxHoldTime);
            infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m";
        }

        if (state == STATE_IDLE) {
            statusText = "FREEDIVE";
            timeText = "Press START";
            infoText = "Back = exit";
        } else if (state == STATE_PREP) {
            statusText = "PREP BREATHE";
            timeText = formatTime(prepTime - currentTime);
            infoText = "START = skip to hold";
        } else if (state == STATE_HOLD) {
            statusText = "HOLD";
            timeText = formatTime(currentTime);
            infoText = "Contractions: " + contractionCount;
        } else if (state == STATE_RECOVERY) {
            statusText = "RECOVERY";
            timeText = formatTime(recoveryTime - currentTime);
            infoText = "START = finish";
        } else if (state == STATE_DONE) {
            statusText = "SAVED";
            timeText = formatTime(maxHoldTime);
            infoText = "START = new session";
        }


     ...
  69
  70         if (state == STATE_IDLE) {
  71             statusText = "FREEDIVE";
  72             timeText = "Press START";
- 73             infoText = "Back = exit";
+ 73             infoText = "POOL " + Settings.poolLengthM + "m  HOLD BACK";
  74         } else if (state == STATE_PREP) {
  75             statusText = "PREP BREATHE";
  76             timeText = formatTime(prepTime - currentTime);
  77             infoText = "START = skip to hold";
  78         } else if (state == STATE_HOLD) {
  79             statusText = "HOLD";
  80             timeText = formatTime(currentTime);
- 81             infoText = "Contractions: " + contractionCount;
+ 81             infoText = "UP=LAP " + lengthCount + "  DOWN=CONT " + contractionCount;
  82         } else if (state == STATE_RECOVERY) {
  83             statusText = "RECOVERY";
  84             timeText = formatTime(recoveryTime - currentTime);
- 85             infoText = "START = finish";
+ 85             infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m  START=finish";
  86         } else if (state == STATE_DONE) {
  87             statusText = "SAVED";
  88             timeText = formatTime(maxHoldTime);
- 89             infoText = "START = new session";
+ 89             infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m";
  90         }
  91
  92         // Title
  93         dc.drawText(cx, y, Graphics.FONT_SMALL, statusText, Graphics.TEXT_JUSTIFY_CENTER);
     ...

--- ~/code/garmin-freedive-app/source/BreathHoldView.mc
+++ ~/code/garmin-freedive-app/source/BreathHoldView.mc
@@ -69,25 +69,25 @@

         if (state == STATE_IDLE) {
             statusText = "FREEDIVE";
             timeText = "Press START";
-            infoText = "Back = exit";
+            infoText = "POOL " + Settings.poolLengthM + "m  HOLD BACK";
         } else if (state == STATE_PREP) {
             statusText = "PREP BREATHE";
             timeText = formatTime(prepTime - currentTime);
             infoText = "START = skip to hold";
         } else if (state == STATE_HOLD) {
             statusText = "HOLD";
             timeText = formatTime(currentTime);
-            infoText = "Contractions: " + contractionCount;
+            infoText = "UP=LAP " + lengthCount + "  DOWN=CONT " + contractionCount;
         } else if (state == STATE_RECOVERY) {
             statusText = "RECOVERY";
             timeText = formatTime(recoveryTime - currentTime);
-            infoText = "START = finish";
+            infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m  START=finish";
         } else if (state == STATE_DONE) {
             statusText = "SAVED";
             timeText = formatTime(maxHoldTime);
-            infoText = "START = new session";
+            infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m";
         }

         // Title
         dc.drawText(cx, y, Graphics.FONT_SMALL, statusText, Graphics.TEXT_JUSTIFY_CENTER);


        // Info / instruction line
        dc.drawText(cx, y + 115, Graphics.FONT_TINY, infoText, Graphics.TEXT_JUSTIFY_CENTER);

        // Pool distance hint on idle and recovery/done screens
        if (state == STATE_IDLE) {
            dc.drawText(cx, y + 140, Graphics.FONT_TINY, "HOLD BACK = settings", Graphics.TEXT_JUSTIFY_CENTER);
        }
    }

    function formatTime(seconds) {

        // Info / instruction line
        dc.drawText(cx, y + 115, Graphics.FONT_TINY, infoText, Graphics.TEXT_JUSTIFY_CENTER);
    }

    function formatTime(seconds) {


     ...
 103         dc.drawText(cx, y + 88, Graphics.FONT_SMALL, "HR " + hrStr, Graphics.TEXT_JUSTIFY_CENTER);
 104
 105         // Info / instruction line
 106         dc.drawText(cx, y + 115, Graphics.FONT_TINY, infoText, Graphics.TEXT_JUSTIFY_CENTER);
+107
+108         // Pool distance hint on idle and recovery/done screens
+109         if (state == STATE_IDLE) {
+110             dc.drawText(cx, y + 140, Graphics.FONT_TINY, "HOLD BACK = settings", Graphics.TEXT_JUSTIFY_CENTER);
+111         }
 107     }
 108
 109     function formatTime(seconds) {
 110         if (seconds < 0) {
     ...

--- ~/code/garmin-freedive-app/source/BreathHoldView.mc
+++ ~/code/garmin-freedive-app/source/BreathHoldView.mc
@@ -103,8 +103,13 @@
         dc.drawText(cx, y + 88, Graphics.FONT_SMALL, "HR " + hrStr, Graphics.TEXT_JUSTIFY_CENTER);

         // Info / instruction line
         dc.drawText(cx, y + 115, Graphics.FONT_TINY, infoText, Graphics.TEXT_JUSTIFY_CENTER);
+
+        // Pool distance hint on idle and recovery/done screens
+        if (state == STATE_IDLE) {
+            dc.drawText(cx, y + 140, Graphics.FONT_TINY, "HOLD BACK = settings", Graphics.TEXT_JUSTIFY_CENTER);
+        }
     }

     function formatTime(seconds) {
         if (seconds < 0) {


    function updateHeartRate() {
        var info = Sensor.getInfo();
        if (info != null && info.heartRate != null) {
            lastHeartRate = info.heartRate;
        }
    }

    function onLengthButton() {
        if (state == STATE_HOLD) {
            lengthCount++;
            horizontalDistanceM = lengthCount * Settings.poolLengthM;
            WatchUi.requestUpdate();
        }
    }

    function startSession() {

    function updateHeartRate() {
        var info = Sensor.getInfo();
        if (info != null && info.heartRate != null) {
            lastHeartRate = info.heartRate;
        }
    }

    function startSession() {


     ...
 148             lastHeartRate = info.heartRate;
 149         }
 150     }
 151
+152     function onLengthButton() {
+153         if (state == STATE_HOLD) {
+154             lengthCount++;
+155             horizontalDistanceM = lengthCount * Settings.poolLengthM;
+156             WatchUi.requestUpdate();
+157         }
+158     }
+159
 152     function startSession() {
 153         if (session == null || !session.isRecording()) {
 154             session = ActivityRecording.createSession({
 155                 :name => "BreathHold",
     ...

--- ~/code/garmin-freedive-app/source/BreathHoldView.mc
+++ ~/code/garmin-freedive-app/source/BreathHoldView.mc
@@ -148,8 +148,16 @@
             lastHeartRate = info.heartRate;
         }
     }

+    function onLengthButton() {
+        if (state == STATE_HOLD) {
+            lengthCount++;
+            horizontalDistanceM = lengthCount * Settings.poolLengthM;
+            WatchUi.requestUpdate();
+        }
+    }
+
     function startSession() {
         if (session == null || !session.isRecording()) {
             session = ActivityRecording.createSession({
                 :name => "BreathHold",


            hrField = session.createField(
                "last_hr_bpm",
                1,
                FitContributor.DATA_TYPE_UINT8,
                {:mesgType => Fit.MESG_TYPE_SESSION, :units => "bpm"}
            );

            distanceField = session.createField(
                "horizontal_distance_m",
                2,
                FitContributor.DATA_TYPE_UINT16,
                {:mesgType => Fit.MESG_TYPE_SESSION, :units => "m"}
            );

            lengthField = session.createField(
                "length_count",
                3,
                FitContributor.DATA_TYPE_UINT8,
                {:mesgType => Fit.MESG_TYPE_SESSION, :units => "laps"}
            );

            session.start();

            hrField = session.createField(
                "last_hr_bpm",
                1,
                FitContributor.DATA_TYPE_UINT8,
                {:mesgType => Fit.MESG_TYPE_SESSION, :units => "bpm"}
            );

            session.start();


     ...
 178                 FitContributor.DATA_TYPE_UINT8,
 179                 {:mesgType => Fit.MESG_TYPE_SESSION, :units => "bpm"}
 180             );
 181
+182             distanceField = session.createField(
+183                 "horizontal_distance_m",
+184                 2,
+185                 FitContributor.DATA_TYPE_UINT16,
+186                 {:mesgType => Fit.MESG_TYPE_SESSION, :units => "m"}
+187             );
+188
+189             lengthField = session.createField(
+190                 "length_count",
+191                 3,
+192                 FitContributor.DATA_TYPE_UINT8,
+193                 {:mesgType => Fit.MESG_TYPE_SESSION, :units => "laps"}
+194             );
+195
 182             session.start();
 183         }
 184     }
 185
     ...

--- ~/code/garmin-freedive-app/source/BreathHoldView.mc
+++ ~/code/garmin-freedive-app/source/BreathHoldView.mc
@@ -178,8 +178,22 @@
                 FitContributor.DATA_TYPE_UINT8,
                 {:mesgType => Fit.MESG_TYPE_SESSION, :units => "bpm"}
             );

+            distanceField = session.createField(
+                "horizontal_distance_m",
+                2,
+                FitContributor.DATA_TYPE_UINT16,
+                {:mesgType => Fit.MESG_TYPE_SESSION, :units => "m"}
+            );
+
+            lengthField = session.createField(
+                "length_count",
+                3,
+                FitContributor.DATA_TYPE_UINT8,
+                {:mesgType => Fit.MESG_TYPE_SESSION, :units => "laps"}
+            );
+
             session.start();
         }
     }


        if (session != null && session.isRecording()) {
            if (holdField != null) {
                holdField.setData(maxHoldTime);
            }
            if (hrField != null && lastHeartRate != null) {
                hrField.setData(lastHeartRate);
            }
            if (distanceField != null) {
                distanceField.setData(horizontalDistanceM);
            }
            if (lengthField != null) {
                lengthField.setData(lengthCount);
            }
            session.stop();
            session.save();
        }

        if (session != null && session.isRecording()) {
            if (holdField != null) {
                holdField.setData(maxHoldTime);
            }
            if (hrField != null && lastHeartRate != null) {
                hrField.setData(lastHeartRate);
            }
            session.stop();
            session.save();
        }


     ...
 208             }
 209             if (hrField != null && lastHeartRate != null) {
 210                 hrField.setData(lastHeartRate);
 211             }
+212             if (distanceField != null) {
+213                 distanceField.setData(horizontalDistanceM);
+214             }
+215             if (lengthField != null) {
+216                 lengthField.setData(lengthCount);
+217             }
 212             session.stop();
 213             session.save();
 214         }
 215
     ...

--- ~/code/garmin-freedive-app/source/BreathHoldView.mc
+++ ~/code/garmin-freedive-app/source/BreathHoldView.mc
@@ -208,8 +208,14 @@
             }
             if (hrField != null && lastHeartRate != null) {
                 hrField.setData(lastHeartRate);
             }
+            if (distanceField != null) {
+                distanceField.setData(horizontalDistanceM);
+            }
+            if (lengthField != null) {
+                lengthField.setData(lengthCount);
+            }
             session.stop();
             session.save();
         }


    function resetRunState() {
        state = STATE_IDLE;
        currentTime = 0;
        maxHoldTime = 0;
        contractionCount = 0;
        lengthCount = 0;
        horizontalDistanceM = 0;
        lastHeartRate = null;
    }

    function resetRunState() {
        state = STATE_IDLE;
        currentTime = 0;
        maxHoldTime = 0;
        contractionCount = 0;
        lastHeartRate = null;
    }


     ...
 243         state = STATE_IDLE;
 244         currentTime = 0;
 245         maxHoldTime = 0;
 246         contractionCount = 0;
+247         lengthCount = 0;
+248         horizontalDistanceM = 0;
 247         lastHeartRate = null;
 248     }
 249
 250     // Button-driven state machine. The delegate calls this on START.
     ...

--- ~/code/garmin-freedive-app/source/BreathHoldView.mc
+++ ~/code/garmin-freedive-app/source/BreathHoldView.mc
@@ -243,8 +243,10 @@
         state = STATE_IDLE;
         currentTime = 0;
         maxHoldTime = 0;
         contractionCount = 0;
+        lengthCount = 0;
+        horizontalDistanceM = 0;
         lastHeartRate = null;
     }

     // Button-driven state machine. The delegate calls this on START.


        return m.format("%02d") + ":" + s.format("%02d");
    }

    // Called once per second by the Timer
    function onTimer() {
        if (state == STATE_PREP) {
            currentTime++;
            if (currentTime >= prepTime) {
                state = STATE_HOLD;
                currentTime = 0;
            }
        } else if (state == STATE_HOLD) {
            currentTime++;
        } else if (state == STATE_RECOVERY) {
            currentTime++;
            if (currentTime >= recoveryTime) {
                finishSession();
                return;
            }
        }

        updateHeartRate();
        WatchUi.requestUpdate();
    }

    function updateHeartRate() {
        var info = Sensor.getInfo();
        if (info != null && info.heartRate != null) {
            lastHeartRate = info.heartRate;
        }
    }

    function onLengthButton() {
        if (state == STATE_HOLD) {
            lengthCount++;
            horizontalDistanceM = lengthCount * Settings.poolLengthM;
            WatchUi.requestUpdate();
        }
    }

    function startSession() {
        if (session == null || !session.isRecording()) {
            session = ActivityRecording.createSession({
                :name => "BreathHold",
                :sport => Activity.SPORT_GENERIC,
                :subSport => Activity.SUB_SPORT_GENERIC
            });

            holdField = session.createField(
                "hold_time_s",
                0,
                FitContributor.DATA_TYPE_UINT16,
                {:mesgType => Fit.MESG_TYPE_SESSION, :units => "s"}
            );

            hrField = session.createField(
                "last_hr_bpm",
                1,
                FitContributor.DATA_TYPE_UINT8,
                {:mesgType => Fit.MESG_TYPE_SESSION, :units => "bpm"}
            );

            distanceField = session.createField(
                "horizontal_distance_m",
                2,
                FitContributor.DATA_TYPE_UINT16,
                {:mesgType => Fit.MESG_TYPE_SESSION, :units => "m"}
            );

            lengthField = session.createField(
                "length_count",
                3,
                FitContributor.DATA_TYPE_UINT8,
                {:mesgType => Fit.MESG_TYPE_SESSION, :units => "laps"}
            );

            session.start();
        }
    }

    function finishSession() {
        if (state == STATE_HOLD) {
            maxHoldTime = currentTime;
        }

        if (session != null && session.isRecording()) {
            if (holdField != null) {
                holdField.setData(maxHoldTime);
            }
            if (hrField != null && lastHeartRate != null) {
                hrField.setData(lastHeartRate);
            }
            if (distanceField != null) {
                distanceField.setData(horizontalDistanceM);
            }
            if (lengthField != null) {
                lengthField.setData(lengthCount);
            }
            session.stop();
            session.save();
        }

        if (timer != null) {
            timer.stop();
        }

        state = STATE_DONE;
        WatchUi.requestUpdate();
    }

    function discardSession() {
        if (session != null && session.isRecording()) {
            session.stop();
            session.discard();
        }
        session = null;
        if (timer != null) {
            timer.stop();
        }
        resetRunState();

[49 more lines in file. Use offset=240 to continue.]


    }

    function resetRunState() {
        state = STATE_IDLE;
        currentTime = 0;
        maxHoldTime = 0;
        contractionCount = 0;
        lengthCount = 0;
        horizontalDistanceM = 0;
        lastHeartRate = null;
    }

    // Button-driven state machine. The delegate calls this on START.
    function onStartButton() {
        if (state == STATE_IDLE) {
            startSession();
            state = STATE_PREP;
            currentTime = 0;
            contractionCount = 0;
            timer.start(method(:onTimer), 1000, true);
        } else if (state == STATE_PREP) {
            // Skip remaining prep and start the breath-hold.
            state = STATE_HOLD;
            currentTime = 0;
        } else if (state == STATE_HOLD) {
            // End the hold, move to recovery.
            maxHoldTime = currentTime;
            state = STATE_RECOVERY;
            currentTime = 0;
        } else if (state == STATE_RECOVERY) {
            // Finish early and save.
            finishSession();
            return;
        } else if (state == STATE_DONE) {
            // Reset and return to idle.
            resetRunState();
        }
        WatchUi.requestUpdate();
    }

    // UP/DOWN buttons: count contractions during the hold.
    function onContractionButton() {
        if (state == STATE_HOLD) {
            contractionCount++;
            WatchUi.requestUpdate();
        }
    }
}


    // Button-driven state machine. The delegate calls this on START.
    function onStartButton() {
        if (state == STATE_IDLE) {
            startSession();
            state = STATE_PREP;
            currentTime = 0;
            contractionCount = 0;
            lengthCount = 0;
            horizontalDistanceM = 0;
            timer.start(method(:onTimer), 1000, true);
        } else if (state == STATE_PREP) {

    // Button-driven state machine. The delegate calls this on START.
    function onStartButton() {
        if (state == STATE_IDLE) {
            startSession();
            state = STATE_PREP;
            currentTime = 0;
            contractionCount = 0;
            timer.start(method(:onTimer), 1000, true);
        } else if (state == STATE_PREP) {


     ...
 255             startSession();
 256             state = STATE_PREP;
 257             currentTime = 0;
 258             contractionCount = 0;
+259             lengthCount = 0;
+260             horizontalDistanceM = 0;
 259             timer.start(method(:onTimer), 1000, true);
 260         } else if (state == STATE_PREP) {
 261             // Skip remaining prep and start the breath-hold.
 262             state = STATE_HOLD;
     ...

--- ~/code/garmin-freedive-app/source/BreathHoldView.mc
+++ ~/code/garmin-freedive-app/source/BreathHoldView.mc
@@ -255,8 +255,10 @@
             startSession();
             state = STATE_PREP;
             currentTime = 0;
             contractionCount = 0;
+            lengthCount = 0;
+            horizontalDistanceM = 0;
             timer.start(method(:onTimer), 1000, true);
         } else if (state == STATE_PREP) {
             // Skip remaining prep and start the breath-hold.
             state = STATE_HOLD;


    // DOWN button: count contractions during the hold.
    function onContractionButton() {

    // UP/DOWN buttons: count contractions during the hold.
    function onContractionButton() {


     ...
 278         }
 279         WatchUi.requestUpdate();
 280     }
 281
-282     // UP/DOWN buttons: count contractions during the hold.
+282     // DOWN button: count contractions during the hold.
 283     function onContractionButton() {
 284         if (state == STATE_HOLD) {
 285             contractionCount++;
 286             WatchUi.requestUpdate();
     ...

--- ~/code/garmin-freedive-app/source/BreathHoldView.mc
+++ ~/code/garmin-freedive-app/source/BreathHoldView.mc
@@ -278,9 +278,9 @@
         }
         WatchUi.requestUpdate();
     }

-    // UP/DOWN buttons: count contractions during the hold.
+    // DOWN button: count contractions during the hold.
     function onContractionButton() {
         if (state == STATE_HOLD) {
             contractionCount++;
             WatchUi.requestUpdate();


const G = 9.80665;
const RHO_FRESH = 997.0;  // kg/m³
const RHO_SALT  = 1023.0; // kg/m³

var surfacePressure = null;
var isSaltWater = false;

function calibrateSurface() {
    var info = Activity.getActivityInfo();
    if (info != null && info has :rawAmbientPressure && info.rawAmbientPressure != null) {
        surfacePressure = info.rawAmbientPressure;
    }
}

function computeDepth() {
    if (surfacePressure == null) { return null; }
    var info = Activity.getActivityInfo();
    if (info == null || !(info has :rawAmbientPressure) || info.rawAmbientPressure == null) {
        return null;
    }
    var rho = isSaltWater ? RHO_SALT : RHO_FRESH;
    var deltaP = info.rawAmbientPressure - surfacePressure;
    return deltaP / (rho * G);
}
```

A depth value can then be written to a `FitContributor` field on each `RECORD` message to produce a depth graph in Garmin Connect.

---

## Build prerequisites

1. A modern computer with the **Connect IQ SDK** installed.
   - Download the SDK Manager from: https://developer.garmin.com/connect-iq/sdk/
   - Use the SDK Manager to download the latest SDK plus the **fēnix 3** and **fēnix 3 HR** device files.
2. **Visual Studio Code** with the official **Monkey C** extension (recommended), or the Eclipse plugin.
3. A USB cable to connect the watch to your computer.

---

## Build steps

1. Open this folder in VS Code.
2. Run **Monkey C: Build Current Project** (or **Monkey C: Export Project**) from the command palette.
3. Select the `fenix3` or `fenix3_hr` target.
4. The compiler produces a `.PRG` file, typically under:
   - `bin/<build-name>.prg` when building for a single device, or
   - A `.iq` zip when exporting for multiple devices.

If you prefer the command line, use the `monkeyc` compiler and `monkeydo` simulator scripts that ship with the SDK.

---

## Sideload onto the watch

[82 more lines in file. Use offset=110 to continue.]


Garmin does not allow diving/breath-hold apps in the Connect IQ Store, so this app is installed manually.

### 1. Put the watch in USB storage mode

1. Connect the watch to your computer with the USB cable.
2. On the watch, confirm **Mass Storage / USB mode** if prompted.
3. The watch appears as a removable drive (e.g., `GARMIN` on Windows, `/media/GARMIN` on Linux, or `GARMIN` on macOS).

### 2. Copy the app

1. Open the watch drive.
2. Copy the compiled `.PRG` file into the `GARMIN/APPS/` folder.
   - Do **not** create a sub-folder.
   - If a previous version exists, overwrite it.
3. Safely eject the watch.

### 3. Launch the app

1. Disconnect the cable.
2. On the watch, go to **Activities** → scroll down to **Apps** (or **More** → **Apps**, depending on firmware).
3. Select **Freedive Breath** and press **START**.

---

## Usage

| Button | Function |
|--------|----------|
| **START** | Begin session / skip prep / end hold / finish recovery / start new session |
| **UP / DOWN** | Count a contraction during the hold phase |
| **BACK (ESC)** | Exit from idle / discard and exit during an active session |

### Typical flow

1. Start the app. You see `FREEDIVE` — press **START**.
2. The 2-minute **PREP BREATHE** countdown starts. Press **START** again to skip to the hold.
3. **HOLD** counts up. Press **UP** or **DOWN** each time you feel a contraction.
4. When you finish the hold, press **START** to move to **RECOVERY**.
5. Recovery counts down from 2 minutes. Press **START** to finish early, or let it time out.
6. The session is saved to a FIT file. You see `SAVED` with your max hold time.
7. Press **START** to begin a new session, or **BACK** to exit.

---

## HRM‑Tri / HRM‑Swim pairing

1. Pair the chest strap with the watch in the regular Garmin **Sensors & Accessories** menu before using the app.
2. Put the strap on before starting the app.
3. The app reads `Sensor.getInfo().heartRate`, which automatically uses the connected ANT+ HR source when available.
4. The strap stores HR data during the breath-hold and syncs it to the watch. The app only samples the live value once per second, so the FIT `last_hr_bpm` field is the last HR seen in the recovery phase.

---

## Customization

Edit these values in `source/BreathHoldView.mc` before building:

```monkey-c
var prepTime = 120;     // seconds
var recoveryTime = 120; // seconds
```

For a pool-only fresh/salt toggle, a depthless water-type setting, or multiple rounds, ask and it can be added.

---

## Troubleshooting

| Problem | Fix |
|---------|-----|
| App does not appear on the watch | Confirm the `.PRG` is directly in `GARMIN/APPS/`, not in a sub-folder. |
| Build fails with API errors | Make sure the SDK includes fēnix 3 device files and the project `minApiLevel` is `1.4.0`. |
| Heart rate shows `--` | Pair the HRM‑Tri with the watch first; wet the strap electrodes; stand close to the watch. |
| App crashes on start | Check that `manifest.xml` includes the `Sensor`, `ActivityRecording`, and `FitContributor` permissions. |
| Timer stops when screen sleeps | This is a known Connect IQ behavior for watch apps. The app resumes when you wake the screen. For long breath-holds, keep the watch awake by pressing LIGHT if needed, or the session will still save with the elapsed time when you wake it. |

---

## Disclaimer

This is a hobby project for personal use only. It is **not** a dive computer, medical device, or safety instrument. Freediving is inherently dangerous. Always dive with a buddy, use proper safety protocols, and rely on certified dive equipment for depth and safety monitoring.


## Usage

### Buttons

| Button | Function |
|--------|----------|
| **START** | Begin session / skip prep / end hold / finish recovery / start new session |
| **UP** | Count a completed pool length (only during the hold phase) |
| **DOWN** | Count a contraction (only during the hold phase) |
| **BACK short press** | Exit from idle / discard and exit during an active session |
| **BACK long press** | Open pool-length settings from the idle screen |

### Setting the pool length

1. From the idle screen, **press and hold BACK** until the `POOL LENGTH` screen appears.
2. Press **UP** / **DOWN** to choose between **25 m**, **50 m**, or **custom**.
3. With **custom** selected, press **START** to edit the value, then **UP** / **DOWN** to change it, and **START** again to save.
4. Press **START** to confirm the selected length, or **BACK** to cancel.

The chosen length is remembered across app launches.

### Typical session flow

1. Start the app. The idle screen shows the configured pool length (e.g., `POOL 25m`).
2. Press **START** to begin. The 2-minute **PREP BREATHE** countdown starts. Press **START** again to skip to the hold.
3. **HOLD** counts up. Press **UP** each time you complete a pool length. Press **DOWN** each time you feel a contraction.
4. When you finish the hold, press **START** to move to **RECOVERY**.
5. Recovery counts down from 2 minutes. Press **START** to finish early, or let it time out.
6. The session is saved to a FIT file. The done screen shows your best hold time, number of lengths, and total horizontal distance.
7. Press **START** to begin a new session, or **BACK** to exit.

---

## Pool horizontal distance

The app records a **manual pool-length distance**:

- You tell the app the pool length (25 m, 50 m, or a custom value).
- During the hold, you press **UP** each time you complete a length.
- The FIT file gets two custom fields:
  - `length_count` — number of lengths you pressed.
  - `horizontal_distance_m` — `length_count × poolLength`.

This is the same principle Garmin’s native **Pool Swim** uses, but the count is manual because third-party apps cannot access the watch’s stroke/turn-detection accelerometer pipeline. Distance is only recorded for pool use; there is no GPS or accelerometer dead-reckoning while swimming.

---

## HRM‑Tri / HRM‑Swim pairing

## Usage

| Button | Function |
|--------|----------|
| **START** | Begin session / skip prep / end hold / finish recovery / start new session |
| **UP / DOWN** | Count a contraction during the hold phase |
| **BACK (ESC)** | Exit from idle / discard and exit during an active session |

### Typical flow

1. Start the app. You see `FREEDIVE` — press **START**.
2. The 2-minute **PREP BREATHE** countdown starts. Press **START** again to skip to the hold.
3. **HOLD** counts up. Press **UP** or **DOWN** each time you feel a contraction.
4. When you finish the hold, press **START** to move to **RECOVERY**.
5. Recovery counts down from 2 minutes. Press **START** to finish early, or let it time out.
6. The session is saved to a FIT file. You see `SAVED` with your max hold time.
7. Press **START** to begin a new session, or **BACK** to exit.

---

## HRM‑Tri / HRM‑Swim pairing


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

     ...
 132 ---
 133
 134 ## Usage
 135
+136 ### Buttons
+137
 136 | Button | Function |
 137 |--------|----------|
 138 | **START** | Begin session / skip prep / end hold / finish recovery / start new session |
-139 | **UP / DOWN** | Count a contraction during the hold phase |
-140 | **BACK (ESC)** | Exit from idle / discard and exit during an active session |
+141 | **UP** | Count a completed pool length (only during the hold phase) |
+142 | **DOWN** | Count a contraction (only during the hold phase) |
+143 | **BACK short press** | Exit from idle / discard and exit during an active session |
+144 | **BACK long press** | Open pool-length settings from the idle screen |
 141
-142 ### Typical flow
+146 ### Setting the pool length
 143
-144 1. Start the app. You see `FREEDIVE` — press **START**.
-145 2. The 2-minute **PREP BREATHE** countdown starts. Press **START** again to skip to the hold.
-146 3. **HOLD** counts up. Press **UP** or **DOWN** each time you feel a contraction.
+148 1. From the idle screen, **press and hold BACK** until the `POOL LENGTH` screen appears.
+149 2. Press **UP** / **DOWN** to choose between **25 m**, **50 m**, or **custom**.
+150 3. With **custom** selected, press **START** to edit the value, then **UP** / **DOWN** to change it, and **START** again to save.
+151 4. Press **START** to confirm the selected length, or **BACK** to cancel.
+152
+153 The chosen length is remembered across app launches.
+154
+155 ### Typical session flow
+156
+157 1. Start the app. The idle screen shows the configured pool length (e.g., `POOL 25m`).
+158 2. Press **START** to begin. The 2-minute **PREP BREATHE** countdown starts. Press **START** again to skip to the hold.
+159 3. **HOLD** counts up. Press **UP** each time you complete a pool length. Press **DOWN** each time you feel a contraction.
 147 4. When you finish the hold, press **START** to move to **RECOVERY**.
 148 5. Recovery counts down from 2 minutes. Press **START** to finish early, or let it time out.
-149 6. The session is saved to a FIT file. You see `SAVED` with your max hold time.
+162 6. The session is saved to a FIT file. The done screen shows your best hold time, number of lengths, and total horizontal distance.
 150 7. Press **START** to begin a new session, or **BACK** to exit.
 151
 152 ---
 153
+167 ## Pool horizontal distance
+168
+169 The app records a **manual pool-length distance**:
+170
+171 - You tell the app the pool length (25 m, 50 m, or a custom value).
+172 - During the hold, you press **UP** each time you complete a length.
+173 - The FIT file gets two custom fields:
+174   - `length_count` — number of lengths you pressed.
+175   - `horizontal_distance_m` — `length_count × poolLength`.
+176
+177 This is the same principle Garmin’s native **Pool Swim** uses, but the count is manual because third-party apps cannot access the watch’s stroke/turn-detection accelerometer pipeline. Distance is only recorded for pool use; there is no GPS or accelerometer dead-reckoning while swimming.
+178
+179 ---
+180
 154 ## HRM‑Tri / HRM‑Swim pairing
 155
 156 1. Pair the chest strap with the watch in the regular Garmin **Sensors & Accessories** menu before using the app.
 157 2. Put the strap on before starting the app.
     ...

--- ~/code/garmin-freedive-app/README.md
+++ ~/code/garmin-freedive-app/README.md
@@ -132,26 +132,53 @@
 ---

 ## Usage

+### Buttons
+
 | Button | Function |
 |--------|----------|
 | **START** | Begin session / skip prep / end hold / finish recovery / start new session |
-| **UP / DOWN** | Count a contraction during the hold phase |
-| **BACK (ESC)** | Exit from idle / discard and exit during an active session |
+| **UP** | Count a completed pool length (only during the hold phase) |
+| **DOWN** | Count a contraction (only during the hold phase) |
+| **BACK short press** | Exit from idle / discard and exit during an active session |
+| **BACK long press** | Open pool-length settings from the idle screen |

-### Typical flow
+### Setting the pool length

-1. Start the app. You see `FREEDIVE` — press **START**.
-2. The 2-minute **PREP BREATHE** countdown starts. Press **START** again to skip to the hold.
-3. **HOLD** counts up. Press **UP** or **DOWN** each time you feel a contraction.
+1. From the idle screen, **press and hold BACK** until the `POOL LENGTH` screen appears.
+2. Press **UP** / **DOWN** to choose between **25 m**, **50 m**, or **custom**.
+3. With **custom** selected, press **START** to edit the value, then **UP** / **DOWN** to change it, and **START** again to save.
+4. Press **START** to confirm the selected length, or **BACK** to cancel.
+
+The chosen length is remembered across app launches.
+
+### Typical session flow
+
+1. Start the app. The idle screen shows the configured pool length (e.g., `POOL 25m`).
+2. Press **START** to begin. The 2-minute **PREP BREATHE** countdown starts. Press **START** again to skip to the hold.
+3. **HOLD** counts up. Press **UP** each time you complete a pool length. Press **DOWN** each time you feel a contraction.
 4. When you finish the hold, press **START** to move to **RECOVERY**.
 5. Recovery counts down from 2 minutes. Press **START** to finish early, or let it time out.
-6. The session is saved to a FIT file. You see `SAVED` with your max hold time.
+6. The session is saved to a FIT file. The done screen shows your best hold time, number of lengths, and total horizontal distance.
 7. Press **START** to begin a new session, or **BACK** to exit.

 ---

+## Pool horizontal distance
+
+The app records a **manual pool-length distance**:
+
+- You tell the app the pool length (25 m, 50 m, or a custom value).
+- During the hold, you press **UP** each time you complete a length.
+- The FIT file gets two custom fields:
+  - `length_count` — number of lengths you pressed.
+  - `horizontal_distance_m` — `length_count × poolLength`.
+
+This is the same principle Garmin’s native **Pool Swim** uses, but the count is manual because third-party apps cannot access the watch’s stroke/turn-detection accelerometer pipeline. Distance is only recorded for pool use; there is no GPS or accelerometer dead-reckoning while swimming.
+
+---
+
 ## HRM‑Tri / HRM‑Swim pairing

 1. Pair the chest strap with the watch in the regular Garmin **Sensors & Accessories** menu before using the app.
 2. Put the strap on before starting the app.


| App crashes on start | Check that `manifest.xml` includes the `Sensor`, `ActivityRecording`, and `FitContributor` permissions. |
| Pool length not saved between launches | `Settings.mc` uses the application object store, which works on fēnix 3 / fēnix 3 HR. |
| BACK opens settings instead of exiting | A **short** BACK press exits from idle; you must **press and hold** BACK for ~0.8 s to enter settings. |
| Timer stops when screen sleeps | This is a known Connect IQ behavior for watch apps. The app resumes when you wake the screen. For long breath-holds, keep the watch awake by pressing LIGHT if needed, or the session will still save with the elapsed time when you wake it. |

| App crashes on start | Check that `manifest.xml` includes the `Sensor`, `ActivityRecording`, and `FitContributor` permissions. |
| Timer stops when screen sleeps | This is a known Connect IQ behavior for watch apps. The app resumes when you wake the screen. For long breath-holds, keep the watch awake by pressing LIGHT if needed, or the session will still save with the elapsed time when you wake it. |


     ...
 207 | App does not appear on the watch | Confirm the `.PRG` is directly in `GARMIN/APPS/`, not in a sub-folder. |
 208 | Build fails with API errors | Make sure the SDK includes fēnix 3 device files and the project `minApiLevel` is `1.4.0`. |
 209 | Heart rate shows `--` | Pair the HRM‑Tri with the watch first; wet the strap electrodes; stand close to the watch. |
 210 | App crashes on start | Check that `manifest.xml` includes the `Sensor`, `ActivityRecording`, and `FitContributor` permissions. |
+211 | Pool length not saved between launches | `Settings.mc` uses the application object store, which works on fēnix 3 / fēnix 3 HR. |
+212 | BACK opens settings instead of exiting | A **short** BACK press exits from idle; you must **press and hold** BACK for ~0.8 s to enter settings. |
 211 | Timer stops when screen sleeps | This is a known Connect IQ behavior for watch apps. The app resumes when you wake the screen. For long breath-holds, keep the watch awake by pressing LIGHT if needed, or the session will still save with the elapsed time when you wake it. |
 212
 213 ---
 214
     ...

--- ~/code/garmin-freedive-app/README.md
+++ ~/code/garmin-freedive-app/README.md
@@ -207,8 +207,10 @@
 | App does not appear on the watch | Confirm the `.PRG` is directly in `GARMIN/APPS/`, not in a sub-folder. |
 | Build fails with API errors | Make sure the SDK includes fēnix 3 device files and the project `minApiLevel` is `1.4.0`. |
 | Heart rate shows `--` | Pair the HRM‑Tri with the watch first; wet the strap electrodes; stand close to the watch. |
 | App crashes on start | Check that `manifest.xml` includes the `Sensor`, `ActivityRecording`, and `FitContributor` permissions. |
+| Pool length not saved between launches | `Settings.mc` uses the application object store, which works on fēnix 3 / fēnix 3 HR. |
+| BACK opens settings instead of exiting | A **short** BACK press exits from idle; you must **press and hold** BACK for ~0.8 s to enter settings. |
 | Timer stops when screen sleeps | This is a known Connect IQ behavior for watch apps. The app resumes when you wake the screen. For long breath-holds, keep the watch awake by pressing LIGHT if needed, or the session will still save with the elapsed time when you wake it. |

 ---