using Toybox.System;
using Toybox.Sensor;

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

    static function sampleHeartRate() {
        var info = Sensor.getInfo();
        if (info != null && info.heartRate != null) {
            return info.heartRate;
        }
        return null;
    }
}

using Toybox.Application;

class AppSettings {
    static var poolLengthM = 25;
    static var poolLengthIndex = 0; // 0=25m, 1=50m, 2=custom
    static var waterType = 0;       // 0=fresh, 1=salt
    static var defaultBreathUpS = 120;
    static var defaultRecoveryS = 120;
    static var distanceDownAction = 0; // 0=strokes, 1=contractions
    static var breathingRestSeries = [180, 150, 120, 90, 60];
    static var breathingHoldTargetS = 120;
    static var staticBreathUpS = 120;
    static var staticRecoveryS = 120;

    static function load() {
        var app = Application.getApp();
        var v;

        v = app.getProperty("poolLengthM");
        if (v != null) { poolLengthM = v; }

        v = app.getProperty("poolLengthIndex");
        if (v != null) { poolLengthIndex = v; }

        v = app.getProperty("waterType");
        if (v != null) { waterType = v; }

        v = app.getProperty("defaultBreathUpS");
        if (v != null) { defaultBreathUpS = v; }

        v = app.getProperty("defaultRecoveryS");
        if (v != null) { defaultRecoveryS = v; }

        v = app.getProperty("distanceDownAction");
        if (v != null) { distanceDownAction = v; }

        v = app.getProperty("breathingHoldTargetS");
        if (v != null) { breathingHoldTargetS = v; }

        v = app.getProperty("staticBreathUpS");
        if (v != null) { staticBreathUpS = v; }

        v = app.getProperty("staticRecoveryS");
        if (v != null) { staticRecoveryS = v; }

        var series = app.getProperty("breathingRestSeries");
        if (series != null && series instanceof Array) {
            breathingRestSeries = series;
        }
    }

    static function save() {
        var app = Application.getApp();
        app.setProperty("poolLengthM", poolLengthM);
        app.setProperty("poolLengthIndex", poolLengthIndex);
        app.setProperty("waterType", waterType);
        app.setProperty("defaultBreathUpS", defaultBreathUpS);
        app.setProperty("defaultRecoveryS", defaultRecoveryS);
        app.setProperty("distanceDownAction", distanceDownAction);
        app.setProperty("breathingHoldTargetS", breathingHoldTargetS);
        app.setProperty("staticBreathUpS", staticBreathUpS);
        app.setProperty("staticRecoveryS", staticRecoveryS);
        app.setProperty("breathingRestSeries", breathingRestSeries);
    }
}


Successfully wrote 561 bytes to /home/liph/code/garmin-freedive-app/source/Common.mc


Successfully wrote 2355 bytes to /home/liph/code/garmin-freedive-app/source/AppSettings.mc


write

using Toybox.Application;
using Toybox.WatchUi;
using Toybox.Sensor;

class Fenix5App extends Application.AppBase {

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

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

        if (Sensor has :setEnabledSensors) {
            Sensor.setEnabledSensors([Sensor.SENSOR_HEARTRATE]);
        }
    }

    function onStop(state) {
        // Nothing to clean up explicitly.
    }

    function getInitialView() {
        return [new MainMenuView(), new MainMenuDelegate()];
    }
}


Successfully wrote 548 bytes to /home/liph/code/garmin-freedive-app/source/Fenix5App.mc


using Toybox.WatchUi;
using Toybox.Graphics;
using Toybox.System;

class MainMenuView extends WatchUi.View {

    const ITEM_DEPTH      = 0;
    const ITEM_DISTANCE   = 1;
    const ITEM_STATIC     = 2;
    const ITEM_BREATHING  = 3;

    const ITEMS = ["Depth", "Distance", "Static Apnoe", "Breathing Tr."];

    var selectedIndex = 0;

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

    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, "FREEDIVE MODE", Graphics.TEXT_JUSTIFY_CENTER);

        var itemY = y + 40;
        for (var i = 0; i < ITEMS.size(); i++) {
            var prefix = (i == selectedIndex) ? "> " : "  ";
            dc.drawText(cx, itemY, Graphics.FONT_MEDIUM, prefix + ITEMS[i], Graphics.TEXT_JUSTIFY_CENTER);
            itemY += 34;
        }

        dc.drawText(cx, dc.getHeight() - 20, Graphics.FONT_TINY,
            "UP/DOWN=move START=start", Graphics.TEXT_JUSTIFY_CENTER);
    }

    function moveUp() {
        selectedIndex--;
        if (selectedIndex < 0) { selectedIndex = ITEMS.size() - 1; }
        WatchUi.requestUpdate();
    }

    function moveDown() {
        selectedIndex++;
        if (selectedIndex >= ITEMS.size()) { selectedIndex = 0; }
        WatchUi.requestUpdate();
    }

    function select() {
        if (selectedIndex == ITEM_DEPTH) {
            WatchUi.pushView(
                new SubMenuView("DEPTH TYPE", ["Constant Weight", "Free Immersion"], method(:onDepthType)),
                new SubMenuDelegate(),
                WatchUi.SLIDE_LEFT);
        } else if (selectedIndex == ITEM_DISTANCE) {
            WatchUi.pushView(
                new SubMenuView("DIST FIN TYPE", ["Bi-fins", "Monofin", "No-fins"], method(:onDistanceType)),
                new SubMenuDelegate(),
                WatchUi.SLIDE_LEFT);
        } else if (selectedIndex == ITEM_STATIC) {
            launchStatic();
        } else if (selectedIndex == ITEM_BREATHING) {
            launchBreathing();
        }
    }

    function onDepthType(index) {
        var discipline;
        if (index == 0) {
            discipline = "CWT";
        } else {
            discipline = "FIM";
        }
        // If Constant Weight, ask for fin style before launching depth view.
        if (discipline.equals("CWT")) {
            WatchUi.pushView(
                new SubMenuView("CWT FIN STYLE", ["Bi-fins", "Monofin", "No-fins"], method(:onCwtFinStyle)),
                new SubMenuDelegate(),
                WatchUi.SLIDE_LEFT);
        } else {
            launchDepth(discipline);
        }
    }

    function onCwtFinStyle(index) {
        var discipline;
        if (index == 0) { discipline = "CWT-B"; }
        else if (index == 1) { discipline = "CWT-M"; }
        else { discipline = "CNF"; }
        launchDepth(discipline);
    }

    function onDistanceType(index) {
        var discipline;
        if (index == 0) { discipline = "DYN-B"; }
        else if (index == 1) { discipline = "DYN-M"; }
        else { discipline = "DNF"; }
        launchDistance(discipline);
    }

    function launchDepth(discipline) {
        WatchUi.switchToView(new DepthView(discipline), new DepthDelegate(new DepthView(discipline)), WatchUi.SLIDE_LEFT);
    }

    function launchDistance(discipline) {
        WatchUi.switchToView(new DistanceView(discipline), new DistanceDelegate(new DistanceView(discipline)), WatchUi.SLIDE_LEFT);
    }

    function launchStatic() {
        WatchUi.switchToView(new StaticView(), new StaticDelegate(new StaticView()), WatchUi.SLIDE_LEFT);
    }

    function launchBreathing() {
        WatchUi.switchToView(new BreathingView(), new BreathingDelegate(new BreathingView()), WatchUi.SLIDE_LEFT);
    }
}


Successfully wrote 3919 bytes to /home/liph/code/garmin-freedive-app/source/MainMenuView.mc


using Toybox.WatchUi;
using Toybox.System;

class MainMenuDelegate extends WatchUi.InputDelegate {

    var view;

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

    function setView(v) {
        view = v;
    }

    function onKeyPressed(keyEvent) {
        if (view == null) {
            return false;
        }

        var key = keyEvent.getKey();

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

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

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

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            System.exit();
            return true;
        }

        return false;
    }

    function onKey(keyEvent) {
        var key = keyEvent.getKey();
        if (key == WatchUi.KEY_UP || key == WatchUi.KEY_DOWN ||
            key == WatchUi.KEY_START || key == WatchUi.KEY_ENTER ||
            key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            return true;
        }
        return false;
    }
}


Successfully wrote 1219 bytes to /home/liph/code/garmin-freedive-app/source/MainMenuDelegate.mc


using Toybox.WatchUi;
using Toybox.Graphics;
using Toybox.System;

class SubMenuView extends WatchUi.View {

    var title;
    var items;
    var selectedIndex = 0;
    var onSelect; // method reference

    function initialize(t, itms, callback) {
        View.initialize();
        title = t;
        items = itms;
        onSelect = callback;
    }

    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, title, Graphics.TEXT_JUSTIFY_CENTER);

        var itemY = y + 40;
        for (var i = 0; i < items.size(); i++) {
            var prefix = (i == selectedIndex) ? "> " : "  ";
            dc.drawText(cx, itemY, Graphics.FONT_MEDIUM, prefix + items[i], Graphics.TEXT_JUSTIFY_CENTER);
            itemY += 34;
        }

        dc.drawText(cx, dc.getHeight() - 20, Graphics.FONT_TINY,
            "UP/DOWN=move START=start", Graphics.TEXT_JUSTIFY_CENTER);
    }

    function moveUp() {
        selectedIndex--;
        if (selectedIndex < 0) { selectedIndex = items.size() - 1; }
        WatchUi.requestUpdate();
    }

    function moveDown() {
        selectedIndex++;
        if (selectedIndex >= items.size()) { selectedIndex = 0; }
        WatchUi.requestUpdate();
    }

    function select() {
        if (onSelect != null) {
            onSelect.invoke(selectedIndex);
        }
    }
}


Successfully wrote 1534 bytes to /home/liph/code/garmin-freedive-app/source/SubMenuView.mc


using Toybox.WatchUi;

class SubMenuDelegate extends WatchUi.InputDelegate {

    var view;

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

    function setView(v) {
        view = v;
    }

    function onKeyPressed(keyEvent) {
        if (view == null) { return false; }

        var key = keyEvent.getKey();

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

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

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

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

        return false;
    }

    function onKey(keyEvent) {
        var key = keyEvent.getKey();
        if (key == WatchUi.KEY_UP || key == WatchUi.KEY_DOWN ||
            key == WatchUi.KEY_START || key == WatchUi.KEY_ENTER ||
            key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            return true;
        }
        return false;
    }
}


Successfully wrote 1200 bytes to /home/liph/code/garmin-freedive-app/source/SubMenuDelegate.mc


using Toybox.Activity;
using Toybox.System;

class DepthModel {

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

    static var surfacePressure = null;

    static function calibrateSurface() {
        var info = Activity.getActivityInfo();
        if (info != null && info has :rawAmbientPressure && info.rawAmbientPressure != null) {
            // Average a few samples if available; for now use single sample.
            surfacePressure = info.rawAmbientPressure;
        }
    }

    static function depthM() {
        if (surfacePressure == null) { return null; }

        var info = Activity.getActivityInfo();
        if (info == null || !(info has :rawAmbientPressure) || info.rawAmbientPressure == null) {
            return null;
        }

        var rho = (AppSettings.waterType == 1) ? RHO_SALT : RHO_FRESH;
        var deltaP = info.rawAmbientPressure - surfacePressure;
        var d = deltaP / (rho * G);
        if (d < 0.0) { d = 0.0; }
        return d;
    }

    static function reset() {
        surfacePressure = null;
    }
}


Successfully wrote 1128 bytes to /home/liph/code/garmin-freedive-app/source/DepthModel.mc


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

class DepthView extends WatchUi.View {

    // States
    const STATE_BREATH_UP     = 0;
    const STATE_SURFACE_READY = 1;
    const STATE_DIVE          = 2;
    const STATE_RECOVERY      = 3;
    const STATE_DONE          = 4;

    var discipline;
    var state = STATE_BREATH_UP;
    var timer = null;

    // FIT
    var session = null;
    var depthRecordField = null;
    var hrRecordField = null;
    var disciplineField = null;
    var maxDepthField = null;
    var diveTimeField = null;
    var surfaceIntervalField = null;
    var breathUpField = null;
    var lastHrField = null;

    // Runtime
    var currentTime = 0;
    var breathUpTime = 0;
    var diveTime = 0;
    var recoveryTime = 0;
    var surfaceIntervalTime = 0;
    var maxDepthM = 0.0;
    var currentDepthM = 0.0;
    var lastHeartRate = null;

    function initialize(disc) {
        View.initialize();
        discipline = disc;
        breathUpTime = AppSettings.defaultBreathUpS;
        recoveryTime = AppSettings.defaultRecoveryS;
    }

    function onShow() {
        timer = new Timer.Timer();
        timer.start(method(:onTimer), 1000, true);
    }

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

    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 = 10;

        var statusText = "";
        var bigText = "";
        var infoText = "";
        var hintText = "";

        if (state == STATE_BREATH_UP) {
            statusText = discipline + "  BREATH UP";
            bigText = Common.formatTime(breathUpTime - currentTime);
            infoText = waterTypeString();
            hintText = "START = skip";
        } else if (state == STATE_SURFACE_READY) {
            statusText = discipline + "  SURFACE";
            bigText = depthString(currentDepthM);
            infoText = "SI " + Common.formatTime(surfaceIntervalTime) + "  HR " + hrString();
            hintText = "START = dive";
        } else if (state == STATE_DIVE) {
            statusText = discipline + "  DIVE";
            bigText = depthString(currentDepthM);
            infoText = "MAX " + depthString(maxDepthM) + "  " + Common.formatTime(diveTime) + "  HR " + hrString();
            hintText = "START = surface";
        } else if (state == STATE_RECOVERY) {
            statusText = discipline + "  RECOVERY";
            bigText = Common.formatTime(recoveryTime - currentTime);
            infoText = "MAX " + depthString(maxDepthM) + "  HR " + hrString();
            hintText = "START = save";
        } else if (state == STATE_DONE) {
            statusText = discipline + "  SAVED";
            bigText = depthString(maxDepthM);
            infoText = "DIVE " + Common.formatTime(diveTime) + "  HR " + hrString();
            hintText = "BACK = menu";
        }

        dc.drawText(cx, y, Graphics.FONT_SMALL, statusText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, y + 36, Graphics.FONT_NUMBER_HOT, bigText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, y + 100, Graphics.FONT_SMALL, infoText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, dc.getHeight() - 22, Graphics.FONT_TINY, hintText, Graphics.TEXT_JUSTIFY_CENTER);
    }

    function waterTypeString() {
        return (AppSettings.waterType == 1) ? "SALT" : "FRESH";
    }

    function depthString(d) {
        if (d == null) { return "--.-"; }
        return d.format("%.1f") + "m";
    }

    function hrString() {
        return (lastHeartRate != null) ? lastHeartRate.toString() : "--";
    }

    function onTimer() {
        lastHeartRate = Common.sampleHeartRate();

        if (state == STATE_BREATH_UP) {
            currentTime++;
            if (currentTime >= breathUpTime) {
                startSurfaceReady();
            }
        } else if (state == STATE_SURFACE_READY) {
            surfaceIntervalTime++;
            updateDepth();
        } else if (state == STATE_DIVE) {
            diveTime++;
            updateDepth();
            if (depthRecordField != null && currentDepthM != null) {
                depthRecordField.setData(currentDepthM);
            }
            if (hrRecordField != null && lastHeartRate != null) {
                hrRecordField.setData(lastHeartRate);
            }
        } else if (state == STATE_RECOVERY) {
            currentTime++;
            if (currentTime >= recoveryTime) {
                finishSession();
                return;
            }
        }

        WatchUi.requestUpdate();
    }

    function updateDepth() {
        var d = DepthModel.depthM();
        if (d != null) {
            currentDepthM = d;
            if (currentDepthM > maxDepthM) {
                maxDepthM = currentDepthM;
            }
        }
    }

    function startSurfaceReady() {
        currentTime = 0;
        state = STATE_SURFACE_READY;
        DepthModel.calibrateSurface();
    }

    function startDive() {
        currentTime = 0;
        diveTime = 0;
        state = STATE_DIVE;
        startRecording();
    }

    function endDive() {
        state = STATE_RECOVERY;
        currentTime = 0;
    }

    function finishSession() {
        if (session != null && session.isRecording()) {
            if (disciplineField != null) { disciplineField.setData(discipline); }
            if (maxDepthField != null) { maxDepthField.setData((maxDepthM * 10).toNumber() / 10); }
            if (diveTimeField != null) { diveTimeField.setData(diveTime); }
            if (surfaceIntervalField != null) { surfaceIntervalField.setData(surfaceIntervalTime); }
            if (breathUpField != null) { breathUpField.setData(breathUpTime); }
            if (lastHrField != null && lastHeartRate != null) { lastHrField.setData(lastHeartRate); }
            session.stop();
            session.save();
        }
        timer.stop();
        state = STATE_DONE;
        WatchUi.requestUpdate();
    }

    function startRecording() {
        if (session == null || !session.isRecording()) {
            session = ActivityRecording.createSession({
                :name => "FreediveDepth",
                :sport => ActivityRecording.SPORT_SWIMMING,
                :subSport => ActivityRecording.SUB_SPORT_OPEN_WATER
            });

            depthRecordField = session.createField(
                "depth_m", 1, FitContributor.DATA_TYPE_FLOAT,
                {:mesgType => FitContributor.MESG_TYPE_RECORD, :units => "m"}
            );

            hrRecordField = session.createField(
                "hr_bpm", 2, FitContributor.DATA_TYPE_UINT8,
                {:mesgType => FitContributor.MESG_TYPE_RECORD, :units => "bpm"}
            );

            disciplineField = session.createField(
                "discipline", 0, FitContributor.DATA_TYPE_STRING,
                {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => ""}
            );

            maxDepthField = session.createField(
                "max_depth_m", 3, FitContributor.DATA_TYPE_UINT16,
                {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "m"}
            );

            diveTimeField = session.createField(
                "dive_time_s", 4, FitContributor.DATA_TYPE_UINT16,
                {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "s"}
            );

            surfaceIntervalField = session.createField(
                "surface_interval_s", 5, FitContributor.DATA_TYPE_UINT16,
                {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "s"}
            );

            breathUpField = session.createField(
                "breath_up_time_s", 10, FitContributor.DATA_TYPE_UINT16,
                {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "s"}
            );

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

            session.start();
        }
    }

    function discardSession() {
        if (session != null && session.isRecording()) {
            session.stop();
            session.discard();
        }
        session = null;
        if (timer != null) {
            timer.stop();
        }
        DepthModel.reset();
        WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
    }

    function onStartButton() {
        if (state == STATE_BREATH_UP) {
            startSurfaceReady();
        } else if (state == STATE_SURFACE_READY) {
            startDive();
        } else if (state == STATE_DIVE) {
            endDive();
        } else if (state == STATE_RECOVERY) {
            finishSession();
        } else if (state == STATE_DONE) {
            DepthModel.reset();
            WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
        }
        WatchUi.requestUpdate();
    }
}


Successfully wrote 9210 bytes to /home/liph/code/garmin-freedive-app/source/DepthView.mc


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

class DepthDelegate 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_ESC || key == WatchUi.KEY_LAP) {
            if (view.state == view.STATE_DONE) {
                view.onStartButton(); // return to menu
                return true;
            }
            if (view.state == view.STATE_BREATH_UP ||
                view.state == view.STATE_SURFACE_READY) {
                backTimer = new Timer.Timer();
                backTimer.start(method(:onBackLongPress), LONG_PRESS_MS, false);
            } else {
                view.discardSession();
            }
            return true;
        }

        return false;
    }

    function onKey(keyEvent) {
        var key = keyEvent.getKey();
        if (key == WatchUi.KEY_START || key == WatchUi.KEY_ENTER ||
            key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            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;
                if (view.state == view.STATE_BREATH_UP ||
                    view.state == view.STATE_SURFACE_READY) {
                    DepthModel.reset();
                    WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
                }
            }
            return true;
        }

        return false;
    }

    function onBackLongPress() {
        backTimer = null;
        // Open settings from breath-up or surface states.
        if (view.state == view.STATE_BREATH_UP ||
            view.state == view.STATE_SURFACE_READY) {
            // TODO: push settings view.
            WatchUi.requestUpdate();
        }
    }
}


Successfully wrote 2276 bytes to /home/liph/code/garmin-freedive-app/source/DepthDelegate.mc


I need to create stub files for DistanceView, DistanceDelegate, StaticView, StaticDelegate, BreathingView, BreathingDelegate because MainMenuView references them. Otherwise the project won't compile. I can create minimal stubs first and flesh them out later.

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

class DistanceView extends WatchUi.View {

    const STATE_BREATH_UP     = 0;
    const STATE_HOLD          = 1;
    const STATE_RECOVERY      = 2;
    const STATE_DONE          = 3;

    var discipline;
    var state = STATE_BREATH_UP;
    var timer = null;

    var session = null;
    var disciplineField = null;
    var distanceField = null;
    var lengthField = null;
    var strokeField = null;
    var holdField = null;
    var breathUpField = null;
    var lastHrField = null;

    var currentTime = 0;
    var breathUpTime = 0;
    var holdTime = 0;
    var recoveryTime = 0;
    var maxHoldTime = 0;
    var lengthCount = 0;
    var strokeCount = 0;
    var horizontalDistanceM = 0;
    var lastHeartRate = null;

    function initialize(disc) {
        View.initialize();
        discipline = disc;
        breathUpTime = AppSettings.defaultBreathUpS;
        recoveryTime = AppSettings.defaultRecoveryS;
    }

    function onShow() {
        timer = new Timer.Timer();
        timer.start(method(:onTimer), 1000, true);
    }

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

    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 = 10;

        var statusText = "";
        var bigText = "";
        var infoText = "";
        var hintText = "";

        if (state == STATE_BREATH_UP) {
            statusText = discipline + "  BREATH UP";
            bigText = Common.formatTime(breathUpTime - currentTime);
            infoText = "POOL " + AppSettings.poolLengthM + "m";
            hintText = "START = skip";
        } else if (state == STATE_HOLD) {
            statusText = discipline + "  SWIM";
            bigText = Common.formatTime(currentTime);
            infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m  HR " + hrString();
            hintText = "UP=LAP DOWN=" + downActionString();
        } else if (state == STATE_RECOVERY) {
            statusText = discipline + "  RECOVERY";
            bigText = Common.formatTime(recoveryTime - currentTime);
            infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m  HR " + hrString();
            hintText = "START = save";
        } else if (state == STATE_DONE) {
            statusText = discipline + "  SAVED";
            bigText = Common.formatTime(maxHoldTime);
            infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m  HR " + hrString();
            hintText = "BACK = menu";
        }

        dc.drawText(cx, y, Graphics.FONT_SMALL, statusText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, y + 36, Graphics.FONT_NUMBER_MEDIUM, bigText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, y + 90, Graphics.FONT_SMALL, infoText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, dc.getHeight() - 22, Graphics.FONT_TINY, hintText, Graphics.TEXT_JUSTIFY_CENTER);
    }

    function downActionString() {
        return (AppSettings.distanceDownAction == 0) ? "STROKE" : "CONT";
    }

    function hrString() {
        return (lastHeartRate != null) ? lastHeartRate.toString() : "--";
    }

    function onTimer() {
        lastHeartRate = Common.sampleHeartRate();

        if (state == STATE_BREATH_UP) {
            currentTime++;
            if (currentTime >= breathUpTime) {
                startHold();
            }
        } else if (state == STATE_HOLD) {
            currentTime++;
        } else if (state == STATE_RECOVERY) {
            currentTime++;
            if (currentTime >= recoveryTime) {
                finishSession();
                return;
            }
        }

        WatchUi.requestUpdate();
    }

    function startHold() {
        currentTime = 0;
        state = STATE_HOLD;
        startRecording();
    }

    function endHold() {
        maxHoldTime = currentTime;
        state = STATE_RECOVERY;
        currentTime = 0;
    }

    function finishSession() {
        if (session != null && session.isRecording()) {
            if (disciplineField != null) { disciplineField.setData(discipline); }
            if (distanceField != null) { distanceField.setData(horizontalDistanceM); }
            if (lengthField != null) { lengthField.setData(lengthCount); }
            if (strokeField != null) { strokeField.setData(strokeCount); }
            if (holdField != null) { holdField.setData(maxHoldTime); }
            if (breathUpField != null) { breathUpField.setData(breathUpTime); }
            if (lastHrField != null && lastHeartRate != null) { lastHrField.setData(lastHeartRate); }
            session.stop();
            session.save();
        }
        timer.stop();
        state = STATE_DONE;
        WatchUi.requestUpdate();
    }

    function startRecording() {
        session = ActivityRecording.createSession({
            :name => "FreediveDistance",
            :sport => ActivityRecording.SPORT_SWIMMING,
            :subSport => ActivityRecording.SUB_SPORT_LAP_SWIMMING
        });

        disciplineField = session.createField(
            "discipline", 0, FitContributor.DATA_TYPE_STRING,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => ""}
        );

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

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

        strokeField = session.createField(
            "stroke_count", 8, FitContributor.DATA_TYPE_UINT8,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "strokes"}
        );

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

        breathUpField = session.createField(
            "breath_up_time_s", 10, FitContributor.DATA_TYPE_UINT16,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "s"}
        );

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

        session.start();
    }

    function discardSession() {
        if (session != null && session.isRecording()) {
            session.stop();
            session.discard();
        }
        session = null;
        if (timer != null) { timer.stop(); }
        WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
    }

    function onStartButton() {
        if (state == STATE_BREATH_UP) {
            startHold();
        } else if (state == STATE_HOLD) {
            endHold();
        } else if (state == STATE_RECOVERY) {
            finishSession();
        } else if (state == STATE_DONE) {
            WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
        }
        WatchUi.requestUpdate();
    }

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

    function onDownButton() {
        if (state == STATE_HOLD) {
            if (AppSettings.distanceDownAction == 0) {
                strokeCount++;
            } else {
                // contraction counter placeholder
            }
            WatchUi.requestUpdate();
        }
    }
}


Successfully wrote 7945 bytes to /home/liph/code/garmin-freedive-app/source/DistanceView.mc


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

class DistanceDelegate 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) {
            view.onUpButton();
            return true;
        }

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

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

        return false;
    }

    function onKey(keyEvent) {
        var key = keyEvent.getKey();
        if (key == WatchUi.KEY_START || key == WatchUi.KEY_ENTER ||
            key == WatchUi.KEY_UP || key == WatchUi.KEY_DOWN ||
            key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            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;
                if (view.state == view.STATE_BREATH_UP) {
                    WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
                }
            }
            return true;
        }

        return false;
    }

    function onBackLongPress() {
        backTimer = null;
        // TODO: open settings view.
    }
}


Successfully wrote 2163 bytes to /home/liph/code/garmin-freedive-app/source/DistanceDelegate.mc


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

class StaticView extends WatchUi.View {

    const STATE_BREATH_UP  = 0;
    const STATE_HOLD       = 1;
    const STATE_RECOVERY   = 2;
    const STATE_DONE       = 3;

    var state = STATE_BREATH_UP;
    var timer = null;

    var session = null;
    var disciplineField = null;
    var holdField = null;
    var breathUpField = null;
    var recoveryField = null;
    var lastHrField = null;

    var currentTime = 0;
    var breathUpTime = 0;
    var recoveryTime = 0;
    var maxHoldTime = 0;
    var lastHeartRate = null;

    function initialize() {
        View.initialize();
        breathUpTime = AppSettings.staticBreathUpS;
        recoveryTime = AppSettings.staticRecoveryS;
    }

    function onShow() {
        timer = new Timer.Timer();
        timer.start(method(:onTimer), 1000, true);
    }

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

    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 = 10;

        var statusText = "";
        var bigText = "";
        var infoText = "";
        var hintText = "";

        if (state == STATE_BREATH_UP) {
            statusText = "STA  BREATH UP";
            bigText = Common.formatTime(breathUpTime - currentTime);
            infoText = "HR " + hrString();
            hintText = "START = skip";
        } else if (state == STATE_HOLD) {
            statusText = "STA  HOLD";
            bigText = Common.formatTime(currentTime);
            infoText = "HR " + hrString();
            hintText = "START = end";
        } else if (state == STATE_RECOVERY) {
            statusText = "STA  RECOVERY";
            bigText = Common.formatTime(recoveryTime - currentTime);
            infoText = "HOLD " + Common.formatTime(maxHoldTime) + "  HR " + hrString();
            hintText = "START = save";
        } else if (state == STATE_DONE) {
            statusText = "STA  SAVED";
            bigText = Common.formatTime(maxHoldTime);
            infoText = "HR " + hrString();
            hintText = "BACK = menu";
        }

        dc.drawText(cx, y, Graphics.FONT_SMALL, statusText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, y + 36, Graphics.FONT_NUMBER_HOT, bigText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, y + 100, Graphics.FONT_SMALL, infoText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, dc.getHeight() - 22, Graphics.FONT_TINY, hintText, Graphics.TEXT_JUSTIFY_CENTER);
    }

    function hrString() {
        return (lastHeartRate != null) ? lastHeartRate.toString() : "--";
    }

    function onTimer() {
        lastHeartRate = Common.sampleHeartRate();

        if (state == STATE_BREATH_UP) {
            currentTime++;
            if (currentTime >= breathUpTime) {
                startHold();
            }
        } else if (state == STATE_HOLD) {
            currentTime++;
        } else if (state == STATE_RECOVERY) {
            currentTime++;
            if (currentTime >= recoveryTime) {
                finishSession();
                return;
            }
        }

        WatchUi.requestUpdate();
    }

    function startHold() {
        currentTime = 0;
        state = STATE_HOLD;
        startRecording();
    }

    function endHold() {
        maxHoldTime = currentTime;
        state = STATE_RECOVERY;
        currentTime = 0;
    }

    function finishSession() {
        if (session != null && session.isRecording()) {
            if (disciplineField != null) { disciplineField.setData("STA"); }
            if (holdField != null) { holdField.setData(maxHoldTime); }
            if (breathUpField != null) { breathUpField.setData(breathUpTime); }
            if (recoveryField != null) { recoveryField.setData(recoveryTime); }
            if (lastHrField != null && lastHeartRate != null) { lastHrField.setData(lastHeartRate); }
            session.stop();
            session.save();
        }
        timer.stop();
        state = STATE_DONE;
        WatchUi.requestUpdate();
    }

    function startRecording() {
        session = ActivityRecording.createSession({
            :name => "FreediveStatic",
            :sport => ActivityRecording.SPORT_TRAINING,
            :subSport => ActivityRecording.SUB_SPORT_EXERCISE
        });

        disciplineField = session.createField(
            "discipline", 0, FitContributor.DATA_TYPE_STRING,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => ""}
        );

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

        breathUpField = session.createField(
            "breath_up_time_s", 10, FitContributor.DATA_TYPE_UINT16,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "s"}
        );

        recoveryField = session.createField(
            "recovery_time_s", 11, FitContributor.DATA_TYPE_UINT16,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "s"}
        );

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

        session.start();
    }

    function discardSession() {
        if (session != null && session.isRecording()) {
            session.stop();
            session.discard();
        }
        session = null;
        if (timer != null) { timer.stop(); }
        WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
    }

    function onStartButton() {
        if (state == STATE_BREATH_UP) {
            startHold();
        } else if (state == STATE_HOLD) {
            endHold();
        } else if (state == STATE_RECOVERY) {
            finishSession();
        } else if (state == STATE_DONE) {
            WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
        }
        WatchUi.requestUpdate();
    }
}


Successfully wrote 6330 bytes to /home/liph/code/garmin-freedive-app/source/StaticView.mc


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

class StaticDelegate 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_ESC || key == WatchUi.KEY_LAP) {
            if (view.state == view.STATE_DONE) {
                view.onStartButton();
                return true;
            }
            if (view.state == view.STATE_BREATH_UP) {
                backTimer = new Timer.Timer();
                backTimer.start(method(:onBackLongPress), LONG_PRESS_MS, false);
            } else {
                view.discardSession();
            }
            return true;
        }

        return false;
    }

    function onKey(keyEvent) {
        var key = keyEvent.getKey();
        if (key == WatchUi.KEY_START || key == WatchUi.KEY_ENTER ||
            key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            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;
                if (view.state == view.STATE_BREATH_UP) {
                    WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
                }
            }
            return true;
        }

        return false;
    }

    function onBackLongPress() {
        backTimer = null;
        // TODO: open settings view.
    }
}


Successfully wrote 1885 bytes to /home/liph/code/garmin-freedive-app/source/StaticDelegate.mc


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

class BreathingView extends WatchUi.View {

    const STATE_SETUP_TARGET = 0;
    const STATE_SETUP_REST   = 1;
    const STATE_REST         = 2;
    const STATE_HOLD         = 3;
    const STATE_DONE         = 4;

    var state = STATE_SETUP_TARGET;
    var timer = null;

    var session = null;
    var disciplineField = null;
    var targetField = null;
    var cyclesField = null;
    var totalHoldField = null;
    var lastHrField = null;

    var targetHoldS = 120;
    var currentRestIndex = 0;
    var currentTime = 0;
    var cycleCount = 0;
    var totalHoldTime = 0;
    var lastHeartRate = null;

    function initialize() {
        View.initialize();
        targetHoldS = AppSettings.breathingHoldTargetS;
    }

    function onShow() {
        timer = new Timer.Timer();
        timer.start(method(:onTimer), 1000, true);
    }

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

    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 = 10;

        var statusText = "";
        var bigText = "";
        var infoText = "";
        var hintText = "";

        if (state == STATE_SETUP_TARGET) {
            statusText = "BREATHE TARGET";
            bigText = Common.formatTime(targetHoldS);
            infoText = "HR " + hrString();
            hintText = "UP/DOWN adjust  START=next";
        } else if (state == STATE_SETUP_REST) {
            statusText = "REST START";
            bigText = Common.formatTime(AppSettings.breathingRestSeries[0]);
            infoText = "CYCLES " + AppSettings.breathingRestSeries.size();
            hintText = "START = begin";
        } else if (state == STATE_REST) {
            statusText = "BREATHE";
            bigText = Common.formatTime(currentTime);
            infoText = "CYCLE " + (cycleCount + 1) + "/" + AppSettings.breathingRestSeries.size() + "  HR " + hrString();
            hintText = "START = hold";
        } else if (state == STATE_HOLD) {
            statusText = "HOLD";
            bigText = Common.formatTime(currentTime);
            infoText = "CYCLE " + (cycleCount + 1) + "/" + AppSettings.breathingRestSeries.size() + "  HR " + hrString();
            hintText = "START = break";
        } else if (state == STATE_DONE) {
            statusText = "FINISHED";
            bigText = Common.formatTime(totalHoldTime);
            infoText = "CYCLES " + cycleCount + "  HR " + hrString();
            hintText = "BACK = menu";
        }

        dc.drawText(cx, y, Graphics.FONT_SMALL, statusText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, y + 36, Graphics.FONT_NUMBER_HOT, bigText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, y + 100, Graphics.FONT_SMALL, infoText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, dc.getHeight() - 22, Graphics.FONT_TINY, hintText, Graphics.TEXT_JUSTIFY_CENTER);
    }

    function hrString() {
        return (lastHeartRate != null) ? lastHeartRate.toString() : "--";
    }

    function onTimer() {
        lastHeartRate = Common.sampleHeartRate();

        if (state == STATE_REST) {
            var restS = AppSettings.breathingRestSeries[currentRestIndex];
            currentTime--;
            if (currentTime <= 0) {
                startHold();
            }
        } else if (state == STATE_HOLD) {
            currentTime--;
            if (currentTime <= 0) {
                finishHold();
            }
        }

        WatchUi.requestUpdate();
    }

    function adjustTarget(delta) {
        targetHoldS += delta;
        if (targetHoldS < 10) { targetHoldS = 10; }
        if (targetHoldS > 600) { targetHoldS = 600; }
        WatchUi.requestUpdate();
    }

    function startRest() {
        if (currentRestIndex >= AppSettings.breathingRestSeries.size()) {
            finishAll();
            return;
        }
        state = STATE_REST;
        currentTime = AppSettings.breathingRestSeries[currentRestIndex];
    }

    function startHold() {
        state = STATE_HOLD;
        currentTime = targetHoldS;
        if (session == null) {
            startRecording();
        }
    }

    function finishHold() {
        totalHoldTime += targetHoldS;
        cycleCount++;

        // Optional: add lap with hold/rest times.
        if (session != null) {
            session.addLap();
        }

        currentRestIndex++;
        if (currentRestIndex >= AppSettings.breathingRestSeries.size()) {
            finishAll();
        } else {
            startRest();
        }
    }

    function finishAll() {
        if (session != null && session.isRecording()) {
            if (disciplineField != null) { disciplineField.setData("BREATHE"); }
            if (targetField != null) { targetField.setData(targetHoldS); }
            if (cyclesField != null) { cyclesField.setData(cycleCount); }
            if (totalHoldField != null) { totalHoldField.setData(totalHoldTime); }
            if (lastHrField != null && lastHeartRate != null) { lastHrField.setData(lastHeartRate); }
            session.stop();
            session.save();
        }
        timer.stop();
        state = STATE_DONE;
        WatchUi.requestUpdate();
    }

    function startRecording() {
        session = ActivityRecording.createSession({
            :name => "BreathingTrainer",
            :sport => ActivityRecording.SPORT_TRAINING,
            :subSport => ActivityRecording.SUB_SPORT_CARDIO_TRAINING
        });

        disciplineField = session.createField(
            "discipline", 0, FitContributor.DATA_TYPE_STRING,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => ""}
        );

        targetField = session.createField(
            "hold_target_s", 15, FitContributor.DATA_TYPE_UINT16,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "s"}
        );

        cyclesField = session.createField(
            "cycles_completed", 13, FitContributor.DATA_TYPE_UINT8,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "cycles"}
        );

        totalHoldField = session.createField(
            "total_hold_time_s", 14, FitContributor.DATA_TYPE_UINT16,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "s"}
        );

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

        session.start();
    }

    function discardSession() {
        if (session != null && session.isRecording()) {
            session.stop();
            session.discard();
        }
        session = null;
        if (timer != null) { timer.stop(); }
        WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
    }

    function onStartButton() {
        if (state == STATE_SETUP_TARGET) {
            state = STATE_SETUP_REST;
        } else if (state == STATE_SETUP_REST) {
            currentRestIndex = 0;
            cycleCount = 0;
            totalHoldTime = 0;
            startRest();
        } else if (state == STATE_REST) {
            startHold();
        } else if (state == STATE_HOLD) {
            finishHold();
        } else if (state == STATE_DONE) {
            WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
        }
        WatchUi.requestUpdate();
    }
}


Successfully wrote 7698 bytes to /home/liph/code/garmin-freedive-app/source/BreathingView.mc


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

class BreathingDelegate 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_SETUP_TARGET) {
                view.adjustTarget(5);
            }
            return true;
        }

        if (key == WatchUi.KEY_DOWN) {
            if (view.state == view.STATE_SETUP_TARGET) {
                view.adjustTarget(-5);
            }
            return true;
        }

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

        return false;
    }

    function onKey(keyEvent) {
        var key = keyEvent.getKey();
        if (key == WatchUi.KEY_START || key == WatchUi.KEY_ENTER ||
            key == WatchUi.KEY_UP || key == WatchUi.KEY_DOWN ||
            key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            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;
                if (view.state == view.STATE_SETUP_TARGET || view.state == view.STATE_SETUP_REST) {
                    WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
                }
            }
            return true;
        }

        return false;
    }

    function onBackLongPress() {
        backTimer = null;
        // TODO: open settings view.
    }
}


Successfully wrote 2403 bytes to /home/liph/code/garmin-freedive-app/source/BreathingDelegate.mc


Now I need to create a build script for fēnix 5 and update the README. Also, I should check if there are any obvious syntax issues. Let me also create a settings view for the long-press BACK from breath-up states, but that can be TODO for now. The user wants the menu system first, and we have it.

Let me create build_fenix5.sh.

#!/bin/bash
# Build script for the fēnix 5 Freedive app (v2 menu + depth build).
#
# Prerequisites:
#   1. Install the Connect IQ SDK Manager (https://developer.garmin.com/connect-iq/sdk/).
#   2. Use the SDK Manager to download the latest SDK and the fēnix 5 / 5S / 5X / 5 Plus device files.
#   3. Make sure ~/.Garmin/ConnectIQ/current-sdk.cfg points to the active SDK.
#   4. Java 11+ must be installed.

set -e

SDK_CFG="$HOME/.Garmin/ConnectIQ/current-sdk.cfg"

if [ ! -f "$SDK_CFG" ]; then
    echo "Connect IQ SDK not found."
    echo "Install the SDK Manager, download an SDK + devices, then try again."
    exit 1
fi

SDK=$(cat "$SDK_CFG")
DEV_KEY="${DEV_KEY:-developer_key.der}"

# Generate a self-signed developer key if the user does not have one.
if [ ! -f "$DEV_KEY" ]; then
    echo "Generating a new developer key ($DEV_KEY)..."
    openssl genrsa -out developer_key.pem 4096
    openssl pkcs8 -topk8 -inform PEM -outform DER -in developer_key.pem -out "$DEV_KEY" -nocrypt
fi

mkdir -p bin

echo "Building for fēnix 5..."
"$SDK/bin/monkeyc" \
    -d fenix5 \
    -f monkey_fenix5.jungle \
    -o bin/Freedive_fenix5.prg \
    -y "$DEV_KEY"

echo "Built: bin/Freedive_fenix5.prg"

echo ""
echo "To sideload, copy the .PRG for your watch to GARMIN/APPS/ over USB."


Successfully wrote 1279 bytes to /home/liph/code/garmin-freedive-app/build_fenix5.sh


bash

chmod +x /home/liph/code/garmin-freedive-app/build_fenix5.sh && ls -la /home/liph/code/garmin-freedive-app/source/*.mc


-rw-r--r-- 1 liph liph 2355 Jul 27 00:23 /home/liph/code/garmin-freedive-app/source/AppSettings.mc
-rw-r--r-- 1 liph liph  670 Jul 26 23:53 /home/liph/code/garmin-freedive-app/source/BreathHoldApp.mc
-rw-r--r-- 1 liph liph 2780 Jul 26 23:58 /home/liph/code/garmin-freedive-app/source/BreathHoldDelegate.mc
-rw-r--r-- 1 liph liph 8514 Jul 26 23:57 /home/liph/code/garmin-freedive-app/source/BreathHoldView.mc
-rw-r--r-- 1 liph liph 2403 Jul 27 00:25 /home/liph/code/garmin-freedive-app/source/BreathingDelegate.mc
-rw-r--r-- 1 liph liph 7698 Jul 27 00:25 /home/liph/code/garmin-freedive-app/source/BreathingView.mc
-rw-r--r-- 1 liph liph  561 Jul 27 00:23 /home/liph/code/garmin-freedive-app/source/Common.mc
-rw-r--r-- 1 liph liph 2276 Jul 27 00:24 /home/liph/code/garmin-freedive-app/source/DepthDelegate.mc
-rw-r--r-- 1 liph liph 1130 Jul 27 00:24 /home/liph/code/garmin-freedive-app/source/DepthModel.mc
-rw-r--r-- 1 liph liph 9210 Jul 27 00:24 /home/liph/code/garmin-freedive-app/source/DepthView.mc
-rw-r--r-- 1 liph liph 2163 Jul 27 00:24 /home/liph/code/garmin-freedive-app/source/DistanceDelegate.mc
-rw-r--r-- 1 liph liph 7945 Jul 27 00:24 /home/liph/code/garmin-freedive-app/source/DistanceView.mc
-rw-r--r-- 1 liph liph  548 Jul 27 00:23 /home/liph/code/garmin-freedive-app/source/Fenix5App.mc
-rw-r--r-- 1 liph liph 1219 Jul 27 00:23 /home/liph/code/garmin-freedive-app/source/MainMenuDelegate.mc
-rw-r--r-- 1 liph liph 3919 Jul 27 00:23 /home/liph/code/garmin-freedive-app/source/MainMenuView.mc
-rw-r--r-- 1 liph liph 1618 Jul 26 23:58 /home/liph/code/garmin-freedive-app/source/PoolLengthDelegate.mc
-rw-r--r-- 1 liph liph 3051 Jul 26 23:53 /home/liph/code/garmin-freedive-app/source/PoolLengthView.mc
-rw-r--r-- 1 liph liph  680 Jul 26 23:56 /home/liph/code/garmin-freedive-app/source/Settings.mc
-rw-r--r-- 1 liph liph 1885 Jul 27 00:25 /home/liph/code/garmin-freedive-app/source/StaticDelegate.mc
-rw-r--r-- 1 liph liph 6330 Jul 27 00:24 /home/liph/code/garmin-freedive-app/source/StaticView.mc
-rw-r--r-- 1 liph liph 1200 Jul 27 00:24 /home/liph/code/garmin-freedive-app/source/SubMenuDelegate.mc
-rw-r--r-- 1 liph liph 1534 Jul 27 00:24 /home/liph/code/garmin-freedive-app/source/SubMenuView.mc


Now I need to think about whether the code will compile. There are several potential issues:

1. In MainMenuView, I pass method references to SubMenuView. The `onDepthType`, `onCwtFinStyle`, `onDistanceType` are instance methods of MainMenuView. When SubMenuView calls `onSelect.invoke(selectedIndex)`, it will call the method with `selectedIndex` as argument. But method references in Monkey C - I need to make sure this works. The `method(:onDepthType)` syntax creates a Method object bound to the current instance? I think so.

2. In MainMenuView, when creating SubMenuView with `new SubMenuView(...)`, the delegate is `new SubMenuDelegate()`. But SubMenuDelegate has a `setView` method that isn't called. The SubMenuView won't be connected to the delegate. This is a bug. I need to either:
   - Pass the view to the delegate constructor
   - Or have the delegate get the view from the pushView call somehow

   In the current code, `SubMenuDelegate` has `setView(v)` but it's never called. So `onKeyPressed` will have `view == null` and do nothing. I need to fix this by creating the view first, then the delegate with the view.

3. Similarly, in Fenix5App.getInitialView(), I return `[new MainMenuView(), new MainMenuDelegate()]`. But MainMenuDelegate also has `setView` that isn't called. However, the system might automatically connect the view and delegate? Actually no, the delegate's `view` will be null. I need to either:
   - Create the view, pass it to delegate, return both
   - Or have the delegate constructor take the view

4. In MainMenuView.launchDepth/launchDistance/etc., I create new view and delegate pairs but pass `new DepthView(discipline)` to both, which creates two separate instances. This is wrong - I should create one view instance and pass it to the delegate. Same issue that was fixed earlier in BreathHoldDelegate.

5. `ActivityRecording.SPORT_SWIMMING` and `SUB_SPORT_OPEN_WATER` / `SUB_SPORT_LAP_SWIMMING` - these exist in API 2.4.0? ActivityRecording sport enums are API 1.0.0, so yes. The `SPORT_SWIMMING` value is 5, `SUB_SPORT_OPEN_WATER` is 18, `SUB_SPORT_LAP_SWIMMING` is 17, `SUB_SPORT_CARDIO_TRAINING` is 26. These should all be available.

6. `FitContributor.DATA_TYPE_STRING` - is this available in API 2.4.0? Yes, FitContributor is API 1.3.0.

7. In `DepthView.finishSession()`, I do `(maxDepthM * 10).toNumber() / 10`. This converts to integer and back to number. But `maxDepthField` is UINT16, so it expects integer. This expression may not work as intended. Actually `(maxDepthM * 10).toNumber()` gives an integer like 243 for 24.3m, then `/ 10` gives 24.3 as a Number, but UINT16 expects integer. So this would fail or be truncated. Better to store max depth as an integer in decimeters or just store meters as integer. Or use FLOAT for max_depth_m. Since depth can be fractional, I should change max_depth_m to FLOAT. But then it can't be UINT16. Let me use FLOAT for max_depth_m to preserve 0.1m precision.

8. In `DepthView`, the `disciplineField` uses `FitContributor.DATA_TYPE_STRING`. Is string supported as developer field? According to docs, yes.

9. The `invoke()` method on Method - does it exist? In Monkey C, Method objects have `invoke(args...)`. Yes.

10. `Array.size()` - is this the right method? In Monkey C, arrays have `.size()` method. Yes.

11. In `MainMenuView`, `ITEMS.size()` - should work.

12. In `DistanceView`, the contraction counter placeholder doesn't increment anything if distanceDownAction == 1. Should add a contractionCount field. But for now it's a placeholder, which is fine but maybe I should add it.

13. In `BreathingView`, I'm counting down in `onTimer` for REST and HOLD. currentTime starts at the rest/hold duration and decrements. But the display shows `Common.formatTime(currentTime)`. That works. However, in `STATE_REST` when `currentTime <= 0`, it calls `startHold()`. But `startHold()` sets currentTime = targetHoldS. Then in the same timer tick, `onTimer` continues with `state == STATE_HOLD` and decrements currentTime. That could cause it to skip one second display. Not a critical bug but worth noting.

14. In `BreathingView.finishHold()`, after `session.addLap()`, I don't set lap fields. That's TODO but fine.

15. In `DepthView`, I create a new DepthView in `launchDepth` and pass a different one to delegate. Need to fix.

16. The `MainMenuDelegate` needs the view. Need to fix `Fenix5App` to connect them.

17. `SubMenuView` needs the delegate connected with the view instance. Need to fix `MainMenuView`.

These are fixable. Let me make the fixes.

Also, I should note that I haven't actually compiled this because `monkeyc` isn't installed. So I need to be extra careful.

Let me fix the critical issues:
1. Fenix5App: create view and delegate, set view on delegate
2. MainMenuView: create single view instances, pass to delegate; create sub-menu view and delegate properly
3. SubMenuDelegate: constructor should take view
4. MainMenuDelegate: constructor should take view
5. Fix maxDepthField type to FLOAT
6. Add contractionCount to DistanceView if needed

Let me make these edits.

edit

    function getInitialView() {
        var view = new MainMenuView();
        var delegate = new MainMenuDelegate();
        delegate.setView(view);
        return [view, delegate];
    }

    function getInitialView() {
        return [new MainMenuView(), new MainMenuDelegate()];
    }


Successfully replaced 1 block(s) in /home/liph/code/garmin-freedive-app/source/Fenix5App.mc.

    ...
 20         // Nothing to clean up explicitly.
 21     }
 22
 23     function getInitialView() {
-24         return [new MainMenuView(), new MainMenuDelegate()];
+24         var view = new MainMenuView();
+25         var delegate = new MainMenuDelegate();
+26         delegate.setView(view);
+27         return [view, delegate];
 25     }
 26 }

--- /home/liph/code/garmin-freedive-app/source/Fenix5App.mc
+++ /home/liph/code/garmin-freedive-app/source/Fenix5App.mc
@@ -20,7 +20,10 @@
         // Nothing to clean up explicitly.
     }

     function getInitialView() {
-        return [new MainMenuView(), new MainMenuDelegate()];
+        var view = new MainMenuView();
+        var delegate = new MainMenuDelegate();
+        delegate.setView(view);
+        return [view, delegate];
     }
 }


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

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

    function setView(v) {
        view = v;
    }


Successfully replaced 1 block(s) in /home/liph/code/garmin-freedive-app/source/MainMenuDelegate.mc.

    ...
  4 class MainMenuDelegate extends WatchUi.InputDelegate {
  5
  6     var view;
  7
- 8     function initialize() {
+ 8     function initialize(v) {
  9         InputDelegate.initialize();
-10         view = null;
-11     }
-12
-13     function setView(v) {
 14         view = v;
 15     }
 16
 17     function onKeyPressed(keyEvent) {
    ...

--- /home/liph/code/garmin-freedive-app/source/MainMenuDelegate.mc
+++ /home/liph/code/garmin-freedive-app/source/MainMenuDelegate.mc
@@ -4,14 +4,10 @@
 class MainMenuDelegate extends WatchUi.InputDelegate {

     var view;

-    function initialize() {
+    function initialize(v) {
         InputDelegate.initialize();
-        view = null;
-    }
-
-    function setView(v) {
         view = v;
     }

     function onKeyPressed(keyEvent) {


Successfully replaced 1 block(s) in /home/liph/code/garmin-freedive-app/source/SubMenuDelegate.mc.

    ...
  3 class SubMenuDelegate extends WatchUi.InputDelegate {
  4
  5     var view;
  6
- 7     function initialize() {
+ 7     function initialize(v) {
  8         InputDelegate.initialize();
- 9         view = null;
-10     }
-11
-12     function setView(v) {
 13         view = v;
 14     }
 15
 16     function onKeyPressed(keyEvent) {
    ...

--- /home/liph/code/garmin-freedive-app/source/SubMenuDelegate.mc
+++ /home/liph/code/garmin-freedive-app/source/SubMenuDelegate.mc
@@ -3,14 +3,10 @@
 class SubMenuDelegate extends WatchUi.InputDelegate {

     var view;

-    function initialize() {
+    function initialize(v) {
         InputDelegate.initialize();
-        view = null;
-    }
-
-    function setView(v) {
         view = v;
     }

     function onKeyPressed(keyEvent) {


        if (selectedIndex == ITEM_DEPTH) {
            var subView = new SubMenuView("DEPTH TYPE", ["Constant Weight", "Free Immersion"], method(:onDepthType));
            WatchUi.pushView(subView, new SubMenuDelegate(subView), WatchUi.SLIDE_LEFT);
        } else if (selectedIndex == ITEM_DISTANCE) {
            var subView2 = new SubMenuView("DIST FIN TYPE", ["Bi-fins", "Monofin", "No-fins"], method(:onDistanceType));
            WatchUi.pushView(subView2, new SubMenuDelegate(subView2), WatchUi.SLIDE_LEFT);
        } else if (selectedIndex == ITEM_STATIC) {

        if (selectedIndex == ITEM_DEPTH) {
            WatchUi.pushView(
                new SubMenuView("DEPTH TYPE", ["Constant Weight", "Free Immersion"], method(:onDepthType)),
                new SubMenuDelegate(),
                WatchUi.SLIDE_LEFT);
        } else if (selectedIndex == ITEM_DISTANCE) {
            WatchUi.pushView(
                new SubMenuView("DIST FIN TYPE", ["Bi-fins", "Monofin", "No-fins"], method(:onDistanceType)),
                new SubMenuDelegate(),
                WatchUi.SLIDE_LEFT);
        } else if (selectedIndex == ITEM_STATIC) {

        if (discipline.equals("CWT")) {
            var subView = new SubMenuView("CWT FIN STYLE", ["Bi-fins", "Monofin", "No-fins"], method(:onCwtFinStyle));
            WatchUi.pushView(subView, new SubMenuDelegate(subView), WatchUi.SLIDE_LEFT);
        } else {
            launchDepth(discipline);
        }

        if (discipline.equals("CWT")) {
            WatchUi.pushView(
                new SubMenuView("CWT FIN STYLE", ["Bi-fins", "Monofin", "No-fins"], method(:onCwtFinStyle)),
                new SubMenuDelegate(),
                WatchUi.SLIDE_LEFT);
        } else {
            launchDepth(discipline);
        }

    function launchDepth(discipline) {
        var view = new DepthView(discipline);
        WatchUi.switchToView(view, new DepthDelegate(view), WatchUi.SLIDE_LEFT);
    }

    function launchDistance(discipline) {
        var view = new DistanceView(discipline);
        WatchUi.switchToView(view, new DistanceDelegate(view), WatchUi.SLIDE_LEFT);
    }

    function launchStatic() {
        var view = new StaticView();
        WatchUi.switchToView(view, new StaticDelegate(view), WatchUi.SLIDE_LEFT);
    }

    function launchBreathing() {
        var view = new BreathingView();
        WatchUi.switchToView(view, new BreathingDelegate(view), WatchUi.SLIDE_LEFT);
    }

    function launchDepth(discipline) {
        WatchUi.switchToView(new DepthView(discipline), new DepthDelegate(new DepthView(discipline)), WatchUi.SLIDE_LEFT);
    }

    function launchDistance(discipline) {
        WatchUi.switchToView(new DistanceView(discipline), new DistanceDelegate(new DistanceView(discipline)), WatchUi.SLIDE_LEFT);
    }

    function launchStatic() {
        WatchUi.switchToView(new StaticView(), new StaticDelegate(new StaticView()), WatchUi.SLIDE_LEFT);
    }

    function launchBreathing() {
        WatchUi.switchToView(new BreathingView(), new BreathingDelegate(new BreathingView()), WatchUi.SLIDE_LEFT);
    }


Successfully replaced 3 block(s) in /home/liph/code/garmin-freedive-app/source/MainMenuView.mc.

     ...
  51     }
  52
  53     function select() {
  54         if (selectedIndex == ITEM_DEPTH) {
- 55             WatchUi.pushView(
- 56                 new SubMenuView("DEPTH TYPE", ["Constant Weight", "Free Immersion"], method(:onDepthType)),
- 57                 new SubMenuDelegate(),
- 58                 WatchUi.SLIDE_LEFT);
+ 55             var subView = new SubMenuView("DEPTH TYPE", ["Constant Weight", "Free Immersion"], method(:onDepthType));
+ 56             WatchUi.pushView(subView, new SubMenuDelegate(subView), WatchUi.SLIDE_LEFT);
  59         } else if (selectedIndex == ITEM_DISTANCE) {
- 60             WatchUi.pushView(
- 61                 new SubMenuView("DIST FIN TYPE", ["Bi-fins", "Monofin", "No-fins"], method(:onDistanceType)),
- 62                 new SubMenuDelegate(),
- 63                 WatchUi.SLIDE_LEFT);
+ 58             var subView2 = new SubMenuView("DIST FIN TYPE", ["Bi-fins", "Monofin", "No-fins"], method(:onDistanceType));
+ 59             WatchUi.pushView(subView2, new SubMenuDelegate(subView2), WatchUi.SLIDE_LEFT);
  64         } else if (selectedIndex == ITEM_STATIC) {
  65             launchStatic();
  66         } else if (selectedIndex == ITEM_BREATHING) {
  67             launchBreathing();
     ...
  76             discipline = "FIM";
  77         }
  78         // If Constant Weight, ask for fin style before launching depth view.
  79         if (discipline.equals("CWT")) {
- 80             WatchUi.pushView(
- 81                 new SubMenuView("CWT FIN STYLE", ["Bi-fins", "Monofin", "No-fins"], method(:onCwtFinStyle)),
- 82                 new SubMenuDelegate(),
- 83                 WatchUi.SLIDE_LEFT);
+ 76             var subView = new SubMenuView("CWT FIN STYLE", ["Bi-fins", "Monofin", "No-fins"], method(:onCwtFinStyle));
+ 77             WatchUi.pushView(subView, new SubMenuDelegate(subView), WatchUi.SLIDE_LEFT);
  84         } else {
  85             launchDepth(discipline);
  86         }
  87     }
     ...
 102         launchDistance(discipline);
 103     }
 104
 105     function launchDepth(discipline) {
-106         WatchUi.switchToView(new DepthView(discipline), new DepthDelegate(new DepthView(discipline)), WatchUi.SLIDE_LEFT);
+100         var view = new DepthView(discipline);
+101         WatchUi.switchToView(view, new DepthDelegate(view), WatchUi.SLIDE_LEFT);
 107     }
 108
 109     function launchDistance(discipline) {
-110         WatchUi.switchToView(new DistanceView(discipline), new DistanceDelegate(new DistanceView(discipline)), WatchUi.SLIDE_LEFT);
+105         var view = new DistanceView(discipline);
+106         WatchUi.switchToView(view, new DistanceDelegate(view), WatchUi.SLIDE_LEFT);
 111     }
 112
 113     function launchStatic() {
-114         WatchUi.switchToView(new StaticView(), new StaticDelegate(new StaticView()), WatchUi.SLIDE_LEFT);
+110         var view = new StaticView();
+111         WatchUi.switchToView(view, new StaticDelegate(view), WatchUi.SLIDE_LEFT);
 115     }
 116
 117     function launchBreathing() {
-118         WatchUi.switchToView(new BreathingView(), new BreathingDelegate(new BreathingView()), WatchUi.SLIDE_LEFT);
+115         var view = new BreathingView();
+116         WatchUi.switchToView(view, new BreathingDelegate(view), WatchUi.SLIDE_LEFT);
 119     }
 120 }

--- /home/liph/code/garmin-freedive-app/source/MainMenuView.mc
+++ /home/liph/code/garmin-freedive-app/source/MainMenuView.mc
@@ -51,17 +51,13 @@
     }

     function select() {
         if (selectedIndex == ITEM_DEPTH) {
-            WatchUi.pushView(
-                new SubMenuView("DEPTH TYPE", ["Constant Weight", "Free Immersion"], method(:onDepthType)),
-                new SubMenuDelegate(),
-                WatchUi.SLIDE_LEFT);
+            var subView = new SubMenuView("DEPTH TYPE", ["Constant Weight", "Free Immersion"], method(:onDepthType));
+            WatchUi.pushView(subView, new SubMenuDelegate(subView), WatchUi.SLIDE_LEFT);
         } else if (selectedIndex == ITEM_DISTANCE) {
-            WatchUi.pushView(
-                new SubMenuView("DIST FIN TYPE", ["Bi-fins", "Monofin", "No-fins"], method(:onDistanceType)),
-                new SubMenuDelegate(),
-                WatchUi.SLIDE_LEFT);
+            var subView2 = new SubMenuView("DIST FIN TYPE", ["Bi-fins", "Monofin", "No-fins"], method(:onDistanceType));
+            WatchUi.pushView(subView2, new SubMenuDelegate(subView2), WatchUi.SLIDE_LEFT);
         } else if (selectedIndex == ITEM_STATIC) {
             launchStatic();
         } else if (selectedIndex == ITEM_BREATHING) {
             launchBreathing();
@@ -76,12 +72,10 @@
             discipline = "FIM";
         }
         // If Constant Weight, ask for fin style before launching depth view.
         if (discipline.equals("CWT")) {
-            WatchUi.pushView(
-                new SubMenuView("CWT FIN STYLE", ["Bi-fins", "Monofin", "No-fins"], method(:onCwtFinStyle)),
-                new SubMenuDelegate(),
-                WatchUi.SLIDE_LEFT);
+            var subView = new SubMenuView("CWT FIN STYLE", ["Bi-fins", "Monofin", "No-fins"], method(:onCwtFinStyle));
+            WatchUi.pushView(subView, new SubMenuDelegate(subView), WatchUi.SLIDE_LEFT);
         } else {
             launchDepth(discipline);
         }
     }
@@ -102,19 +96,23 @@
         launchDistance(discipline);
     }

     function launchDepth(discipline) {
-        WatchUi.switchToView(new DepthView(discipline), new DepthDelegate(new DepthView(discipline)), WatchUi.SLIDE_LEFT);
+        var view = new DepthView(discipline);
+        WatchUi.switchToView(view, new DepthDelegate(view), WatchUi.SLIDE_LEFT);
     }

     function launchDistance(discipline) {
-        WatchUi.switchToView(new DistanceView(discipline), new DistanceDelegate(new DistanceView(discipline)), WatchUi.SLIDE_LEFT);
+        var view = new DistanceView(discipline);
+        WatchUi.switchToView(view, new DistanceDelegate(view), WatchUi.SLIDE_LEFT);
     }

     function launchStatic() {
-        WatchUi.switchToView(new StaticView(), new StaticDelegate(new StaticView()), WatchUi.SLIDE_LEFT);
+        var view = new StaticView();
+        WatchUi.switchToView(view, new StaticDelegate(view), WatchUi.SLIDE_LEFT);
     }

     function launchBreathing() {
-        WatchUi.switchToView(new BreathingView(), new BreathingDelegate(new BreathingView()), WatchUi.SLIDE_LEFT);
+        var view = new BreathingView();
+        WatchUi.switchToView(view, new BreathingDelegate(view), WatchUi.SLIDE_LEFT);
     }
 }


            maxDepthField = session.createField(
                "max_depth_m", 3, FitContributor.DATA_TYPE_FLOAT,
                {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "m"}
            );

            maxDepthField = session.createField(
                "max_depth_m", 3, FitContributor.DATA_TYPE_UINT16,
                {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "m"}
            );

if (maxDepthField != null) { maxDepthField.setData(maxDepthM); }

if (maxDepthField != null) { maxDepthField.setData((maxDepthM * 10).toNumber() / 10); }


Successfully replaced 2 block(s) in /home/liph/code/garmin-freedive-app/source/DepthView.mc.

     ...
 178
 179     function finishSession() {
 180         if (session != null && session.isRecording()) {
 181             if (disciplineField != null) { disciplineField.setData(discipline); }
-182             if (maxDepthField != null) { maxDepthField.setData((maxDepthM * 10).toNumber() / 10); }
+182             if (maxDepthField != null) { maxDepthField.setData(maxDepthM); }
 183             if (diveTimeField != null) { diveTimeField.setData(diveTime); }
 184             if (surfaceIntervalField != null) { surfaceIntervalField.setData(surfaceIntervalTime); }
 185             if (breathUpField != null) { breathUpField.setData(breathUpTime); }
 186             if (lastHrField != null && lastHeartRate != null) { lastHrField.setData(lastHeartRate); }
     ...
 215                 {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => ""}
 216             );
 217
 218             maxDepthField = session.createField(
-219                 "max_depth_m", 3, FitContributor.DATA_TYPE_UINT16,
+219                 "max_depth_m", 3, FitContributor.DATA_TYPE_FLOAT,
 220                 {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "m"}
 221             );
 222
 223             diveTimeField = session.createField(
     ...

--- /home/liph/code/garmin-freedive-app/source/DepthView.mc
+++ /home/liph/code/garmin-freedive-app/source/DepthView.mc
@@ -178,9 +178,9 @@

     function finishSession() {
         if (session != null && session.isRecording()) {
             if (disciplineField != null) { disciplineField.setData(discipline); }
-            if (maxDepthField != null) { maxDepthField.setData((maxDepthM * 10).toNumber() / 10); }
+            if (maxDepthField != null) { maxDepthField.setData(maxDepthM); }
             if (diveTimeField != null) { diveTimeField.setData(diveTime); }
             if (surfaceIntervalField != null) { surfaceIntervalField.setData(surfaceIntervalTime); }
             if (breathUpField != null) { breathUpField.setData(breathUpTime); }
             if (lastHrField != null && lastHeartRate != null) { lastHrField.setData(lastHeartRate); }
@@ -215,9 +215,9 @@
                 {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => ""}
             );

             maxDepthField = session.createField(
-                "max_depth_m", 3, FitContributor.DATA_TYPE_UINT16,
+                "max_depth_m", 3, FitContributor.DATA_TYPE_FLOAT,
                 {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "m"}
             );

             diveTimeField = session.createField(


    var contractionCount = 0;
    var horizontalDistanceM = 0;
    var lastHeartRate = null;

    function onDownButton() {
        if (state == STATE_HOLD) {
            if (AppSettings.distanceDownAction == 0) {
                strokeCount++;
            } else {
                contractionCount++;
            }
            WatchUi.requestUpdate();
        }
    }

    function onDownButton() {
        if (state == STATE_HOLD) {
            if (AppSettings.distanceDownAction == 0) {
                strokeCount++;
            } else {
                // contraction counter placeholder
            }
            WatchUi.requestUpdate();
        }
    }


Successfully replaced 2 block(s) in /home/liph/code/garmin-freedive-app/source/DistanceView.mc.

     ...
  32     var recoveryTime = 0;
  33     var maxHoldTime = 0;
  34     var lengthCount = 0;
  35     var strokeCount = 0;
+ 36     var contractionCount = 0;
  36     var horizontalDistanceM = 0;
  37     var lastHeartRate = null;
  38
  39     function initialize(disc) {
     ...
 231         if (state == STATE_HOLD) {
 232             if (AppSettings.distanceDownAction == 0) {
 233                 strokeCount++;
 234             } else {
-235                 // contraction counter placeholder
+236                 contractionCount++;
 236             }
 237             WatchUi.requestUpdate();
 238         }
 239     }
     ...

--- /home/liph/code/garmin-freedive-app/source/DistanceView.mc
+++ /home/liph/code/garmin-freedive-app/source/DistanceView.mc
@@ -32,8 +32,9 @@
     var recoveryTime = 0;
     var maxHoldTime = 0;
     var lengthCount = 0;
     var strokeCount = 0;
+    var contractionCount = 0;
     var horizontalDistanceM = 0;
     var lastHeartRate = null;

     function initialize(disc) {
@@ -231,9 +232,9 @@
         if (state == STATE_HOLD) {
             if (AppSettings.distanceDownAction == 0) {
                 strokeCount++;
             } else {
-                // contraction counter placeholder
+                contractionCount++;
             }
             WatchUi.requestUpdate();
         }
     }


Now I need to think about more potential issues. Let me review the MainMenuView after edits.


using Toybox.WatchUi;
using Toybox.Graphics;
using Toybox.System;

class MainMenuView extends WatchUi.View {

    const ITEM_DEPTH      = 0;
    const ITEM_DISTANCE   = 1;
    const ITEM_STATIC     = 2;
    const ITEM_BREATHING  = 3;

    const ITEMS = ["Depth", "Distance", "Static Apnoe", "Breathing Tr."];

    var selectedIndex = 0;

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

    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, "FREEDIVE MODE", Graphics.TEXT_JUSTIFY_CENTER);

        var itemY = y + 40;
        for (var i = 0; i < ITEMS.size(); i++) {
            var prefix = (i == selectedIndex) ? "> " : "  ";
            dc.drawText(cx, itemY, Graphics.FONT_MEDIUM, prefix + ITEMS[i], Graphics.TEXT_JUSTIFY_CENTER);
            itemY += 34;
        }

        dc.drawText(cx, dc.getHeight() - 20, Graphics.FONT_TINY,
            "UP/DOWN=move START=start", Graphics.TEXT_JUSTIFY_CENTER);
    }

    function moveUp() {
        selectedIndex--;
        if (selectedIndex < 0) { selectedIndex = ITEMS.size() - 1; }
        WatchUi.requestUpdate();
    }

    function moveDown() {
        selectedIndex++;
        if (selectedIndex >= ITEMS.size()) { selectedIndex = 0; }
        WatchUi.requestUpdate();
    }

    function select() {
        if (selectedIndex == ITEM_DEPTH) {
            var subView = new SubMenuView("DEPTH TYPE", ["Constant Weight", "Free Immersion"], method(:onDepthType));
            WatchUi.pushView(subView, new SubMenuDelegate(subView), WatchUi.SLIDE_LEFT);
        } else if (selectedIndex == ITEM_DISTANCE) {
            var subView2 = new SubMenuView("DIST FIN TYPE", ["Bi-fins", "Monofin", "No-fins"], method(:onDistanceType));
            WatchUi.pushView(subView2, new SubMenuDelegate(subView2), WatchUi.SLIDE_LEFT);
        } else if (selectedIndex == ITEM_STATIC) {
            launchStatic();
        } else if (selectedIndex == ITEM_BREATHING) {
            launchBreathing();
        }
    }

    function onDepthType(index) {
        var discipline;
        if (index == 0) {
            discipline = "CWT";
        } else {
            discipline = "FIM";
        }
        // If Constant Weight, ask for fin style before launching depth view.
        if (discipline.equals("CWT")) {
            var subView = new SubMenuView("CWT FIN STYLE", ["Bi-fins", "Monofin", "No-fins"], method(:onCwtFinStyle));
            WatchUi.pushView(subView, new SubMenuDelegate(subView), WatchUi.SLIDE_LEFT);
        } else {
            launchDepth(discipline);
        }
    }

    function onCwtFinStyle(index) {
        var discipline;
        if (index == 0) { discipline = "CWT-B"; }
        else if (index == 1) { discipline = "CWT-M"; }
        else { discipline = "CNF"; }
        launchDepth(discipline);
    }

    function onDistanceType(index) {
        var discipline;
        if (index == 0) { discipline = "DYN-B"; }
        else if (index == 1) { discipline = "DYN-M"; }
        else { discipline = "DNF"; }
        launchDistance(discipline);
    }

    function launchDepth(discipline) {
        var view = new DepthView(discipline);
        WatchUi.switchToView(view, new DepthDelegate(view), WatchUi.SLIDE_LEFT);
    }

    function launchDistance(discipline) {
        var view = new DistanceView(discipline);
        WatchUi.switchToView(view, new DistanceDelegate(view), WatchUi.SLIDE_LEFT);
    }

    function launchStatic() {
        var view = new StaticView();
        WatchUi.switchToView(view, new StaticDelegate(view), WatchUi.SLIDE_LEFT);
    }

    function launchBreathing() {
        var view = new BreathingView();
        WatchUi.switchToView(view, new BreathingDelegate(view), WatchUi.SLIDE_LEFT);
    }
}


Good. Now there are still some potential Monkey C issues:

1. `method(:onDepthType)` - Does this work on instance methods? In Monkey C, `method(:name)` creates a Method reference. It should work.

2. `onSelect.invoke(selectedIndex)` - The `invoke` method on Method objects. Should work.

3. In `SubMenuView`, the `onSelect` field stores a Method. When `select()` calls `onSelect.invoke(selectedIndex)`, it should call the callback. But the callback methods (`onDepthType`, `onCwtFinStyle`, `onDistanceType`) are in MainMenuView. Since the Method reference is bound to the MainMenuView instance, it should work.

4. `discipline.equals("CWT")` - In Monkey C, String has `equals()` method? I think so, but often people use `==` or `equals()`. Let me check. Actually in Monkey C, `==` on strings might compare references, not content. The `String` class has `equals()` method. So `discipline.equals("CWT")` should work.

5. `ITEMS.size()` - arrays have `.size()` method. Good.

6. `dc.getHeight()` and `dc.getWidth()` - yes.

7. `Graphics.FONT_NUMBER_HOT` - does this exist? On fēnix 5 with API 2.4.0, I need to check. The available fonts include FONT_NUMBER_HOT, FONT_NUMBER_THAI_HOT, etc. I believe FONT_NUMBER_HOT exists. But on fēnix 3 it might not. Since this is fēnix 5 build, it should be fine. But to be safe, maybe use FONT_NUMBER_MEDIUM or FONT_NUMBER. Actually FONT_NUMBER_HOT is a valid font resource. Let me verify with web search, but I think it's OK.

Actually, in the v1 code I used FONT_NUMBER_MEDIUM which exists. FONT_NUMBER_HOT also exists on devices with higher resolution. fēnix 5 is 240x240. FONT_NUMBER_HOT should be available. But to be safe, I could use FONT_NUMBER_MEDIUM. Let me keep FONT_NUMBER_HOT for now since fēnix 5 has it.

8. `AppSettings.breathingRestSeries.size()` - array.size() works.

9. In `DistanceView`, I use `Graphics.FONT_NUMBER_MEDIUM` which is safe.

10. `ActivityRecording.SPORT_SWIMMING` etc. - safe.

11. `FitContributor.DATA_TYPE_STRING` - the STRING type might require special handling. Let me check if it's supported for developer fields. The API docs list DATA_TYPE_STRING as a valid type. But writing strings via setData might need to be String objects. discipline is a String. Should be OK.

12. Potential issue: in `DepthView.onUpdate`, I call `waterTypeString()` which is an instance method. That's fine.

13. In `DepthView.onTimer`, I call `updateDepth()` and then `if (depthRecordField != null && currentDepthM != null)`. But currentDepthM is initialized to 0.0, so it's never null. updateDepth sets it to a number or leaves it. The `DepthModel.depthM()` returns null only if surfacePressure is null. But we calibrate before dive. So currentDepthM stays at 0.0 if no reading. Fine.

14. In `DepthView.finishSession`, `maxDepthField.setData(maxDepthM)` with FLOAT type. Good.

15. In `DistanceView`, there's no contractionCount field being saved to FIT. I should add it. But the user wanted strokes or contractions. Since distanceDownAction is configurable, I should save both stroke_count and contraction_count, or save whichever was used. Currently I only have stroke_count field. I should add a contraction_count field and save the right one. Or just save stroke_count and contractionCount separately. Let me add a contractionCount field.

Actually, simpler: have one field `down_count` that saves either strokes or contractions depending on the mode? No, better to have both fields and save 0 for unused. Or save the active one to its own field. Let me add `contraction_count` field.

16. The `DistanceView` doesn't reset contractionCount in onStartButton. Should reset. I have `lengthCount = 0; horizontalDistanceM = 0;` but not contractionCount. Need to add.

17. In `DistanceView.finishSession`, I don't save contractionCount. Need to add.

Let me fix these. Also, I should add a settings view eventually, but that's a bigger task. For now, long-press BACK in breath-up states is TODO.

Let me make the DistanceView fixes.

    var strokeField = null;
    var contractionField = null;
    var holdField = null;

    var strokeField = null;
    var holdField = null;

    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 = 10;

        var statusText = "";
        var bigText = "";
        var infoText = "";
        var hintText = "";

        if (state == STATE_BREATH_UP) {
            statusText = discipline + "  BREATH UP";
            bigText = Common.formatTime(breathUpTime - currentTime);
            infoText = "POOL " + AppSettings.poolLengthM + "m";
            hintText = "START = skip";
        } else if (state == STATE_HOLD) {
            statusText = discipline + "  SWIM";
            bigText = Common.formatTime(currentTime);
            infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m  HR " + hrString();
            hintText = "UP=LAP DOWN=" + downActionString();
        } else if (state == STATE_RECOVERY) {
            statusText = discipline + "  RECOVERY";
            bigText = Common.formatTime(recoveryTime - currentTime);
            infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m  HR " + hrString();
            hintText = "START = save";
        } else if (state == STATE_DONE) {
            statusText = discipline + "  SAVED";
            bigText = Common.formatTime(maxHoldTime);
            infoText = "L" + lengthCount + " D" + horizontalDistanceM + "m  HR " + hrString();
            hintText = "BACK = menu";
        }

        dc.drawText(cx, y, Graphics.FONT_SMALL, statusText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, y + 36, Graphics.FONT_NUMBER_MEDIUM, bigText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, y + 90, Graphics.FONT_SMALL, infoText, Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, dc.getHeight() - 22, Graphics.FONT_TINY, hintText, Graphics.TEXT_JUSTIFY_CENTER);
    }

            strokeField = session.createField(
                "stroke_count", 8, FitContributor.DATA_TYPE_UINT8,
                {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "strokes"}
            );

            contractionField = session.createField(
                "contraction_count", 16, FitContributor.DATA_TYPE_UINT8,
                {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "contractions"}
            );

            if (strokeField != null) { strokeField.setData(strokeCount); }
            if (contractionField != null) { contractionField.setData(contractionCount); }
            if (holdField != null) { holdField.setData(maxHoldTime); }

            if (strokeField != null) { strokeField.setData(strokeCount); }
            if (holdField != null) { holdField.setData(maxHoldTime); }

            state = STATE_HOLD;
            currentTime = 0;
            lengthCount = 0;
            horizontalDistanceM = 0;
            strokeCount = 0;
            contractionCount = 0;
            startRecording();

            state = STATE_HOLD;
            currentTime = 0;
            startRecording();


Could not find edits[2] in /home/liph/code/garmin-freedive-app/source/DistanceView.mc. The oldText must match exactly including all whitespace and newlines.


The edit failed on the third replacement. I think because the second replacement (onUpdate) consumed a lot and the third one might have overlapping context, or the oldText didn't match. Actually I made 5 edits in one call, but the tool says "edits[2]" failed - that's the third edit (0-indexed), which is the strokeField/contractionField replacement. Maybe because the onUpdate replacement changed the file content and the subsequent edits were applied against the original file, and the oldText no longer matched? Wait, no, the edit tool applies all edits against the original file simultaneously. So they shouldn't affect each other.

The third edit oldText was:
```
            strokeField = session.createField(
                "stroke_count", 8, FitContributor.DATA_TYPE_UINT8,
                {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "strokes"}
            );
```

Maybe the actual file has different spacing. Let me read the relevant part of DistanceView to check.


Validation failed for tool "ffgrep":
  - pattern: must have required properties pattern

Received arguments:
{}


        );

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

        breathUpField = session.createField(
            "breath_up_time_s", 10, FitContributor.DATA_TYPE_UINT16,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "s"}
        );

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

        session.start();
    }

    function discardSession() {
        if (session != null && session.isRecording()) {
            session.stop();
            session.discard();
        }
        session = null;
        if (timer != null) { timer.stop(); }
        WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
    }

    function onStartButton() {
        if (state == STATE_BREATH_UP) {
            startHold();
        } else if (state == STATE_HOLD) {
            endHold();
        } else if (state == STATE_RECOVERY) {
            finishSession();
        } else if (state == STATE_DONE) {
            WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
        }
        WatchUi.requestUpdate();
    }

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

    function onDownButton() {
        if (state == STATE_HOLD) {
            if (AppSettings.distanceDownAction == 0) {
                strokeCount++;
            } else {
                contractionCount++;
            }
            WatchUi.requestUpdate();
        }
    }
}


The first edit (adding contractionField) probably succeeded, and the second edit (onUpdate) succeeded, but the third edit (replacing strokeField block) failed because the first edit already changed the file? No, all edits are against the original file. But the first edit was to add `contractionField = null;` before `holdField = null;`. The third edit was to replace the strokeField createField block. These are in different parts of the file. The failure of edits[2] (third edit) means the oldText didn't match. But it should match based on the original file.

Wait, maybe the first edit DID succeed, and then the third edit's oldText matched the original, but the first edit changed `var strokeField = null; var holdField = null;` to `var strokeField = null; var contractionField = null; var holdField = null;`. That shouldn't affect the createField block.

Let me read the createField section of the current file after the partial edit.


            if (disciplineField != null) { disciplineField.setData(discipline); }
            if (distanceField != null) { distanceField.setData(horizontalDistanceM); }
            if (lengthField != null) { lengthField.setData(lengthCount); }
            if (strokeField != null) { strokeField.setData(strokeCount); }
            if (holdField != null) { holdField.setData(maxHoldTime); }
            if (breathUpField != null) { breathUpField.setData(breathUpTime); }
            if (lastHrField != null && lastHeartRate != null) { lastHrField.setData(lastHeartRate); }
            session.stop();
            session.save();
        }
        timer.stop();
        state = STATE_DONE;
        WatchUi.requestUpdate();
    }

    function startRecording() {
        session = ActivityRecording.createSession({
            :name => "FreediveDistance",
            :sport => ActivityRecording.SPORT_SWIMMING,
            :subSport => ActivityRecording.SUB_SPORT_LAP_SWIMMING
        });

        disciplineField = session.createField(
            "discipline", 0, FitContributor.DATA_TYPE_STRING,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => ""}
        );

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

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

        strokeField = session.createField(
            "stroke_count", 8, FitContributor.DATA_TYPE_UINT8,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "strokes"}
        );

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

        breathUpField = session.createField(
            "breath_up_time_s", 10, FitContributor.DATA_TYPE_UINT16,
            {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "s"}

[53 more lines in file. Use offset=190 to continue.]


I see - the first edit (adding contractionField var) and the second edit (onUpdate) succeeded, but the third edit (adding contractionField createField) failed. Let me check if the var declaration got added.