    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 contractionCount = 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) {

[168 more lines in file. Use offset=75 to continue.]


The first edit succeeded (contractionCount added), the second edit succeeded (onUpdate unchanged since replacement was identical), but the third failed. The third edit's oldText was the strokeField createField block. It should still match. Let me try the third edit again separately, and also the fourth and fifth.

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

And the file currently has exactly that (from read above). Let me try again.

edit

            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[0] in /home/liph/code/garmin-freedive-app/source/DistanceView.mc. The oldText must match exactly including all whitespace and newlines.


Hmm, the oldText doesn't match. Maybe there's a tab vs space issue, or the formatting is slightly different. Let me read the exact block around line 175-185 and copy it precisely.


        );

        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();
        }

[38 more lines in file. Use offset=205 to continue.]


The text looks exactly like what I tried. Maybe the issue is that the indentation is spaces vs tabs? In the file, the indentation appears to be spaces. In my oldText I used 12 spaces. Let me count: "            strokeField" - that's 12 spaces. The file shows the same. Hmm.

Wait, maybe the problem is that the previous edit that added `contractionField = null;` changed something else and now the offsets are different? No, edit uses text matching, not offsets.

Let me try a smaller, more unique oldText. Instead of matching the whole block, I'll match just the closing part.


Could not find the exact text in /home/liph/code/garmin-freedive-app/source/DistanceView.mc. The old text must match exactly including all whitespace and newlines.


Still not matching. This is strange. Maybe there are non-printable characters or the quotes are different. Let me use a shell command to inspect the exact bytes. Or use ctx_execute_file to print the lines with cat -A equivalent.

Actually, let me use bash with sed to show the exact lines.

bash

sed -n '177,183p' /home/liph/code/garmin-freedive-app/source/DistanceView.mc | cat -A


        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,$


I see - the indentation is 8 spaces, not 12. In my oldText I used 12 spaces for the first line (`            strokeField`). It should be 8 spaces (`        strokeField`). That's why it didn't match. My read tool was displaying it with what looked like 12 spaces but is actually 8. Let me fix the indentation and try again.

edit

        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"}
        );


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

     ...
 178             "stroke_count", 8, FitContributor.DATA_TYPE_UINT8,
 179             {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "strokes"}
 180         );
 181
+182         contractionField = session.createField(
+183             "contraction_count", 16, FitContributor.DATA_TYPE_UINT8,
+184             {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "contractions"}
+185         );
+186
 182         holdField = session.createField(
 183             "breathhold_time_s", 9, FitContributor.DATA_TYPE_UINT16,
 184             {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "s"}
 185         );
     ...

--- /home/liph/code/garmin-freedive-app/source/DistanceView.mc
+++ /home/liph/code/garmin-freedive-app/source/DistanceView.mc
@@ -178,8 +178,13 @@
             "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"}
+        );
+
         holdField = session.createField(
             "breathhold_time_s", 9, FitContributor.DATA_TYPE_UINT16,
             {:mesgType => FitContributor.MESG_TYPE_SESSION, :units => "s"}
         );


bash

sed -n '148,156p' /home/liph/code/garmin-freedive-app/source/DistanceView.mc | cat -A


            session.save();$
        }$
        timer.stop();$
        state = STATE_DONE;$
        WatchUi.requestUpdate();$
    }$
$
    function startRecording() {$
        session = ActivityRecording.createSession({$


sed -n '138,148p' /home/liph/code/garmin-freedive-app/source/DistanceView.mc | cat -A


    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();$


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


The first edit succeeded (contractionField save), but the second failed. Let me find the exact startHold function text.

grep -n "function startHold" -A 10 /home/liph/code/garmin-freedive-app/source/DistanceView.mc | cat -A


126:    function startHold() {$
127-        currentTime = 0;$
128-        state = STATE_HOLD;$
129-        startRecording();$
130-    }$
131-$
132-    function endHold() {$
133-        maxHoldTime = currentTime;$
134-        state = STATE_RECOVERY;$
135-        currentTime = 0;$
136-    }$


Ah, the order is different: currentTime = 0; then state = STATE_HOLD; then startRecording();. My oldText expected state first. Let me fix that.

edit

    function startHold() {
        currentTime = 0;
        state = STATE_HOLD;
        lengthCount = 0;
        horizontalDistanceM = 0;
        strokeCount = 0;
        contractionCount = 0;
        startRecording();
    }

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


     ...
 125
 126     function startHold() {
 127         currentTime = 0;
 128         state = STATE_HOLD;
+129         lengthCount = 0;
+130         horizontalDistanceM = 0;
+131         strokeCount = 0;
+132         contractionCount = 0;
 129         startRecording();
 130     }
 131
 132     function endHold() {
     ...

--- /home/liph/code/garmin-freedive-app/source/DistanceView.mc
+++ /home/liph/code/garmin-freedive-app/source/DistanceView.mc
@@ -125,8 +125,12 @@

     function startHold() {
         currentTime = 0;
         state = STATE_HOLD;
+        lengthCount = 0;
+        horizontalDistanceM = 0;
+        strokeCount = 0;
+        contractionCount = 0;
         startRecording();
     }

     function endHold() {


Good. Now I need to also add the `contractionField = null;` declaration. Wait, I already added `contractionCount = 0;` but did I add `contractionField = null;`? Let me check. Earlier first edit was supposed to add `var contractionField = null;` but the partial edit call failed overall, yet when I read the file I saw `var contractionCount = 0;` was added. Did `contractionField` get added? Let me check.

bash

grep -n "contractionField\|contractionCount" /home/liph/code/garmin-freedive-app/source/DistanceView.mc


36:    var contractionCount = 0;
132:        contractionCount = 0;
186:        contractionField = session.createField(
245:                contractionCount++;


contractionField is used at line 186 but never declared! I need to add `var contractionField = null;` at the top. Let me add it.

sed -n '25,40p' /home/liph/code/garmin-freedive-app/source/DistanceView.mc | cat -A


    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 contractionCount = 0;$
    var horizontalDistanceM = 0;$
    var lastHeartRate = null;$
$
    function initialize(disc) {$


I need to add `var contractionField = null;` after `var strokeField = null;`. Let me find the exact location.

grep -n "strokeField = null" /home/liph/code/garmin-freedive-app/source/DistanceView.mc


24:    var strokeField = null;


edit

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

    var strokeField = null;
    var holdField = null;


     ...
  21     var disciplineField = null;
  22     var distanceField = null;
  23     var lengthField = null;
  24     var strokeField = null;
+ 25     var contractionField = null;
  25     var holdField = null;
  26     var breathUpField = null;
  27     var lastHrField = null;
  28
     ...

--- /home/liph/code/garmin-freedive-app/source/DistanceView.mc
+++ /home/liph/code/garmin-freedive-app/source/DistanceView.mc
@@ -21,8 +21,9 @@
     var disciplineField = null;
     var distanceField = null;
     var lengthField = null;
     var strokeField = null;
+    var contractionField = null;
     var holdField = null;
     var breathUpField = null;
     var lastHrField = null;


Good. Now I need to think about other issues. Let me also check that the APP_DESIGN_V2.md FIT field table is consistent with the field IDs I'm using. I added contraction_count at ID 16 which isn't in the table. I should update the table.

Also, I should update README to mention the v2 fēnix 5 build.

Let me also check if there are other obvious issues.

In `DepthView.onUpdate`, I use `Graphics.FONT_NUMBER_HOT`. On fēnix 5 this should exist, but to be safe maybe use `FONT_NUMBER_MEDIUM`. However, the user wants a clear depth display, and FONT_NUMBER_HOT is larger. I'll keep it.

In `MainMenuView`, the menu items use `Graphics.FONT_MEDIUM`. The text "Static Apnoe" and "Breathing Tr." are 12 and 11 chars which should fit 240 width with FONT_MEDIUM. Good.

In `SubMenuView`, same.

In `DepthView`, `statusText = discipline + "  BREATH UP"` etc. For discipline "CWT-B" etc, the status line might be long but FONT_SMALL should fit.

In `BreathingView`, `STATE_SETUP_REST` infoText uses `AppSettings.breathingRestSeries.size()`. The array `.size()` method works.

One issue: in `BreathingView.onTimer`, for REST and HOLD states I do `currentTime--`. But `currentTime` is initialized to the rest/hold duration. In `onUpdate`, `bigText = Common.formatTime(currentTime)`. So it counts down from e.g. 120 to 0. Good. But in `STATE_REST`, when `currentTime <= 0`, `startHold()` is called which sets currentTime = targetHoldS. Then the same timer tick continues? No, `onTimer` returns after `startHold()`? Let me check the code.


                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"}
        );

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


The BreathingView logic looks OK. The timer calls `startHold()` which sets currentTime, then `WatchUi.requestUpdate()` is called after the if/else block. The display will update with the new hold time. Good.

One issue: in `BreathingView`, after `finishHold()` calls `startRest()`, the `currentTime` is set to the next rest value. But the `onTimer` continues and `currentTime--` happens in the next tick. Good.

Now, let me think about whether the app will compile. The biggest risk is method references and `invoke`. Let me do a quick web search to verify Monkey C method reference invocation syntax.

Actually, I've seen `method(:name)` and `.invoke()` in forum posts. The static method bug forum post showed `staticClass.doSomething()` being called, not `invoke`. But for Method objects, `invoke` is documented. Let me verify.

Also, I should check if `Array.size()` is correct or if it's `Array.size` (property). I believe it's `size()` method. Let me verify quickly with web search.

Question: Garmin Monkey C method reference invoke syntax method(:name)


Reviewed 4 sources · synthesized answer with 4 findings

Reference Guides

Monkey C

- [web_fetch] Community/practical context: A collection of Connect IQ apps and libraries Purpose This repo is officially maintained by the Garmin Connect IQ team, containing a small set of reference examples and monkey barr
- [web_fetch] Community/practical context: AI CODE CREATIONGitHub CopilotWrite better code with AIGitHub Copilot appDirect agents from issue to mergeMCP RegistryNewIntegrate external toolsDEVELOPER WORKFLOWSActionsAutomate
- [web_fetch] Community/practical context: Garmin DevelopersOverviewCompatible DevicesAPI DocsGet the SDKSubmit an AppStay InformedConnect IQ BasicsMonkey CCore TopicsUser Experience GuidelinesPersonality LibraryConnect IQ
- [web_fetch] Community/practical context: Garmin DevelopersOverviewCompatible DevicesAPI DocsGet the SDKSubmit an AppStay InformedConnect IQ BasicsMonkey CFunctionsObjects and MemoryContainersMonkey TypesExceptions and Err

Internal research: web_search ×4, web_fetch ×10, web_fetch_headless ×0

Findings
- [web_fetch] Community/practical context: A collection of Connect IQ apps and libraries Purpose This repo is officially maintained by the Garmin Connect IQ team, containing a small set of reference examples and monkey barr
- [web_fetch] Community/practical context: AI CODE CREATIONGitHub CopilotWrite better code with AIGitHub Copilot appDirect agents from issue to mergeMCP RegistryNewIntegrate external toolsDEVELOPER WORKFLOWSActionsAutomate
- [web_fetch] Community/practical context: Garmin DevelopersOverviewCompatible DevicesAPI DocsGet the SDKSubmit an AppStay InformedConnect IQ BasicsMonkey CCore TopicsUser Experience GuidelinesPersonality LibraryConnect IQ
- [web_fetch] Community/practical context: Garmin DevelopersOverviewCompatible DevicesAPI DocsGet the SDKSubmit an AppStay InformedConnect IQ BasicsMonkey CFunctionsObjects and MemoryContainersMonkey TypesExceptions and Err

Sources
- [web_fetch] GitHub - garmin/connectiq-apps: A collection of Connect IQ apps.: https://github.com/garmin/connectiq-apps
- [web_fetch] garmin-connectiq-linux/ProgrammersGuide.html at master · BodyFatControl/garmin-connectiq-linux: https://github.com/BodyFatControl/garmin-connectiq-linux/blob/master/ProgrammersGuide.html
- [web_fetch] Reference Guides: https://developer.garmin.com/connect-iq/reference-guides/monkey-c-reference/
- [web_fetch] Monkey C: https://developer.garmin.com/connect-iq/monkey-c/functions/

Internal tools
Internal research: web_search ×4, web_fetch ×10, web_fetch_headless ×0

Caveat
Evidence is partial: the strongest readable sources were mostly community/practical context.

Reference Guides

Monkey C


Question: Garmin Connect IQ Monkey C Method.invoke syntax method(:name) callback


1. Toybox.Lang.Method
   URL: https://developer.garmin.com/connect-iq/api-docs/Toybox/Lang/Method.html
   Toybox.Lang.Method

# Class: Toybox.Lang.Method

Inherits: Toybox.Lang.Object

show all

## Overview

Method is a class that represents a callback, or a function that can be used as an argument to another function. You can create one using the method() call, and invoke the Method using the invoke() method.

## See Also:

## Instance Method Summary collapse

Get a hash code value for a Method.

initialize(aClass, aMethod as Lang.Symbol)

Method Constructor.

Invoke a Method.

## Instance Method Details

### hashCode() as Lang.Number

Get a hash code value for a Method. This computes a 32-bit Number that is typically used as an index when placing Objects into a Dictionary. Hash code values have the following characteristics:

The computed hash code is constant for the lifetime of an Object

If two Objects are equal, their hash codes will be equal

### initialize(aClass, aMethod as Lang.Symbol)

Method Constructor.

### invoke(parameters...) Lang.Object

Invoke a Method.

---

Generated Apr 15, 2026, 10:00:20 AM

2. Documentation clarification: Object.method() and Lang::Method - Connect IQ App Development Discussion - Connect IQ - Garmin Forums
   URL: https://forums.garmin.com/developer/connect-iq/f/discussion/2289/documentation-clarification-object-method-and-lang-method
   Documentation clarification: Object.method() and Lang::Method - Connect IQ App Development Discussion - Connect IQ - Garmin Forums

- Replies 1 reply
- Subscribers 15 subscribers
- Views 1163 views
- Users 0 members are here

Options

Related

# Documentation clarification: Object.method() and Lang::Method

Just thought I would clarify this for others: When you call method( :some-symbol) in an object instance method, that call is effectively equivalent to "new Lang.Method(self, :some-symbol)" The docs for Method.initalize() say that initialize expects "Classdef of method" as the first parameter; however, the initialize function really requires an object as it's first parameter. (self is a reserved word in MonkeyC that refers to the current instance of the class that the currently executing method is a member of - roughly equivalent to 'this' in C++.) Note that you can create a method object that will invoke some method on some other object by using "new Lang.Method(some_other_object, :some-symbol)". Unfortunately, there doesn't appear to be any equivalent syntax / construct to refer to instance variables - but that can obviously be implemented by creating member functions for the variables you need to expose (wrap a system object with a new class if necessary). Unfortunately, the docs give no indication if the Method object obtains a strong or weak reference on the target object instance. As with many places in MonkeyC (but not all), :some-symbol can be a variable that holds an instance of Symbol. (And again it's really unfortunate that Symbol.toString() always returns the string 'Symbol': quite useless.) A notable place that doesn't accept a variable holding a Symbol object is the rhs of the 'has' operator, which is also really unfortunate - especially given that there is no way to trap a "symbol not found" error. (The above all applies to SDK 1.2.2; not sure about any other version.) -frank

- Sign in to reply
- Cancel

> however, the initialize function really requires an object as it's first parameter.

Note that you can also invoke a module function like this... new Lang.Method(Toybox.Activity, :getActivityInfo)[/code], which I don't believe you can do with the method() method..

> Note that you can create a method object that will invoke some method on some other object by using "new Lang.Method(some_other_object, :some-symbol)".

You can more simply write some_other_object.method(:some_symbol) which is much more readable.

> Unfortunately, there doesn't appear to be any equivalent syntax / construct to refer to instance variables - but that can obviously be implemented by creating member functions for the variables you need to expose (wrap a system object with a new class if necessary).

Other dynamically typed languages allow this, and I'm fairly certain it works with MonkeyC. I don't have a compiler at work, but...`class X {
 var a;
 var b;
}

var x = new X;
x[:a] = 1;
`

> Unfortunately, the docs give no indication if the Method object obtains a strong or weak reference on the target object instance.

It is a strong reference. If it were a weak reference, then it would be possible for the referenced object to be destroyed before invoking the callback, and the callback couldn't be invoked.

> As with many places in MonkeyC (but not all), :some-symbol can be a variable that holds an instance of Symbol. A notable place that doesn't accept a variable holding a Symbol object is the rhs of the 'has' operator, which is also really unfortunate.

The following runs fine for me. It prints true twice (I've pasted it into the initialize() method of a class derived from Ui.View).`var x = (self has :onUpdate);
Sys.println(x);

var symbol = :onUpdate;
var y = (self has symbol);
Sys.println(y);
` Travis

3. method() throws error: Failed invoking - Connect IQ App Development Discussion - Connect IQ - Garmin Forums
   URL: https://forums.garmin.com/developer/connect-iq/f/discussion/305994/method-throws-error-failed-invoking
   method() throws error: Failed invoking - Connect IQ App Development Discussion - Connect IQ - Garmin Forums

- Cancel
- More
- German Forum
- Beta Program
- Developer
- Pilots
- Sports & Fitness
- Outdoor Recreation
- Apps & Software

- Users 0 members are here
- Views 2939 views
- Subscribers 19 subscribers
- Replies 14 replies
- State Verified Answer

- communication
- error
- Connect IQ
- webrequest
- background
- Connect IQ Developers

Options

- Cancel
- More
- Share

Related

# method() throws error: Failed invoking

Hello,

I just can't find the issue with this code, it's mostly identical to the examples in the documentation. I get the following error:

Error: Invalid Value

Details: Failed invoking

Code:

import Toybox.System;

import Toybox.Communications;

import Toybox.Lang;

(:background)

class LogService {

function onReceive(responseCode as Number, data as Dictionary?) as Void {

if (responseCode == 200) {

System.println("Request Successful"); // print success

} else {

System.println("Response: " + responseCode); // print response code

}

Background.exit();

}

function makeRequest() as Void {

var url = "-";

var params = { // set the parameters

"deviceId" => "123456789abcdefg",

"deviceType" => "venu",

"appName" => "test",

"dateTime" => Time.now()

};

var options = { // set the options

:method => Communications.HTTP_REQUEST_METHOD_POST, // set HTTP method

:headers => { // set headers

"Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON},

// set response type

:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON

};

var callback = method(:onReceive); // Here I get the error

// onReceive() method

// Make the Communications.makeWebRequest() call

Communications.makeWebRequest(url, params, options, callback);

}

}

- Cancel
- Sign in to reply

## Top Replies

- This is the correct answer. It works as a class instance. Reading the documentation, it makes sense:However, using the method() function inherited from Toybox.Lang.Object, a class instance can create a M…
- Try again with an instance. I do something similar, but with an instance.

## All Replies

do you have correct permisions

Why don't you simple put method directly in call:

Communications.makeWebRequest(url, params, options, :onReceive);

Permissions are there, so it's not about those.

Regarding your suggestion, it doesn't work and all the examples use method(:onReceive), not :onReceive.

The following is not helping either, which is expected since it's practically the same code:

Communications.makeWebRequest(url, params, options, method(:onReceive));

You could try self.method(:onReceive) inside the class.

I tried even that

is your onReceive function in the same class as you are trying to do the method(:onReceive)?

yes, you can see in the code above. Both are in LogService class.

how do you invoke makeRequest()?

maybe bug is in line Time.now()

- no import Time

- I'm wonder what is put in params.dateTime as now() returns Moment....

it's nothing about that, the error is on line:

RoLynx said:

var callback = method(:onReceive); // Here I get the error

I made it work by removing the 2 methods from the class (having them global) and using:

Communications.makeWebRequest(url, params, options, new Lang.Method($, :onReceive));

But I don't know if I should submit a bug or not.

I think you hit the memory limit for background process. I got a similar error on a glance view while using a big API class. Reducing the amount of uses classes (perhaps vopy only needednfunctions into a separated background class could solve this issue.

The error is not really useful if it's caused by a memory limit.

PS: Perhaps all would be fine until calling the makeWebRequest. The result (http body) and the converted JSON object will need additional memory. That could cause a app crash due to the memory limit. Then you will get a similar error message.

Were you using the class or an instance of the class?

so

LogService.makeRequest();

or

var x=new LogService();

x.makeRequest();

4. Invoking callback function - Connect IQ App Development Discussion - Connect IQ - Garmin Forums
   URL: https://forums.garmin.com/developer/connect-iq/f/discussion/83/invoking-callback-function
   Invoking callback function - Connect IQ App Development Discussion - Connect IQ - Garmin Forums

- Replies 4 replies
- Subscribers 15 subscribers
- Views 3202 views
- Users 0 members are here

Options

Related

# Invoking callback function

The documentation describes how to create a callback function object wrapper. The documentation I'm following comes from page 18 of the Monkey C Programmers Guide. It reads...

> If you want to create a callback, you can create a Method object. Method objects are a combination of the function and the object instance or module they originate from. You can then call the method using the invoke method.`// Handle completion of long
> function wakeMeUpBeforeYouGoGo() {
> // Do something here
> }
>
> // Create a call back with the method method (jinx!)
> doLongRunningTask(method(:wakeMeUpBeforeYouGoGo));
> `

I've tried several times, and I'm unable to figure out what I need to do to be able to invoke the passed callback.`using Toybox.WatchUi as Ui;
using Toybox.System as Sys;

class TestView extends Ui.DataField {

 function onLayout(dc) {
 }

 function onShow() {
 }

 function compute(info) {
 }

 function doLongRunningTask(fn) {
 invoke(fn); // what to do here?
 }

 function wakeMeUpBeforeYouGoGo() {
 Sys.println("!");
 }

 //! Update the view
 function onUpdate(dc) {
 doLongRunningTask(method(:wakeMeUpBeforeYouGoGo));
 }
}
`

- Sign in to reply
- Cancel

Nevermind. I figured it out. The method function creates a Toybox.Lang.Method object, which has an invoke() method on it. So the syntax to invoke the method object fn in the above example is...`function doLongRunningTask(fn) { fn.invoke();}` If you'd like to invoke the method with parameters, you just pass them to the invoke method, like this...`fn.invoke(1); fn.invoke("Hello");`

> Nevermind. I figured it out. The method function creates a Toybox.Lang.Method object, which has an invoke() method on it. So the syntax to invoke the method object fn in the above example is...`function doLongRunningTask(fn) {
> fn.invoke();
> }
> ` If you'd like to invoke the method with parameters, you just pass them to the invoke method, like this...`fn.invoke(1);
> fn.invoke("Hello");
> `

I've tried it, but it does not work. Error Message: "Could not find symbol method." in line "doLongRunningTask(method(:wakeMeUpBeforeYouGoGo));". What's going wrong??

It works just fine. There is something wrong with your code. Given the error message, the place that you are calling method is probably a context where there is no object (i.e., not a function of a class). In that case, you need to tell the system what object you want to invoke the function on by qualifying the call with an object reference (obj.method(:wakeMeUpBeforeYouGoGo) will create a Lang.Method that, when invoked, will call obj.wakeMeUpBeforeYouGoGo()). Travis

Thank you very much! Your tip pointed me in the right direction. In my case it was a function of a module. A "new Lang.Method(ModuleName, :functionOfModul)" did the trick.

5. Method.invoke with variable list of parameters - Connect IQ App Development Discussion - Connect IQ - Garmin Forums
   URL: https://forums.garmin.com/developer/connect-iq/f/discussion/408937/method-invoke-with-variable-list-of-parameters/1922686
   Method.invoke with variable list of parameters - Connect IQ App Development Discussion - Connect IQ - Garmin Forums

- Cancel
- More
- German Forum
- Beta Program
- Developer
- Pilots
- Sports & Fitness
- Outdoor Recreation
- Apps & Software

- Users 0 members are here
- Views 784 views
- Subscribers 18 subscribers
- Replies 6 replies

Options

- Cancel
- More
- Share

Related

# Method.invoke with variable list of parameters

I would like to write a class representing a Method and an variably-sized array of parameters to pass into the method on invocation.

Is there a more elegant solution to calling invoke of the Method than one call for each of the number of parameters I want to support?

```
class EvccTask {    private var _method as Method;    private var _args as Array<Object>;    public function initialize( method as Method, args as Array<Object>? ) {        _method = method;        _args = args == null ? new Array<Object>[0] : args;    }    public function invoke() as Void {        if( _args == null || _args.size() == 0 ) {            _method.invoke();        } else if (_args.size() == 1 ) {            _method.invoke( _args[0] );        } else if (_args.size() == 2 ) {            _method.invoke( _args[0], _args[1] );        } else if (_args.size() == 3 ) {            _method.invoke( _args[0], _args[1], _args[2] );        } else {            throw new OperationNotAllowedException( "EvccTask: too many arguments!" );        }    }}
```

- Cancel
- Sign in to reply

## Top Replies

the-ninth said:

executeTask( new Method( aClass, :aMethod ) ); // this trips up the strict type-checkerexecuteTask( aClass.method( :aMethod ) ); // this works

This has come up before, so I opened a bug…

flowstate said:

there's no varargs

I guess Method.invoke() actually does use varargs somehow, but I don't know of any way for *us* to use varargs.

So it seems like Lang.Method uses at least 2 "special…

## All Replies

You could change the methods to receive an array or dictionary instead of different number of params.

I did not want to change the called functions, because they are also called outside of this task wrapper. So instead I opted for replacing Method with an interface:

```
typedef EvccTask as interface {    function invoke() as Void;};
```

Now I can pass it in either Method objects, or wrapper objects that have a method inside, but also store parameters and apply them for the invoke. The actual execution can therefore always be without parameters, something like this:

```
function executeTask( task as EvccTask ) as Void {    task.invoke();}
```

However the interface is not really the same as Method.invoke, apart from the Void as return, the variable number of parameters seems to be something that cannot be declared for normal functions. But there seems to be an inconsistency in the type checker that I could exploit to still pass in Method objects without having to disable type checker for the functions that execute tasks:

```
executeTask( new Method( aClass, :aMethod ) ); // this trips up the strict type-checkerexecuteTask( aClass.method( :aMethod ) ); // this works
```

flocsy🤠 said:

You could change the methods to receive an array or dictionary instead of different number of params.

the-ninth said:

I did not want to change the called functions, because they are also called outside of this task wrapper.

Is the specific concern here style/readability or type safety?

If it's type safety, you could change the functions so they accept a tuple as an argument. A tuple is a fixed-length array where each indexed item has a distinct type.

Given that calling a function with the wrong number of args is a run-time error, and there's no varargs or array spread operation in Monkey C, I don't think there's a way to do exactly what you want.

the-ninth said:

executeTask( new Method( aClass, :aMethod ) ); // this trips up the strict type-checkerexecuteTask( aClass.method( :aMethod ) ); // this works

This has come up before, so I opened a bug report:

forums.garmin.com/.../type-checker-cannot-infer-exact-type-of-callback-created-with-lang-method-but-it-can-do-so-for-object-method

EDIT: maybe I misunderstood the issue and bug report isn't that relevant :/

EDIT2: no, I think I understood the issue after all. I've added a comment to the bug report explaining what I think you're seeing (although additional context, like examples of aClass and aMethod, would be helpful)

https://forums.garmin.com/developer/connect-iq/i/bug-reports/type-checker-cannot-infer-exact-type-of-callback-created-with-lang-method-but-it-can-do-so-for-object-method?CommentId=1712190c-530b-474d-8022-51aaaf418602

TL;DR

- new Lang.Method() always produces a generic Method (with unknown args and return type), it cannot fulfill your interface contract (and that's why it also produces a warning/error when used as one of the standard callback types in the Connect IQ API)

- Object.method() produces a method with correct information about the args and return type of the referenced function, so it can fulfill your interface contract, provided the function exists and type of args and return value match that of invoke() in your interface.

e.g.

```
import Toybox.Graphics;
import Toybox.WatchUi;
import Toybox.Lang;

typedef EvccTask as interface {
    function invoke() as Void;
};

function executeTask( task as EvccTask ) as Void {
    task.invoke();
}

class SomeClass extends Object {
    // does not fulfill EvccTask (signature does not match)
    function randomMethod(x as Number) as Void {}
    // does fulfill EvccTask (signature matches)
    function randomMethod2() as Void {}
}

function foo() {
    // in the case of new Method() the type checker has no clue about the method signatures (args, return value)
    executeTask(new Method(SomeClass, :randomMethod)); // ERROR: Invalid '$.Toybox.Lang.Method' passed as parameter 1 of type 'interface { function invoke() as Void; }'.
    executeTask(new Method(SomeClass, :nonExistentSymbol)); // ERROR: Invalid '$.Toybox.Lang.Method' passed as parameter 1 of type 'interface { function invoke() as Void; }'.

    // in the case of Object.method(), the type checker does know about the method signatures (args, return value),
    // as long as the passed-in symbol refers to an known, existing function
    var someObject = new SomeClass();
    executeTask(someObject.method(:randomMethod)); // ERROR: Invalid '$.Toybox.Lang.Method(x as $.Toybox.Lang.Number) as Void' passed as parameter 1 of type 'interface { function invoke() as Void; }'.
    executeTask(someObject.method(:randomMethod2)); // this produces no warning/error, and I don't expect it to
    executeTask(someObject.method(:nonExistentSymbol)); // ERROR: Invalid '$.Toybox.Lang.Method() as Any' passed as parameter 1 of type 'interface { function invoke() as Void; }'.

}
```

flowstate said:

there's no varargs

I guess Method.invoke() actually does use varargs somehow, but I don't know of any way for *us* to use varargs.

So it seems like Lang.Method uses at least 2 "special" language features that don't really work (or aren't fully supported) in other contexts:

- varargs (no way to to declare / write our own function that uses varargs)

- accepting class / module name as an argument. Yes, we can write a function that does this, but certain things don't work well - like if you assign a class name to a variable, then type checking doesn't work properly

flowstate said:

- new Lang.Method() always produces a generic Method (with unknown args and return type), it cannot fulfill your interface contract (and that's why it also produces a warning/error when used as one of the standard callback types in the Connect IQ API)

- Object.method() produces a method with correct information about the args and return type of the referenced function (provides the function exists), so it can fulfill your interface contract, provided the referenced functions args and return value matches that of invoke() in your interface.

Yes, that sounds like what I am seeing!

Toybox.Lang.Method


Good - `method(:name)` and `.invoke()` are confirmed. The method reference in a class instance is bound to `self`. So my SubMenuView callback should work.

However, there's a subtle issue: the `SubMenuView.onSelect` field is of type `Method`. When I call `method(:onDepthType)` inside `MainMenuView`, it creates a Method bound to the MainMenuView instance. When SubMenuView calls `onSelect.invoke(selectedIndex)`, it will call `MainMenuView.onDepthType(selectedIndex)`. Good.

Now, another potential issue: when I push the Depth view via `WatchUi.switchToView`, what happens to the menu view stack? `switchToView` replaces the current view. But the menu was pushed as a view stack. Actually, `getInitialView` returns the main menu. When we push sub-menus, they are on the stack. When we `switchToView` from a sub-menu callback, we replace the top view (sub-menu) with Depth view. But the main menu is still underneath. That's fine - when we later switch back to MainMenuView, we replace Depth view. The stack will have main menu at bottom. But we might also want to pop the sub-menu first. Actually, `switchToView` replaces the current top view in the stack. So after launching Depth, the stack is [MainMenu, Depth]. When Depth switches back to MainMenu, it becomes [MainMenu] (Depth replaced). Good.

One issue: `switchToView` might not be available in API 2.4.0? Let me check. `WatchUi.switchToView` is available. Yes.

Another issue: In `DepthView.discardSession`, I call `WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), ...)` but the delegate constructor now requires a view parameter. I used `new MainMenuDelegate()` - this will fail because the constructor expects a view. I need to fix all places that create MainMenuDelegate. Let me search.


No matches found


No bare `new MainMenuDelegate()` in source. But there might be `new SubMenuDelegate()` bare. Let me check.

ffgrep

new SubMenuDelegate\(\)|new DepthDelegate\(\)|new DistanceDelegate\(\)|new StaticDelegate\(\)|new BreathingDelegate\(\)


Good. All delegate instantiations pass the view. Now I should also check `new MainMenuDelegate()` in DepthView, DistanceView, etc. Let me search for MainMenuDelegate creation.


The ffgrep isn't finding anything, maybe because the path pattern is wrong. Let me use bash grep.

bash

grep -rn "new MainMenuDelegate\|new SubMenuDelegate\|new DepthDelegate\|new DistanceDelegate\|new StaticDelegate\|new BreathingDelegate" /home/liph/code/garmin-freedive-app/source/


/home/liph/code/garmin-freedive-app/source/BreathingDelegate.mc:73:                    WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
/home/liph/code/garmin-freedive-app/source/DistanceDelegate.mc:69:                    WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
/home/liph/code/garmin-freedive-app/source/StaticDelegate.mc:58:                    WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
/home/liph/code/garmin-freedive-app/source/DistanceView.mc:217:        WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
/home/liph/code/garmin-freedive-app/source/DistanceView.mc:228:            WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
/home/liph/code/garmin-freedive-app/source/Fenix5App.mc:25:        var delegate = new MainMenuDelegate();
/home/liph/code/garmin-freedive-app/source/MainMenuView.mc:56:            WatchUi.pushView(subView, new SubMenuDelegate(subView), WatchUi.SLIDE_LEFT);
/home/liph/code/garmin-freedive-app/source/MainMenuView.mc:59:            WatchUi.pushView(subView2, new SubMenuDelegate(subView2), WatchUi.SLIDE_LEFT);
/home/liph/code/garmin-freedive-app/source/MainMenuView.mc:77:            WatchUi.pushView(subView, new SubMenuDelegate(subView), WatchUi.SLIDE_LEFT);
/home/liph/code/garmin-freedive-app/source/MainMenuView.mc:101:        WatchUi.switchToView(view, new DepthDelegate(view), WatchUi.SLIDE_LEFT);
/home/liph/code/garmin-freedive-app/source/MainMenuView.mc:106:        WatchUi.switchToView(view, new DistanceDelegate(view), WatchUi.SLIDE_LEFT);
/home/liph/code/garmin-freedive-app/source/MainMenuView.mc:111:        WatchUi.switchToView(view, new StaticDelegate(view), WatchUi.SLIDE_LEFT);
/home/liph/code/garmin-freedive-app/source/MainMenuView.mc:116:        WatchUi.switchToView(view, new BreathingDelegate(view), WatchUi.SLIDE_LEFT);
/home/liph/code/garmin-freedive-app/source/DepthView.mc:257:        WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
/home/liph/code/garmin-freedive-app/source/DepthView.mc:271:            WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
/home/liph/code/garmin-freedive-app/source/DepthDelegate.mc:61:                    WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
/home/liph/code/garmin-freedive-app/source/StaticView.mc:182:        WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
/home/liph/code/garmin-freedive-app/source/StaticView.mc:193:            WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
/home/liph/code/garmin-freedive-app/source/BreathingView.mc:215:        WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);
/home/liph/code/garmin-freedive-app/source/BreathingView.mc:231:            WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi.SLIDE_RIGHT);


There are many bare `new MainMenuDelegate()` calls. I need to fix all of them. I'll create a helper function in MainMenuView to return the main menu view+delegate pair, or just fix each occurrence. A helper would be cleaner. Let me add a static method `MainMenuView.push()` or just update each call.

Actually, the simplest is to make `MainMenuDelegate` have a no-arg constructor that creates a new MainMenuView and sets it. But then the view and delegate are separate instances - the delegate's view might not be the same as the one returned. However, for the main menu, the delegate only needs the view to call its methods. If the delegate creates its own view, that's fine as long as that's the view being displayed. But if I return `[new MainMenuView(), delegate]` where delegate created its own view, there will be two view instances. The displayed one won't receive the delegate's actions.

Better: create a helper function `Common.switchToMainMenu()` that creates one view and one delegate. Let me add that to Common.mc or MainMenuView.

Actually, a static method in MainMenuView is clean:

```monkey-c
static function switchToMainMenu(slideDirection) {
    var view = new MainMenuView();
    WatchUi.switchToView(view, new MainMenuDelegate(view), slideDirection);
}
```

Then replace all `WatchUi.switchToView(new MainMenuView(), new MainMenuDelegate(), ...)` with `MainMenuView.switchToMainMenu(...)`.

Let me do this with a global replacement in each file. I can use bash with sed, but better to use edit tool for precise changes. There are many occurrences though.

Let me add the static method to MainMenuView, then use sed to replace all occurrences in the source files.

Actually, using sed is efficient for this mechanical change. But I need to be careful. Let me do it.

First, add the static method to MainMenuView.

edit

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

    static function switchToMainMenu(slideDir) {
        var view = new MainMenuView();
        WatchUi.switchToView(view, new MainMenuDelegate(view), slideDir);
    }
}

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


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

     ...
 114     function launchBreathing() {
 115         var view = new BreathingView();
 116         WatchUi.switchToView(view, new BreathingDelegate(view), WatchUi.SLIDE_LEFT);
 117     }
+118
+119     static function switchToMainMenu(slideDir) {
+120         var view = new MainMenuView();
+121         WatchUi.switchToView(view, new MainMenuDelegate(view), slideDir);
+122     }
 118 }

--- /home/liph/code/garmin-freedive-app/source/MainMenuView.mc
+++ /home/liph/code/garmin-freedive-app/source/MainMenuView.mc
@@ -114,5 +114,10 @@
     function launchBreathing() {
         var view = new BreathingView();
         WatchUi.switchToView(view, new BreathingDelegate(view), WatchUi.SLIDE_LEFT);
     }
+
+    static function switchToMainMenu(slideDir) {
+        var view = new MainMenuView();
+        WatchUi.switchToView(view, new MainMenuDelegate(view), slideDir);
+    }
 }


bash

cd /home/liph/code/garmin-freedive-app/source && sed -i 's/WatchUi\.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi\.SLIDE_RIGHT);/MainMenuView.switchToMainMenu(WatchUi.SLIDE_RIGHT);/g' *.mc && sed -i 's/WatchUi\.switchToView(new MainMenuView(), new MainMenuDelegate(), WatchUi\.SLIDE_LEFT);/MainMenuView.switchToMainMenu(WatchUi.SLIDE_LEFT);/g' *.mc


(no output)


Good. Now let me verify the replacements.

grep -rn "new MainMenuDelegate()\|new MainMenuView(), new MainMenuDelegate" /home/liph/code/garmin-freedive-app/source/ || echo "none found"


/home/liph/code/garmin-freedive-app/source/Fenix5App.mc:25:        var delegate = new MainMenuDelegate();


Fenix5App has `new MainMenuDelegate()` because it creates the view and delegate separately and then calls `delegate.setView(view)`. Wait, I changed MainMenuDelegate to take a view in constructor. So Fenix5App should now use `new MainMenuDelegate(view)`. Let me fix it.

edit

var delegate = new MainMenuDelegate(view);


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

    ...
 21     }
 22
 23     function getInitialView() {
 24         var view = new MainMenuView();
-25         var delegate = new MainMenuDelegate();
+25         var delegate = new MainMenuDelegate(view);
 26         delegate.setView(view);
 27         return [view, delegate];
 28     }
 29 }

--- /home/liph/code/garmin-freedive-app/source/Fenix5App.mc
+++ /home/liph/code/garmin-freedive-app/source/Fenix5App.mc
@@ -21,9 +21,9 @@
     }

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


Good. Now I need to also check for any bare `new SubMenuDelegate()` - I already fixed those in MainMenuView. Let me verify.

bash

grep -rn "new SubMenuDelegate()" /home/liph/code/garmin-freedive-app/source/ || echo "none found"


none found


Good. Now let me also update the APP_DESIGN_V2.md FIT field table to include contraction_count at ID 16.

edit

| 8 | `stroke_count` | SESSION |
| 9 | `contraction_count` | SESSION |
| 10 | `breathhold_time_s` | SESSION |

| 8 | `stroke_count` | SESSION |
| 9 | `breathhold_time_s` | SESSION |


Successfully replaced 1 block(s) in /home/liph/code/garmin-freedive-app/APP_DESIGN_V2.md.

     ...
 317 | 5 | `surface_interval_s` | SESSION |
 318 | 6 | `horizontal_distance_m` | SESSION |
 319 | 7 | `length_count` | SESSION |
 320 | 8 | `stroke_count` | SESSION |
-321 | 9 | `breathhold_time_s` | SESSION |
+321 | 9 | `contraction_count` | SESSION |
+322 | 10 | `breathhold_time_s` | SESSION |
 322 | 10 | `breath_up_time_s` | SESSION |
 323 | 11 | `recovery_time_s` | SESSION |
 324 | 12 | `last_hr_bpm` | SESSION |
 325 | 13 | `cycles_completed` | SESSION |
     ...

--- /home/liph/code/garmin-freedive-app/APP_DESIGN_V2.md
+++ /home/liph/code/garmin-freedive-app/APP_DESIGN_V2.md
@@ -317,9 +317,10 @@
 | 5 | `surface_interval_s` | SESSION |
 | 6 | `horizontal_distance_m` | SESSION |
 | 7 | `length_count` | SESSION |
 | 8 | `stroke_count` | SESSION |
-| 9 | `breathhold_time_s` | SESSION |
+| 9 | `contraction_count` | SESSION |
+| 10 | `breathhold_time_s` | SESSION |
 | 10 | `breath_up_time_s` | SESSION |
 | 11 | `recovery_time_s` | SESSION |
 | 12 | `last_hr_bpm` | SESSION |
 | 13 | `cycles_completed` | SESSION |


I need to also update the other field IDs in the table because I shifted them. In the code, I used:
- discipline: 0
- depth_m (RECORD): 1
- hr_bpm (RECORD): 2
- max_depth_m: 3
- dive_time_s: 4
- surface_interval_s: 5
- horizontal_distance_m: 6
- length_count: 7
- stroke_count: 8
- contraction_count: 16 (not sequential)
- breathhold_time_s: 9
- breath_up_time_s: 10
- recovery_time_s: 11
- last_hr_bpm: 12
- cycles_completed: 13
- total_hold_time_s: 14
- hold_target_s: 15

The table currently has:
- 0 discipline
- 1 depth_m
- 2 hr_bpm
- 3 max_depth_m
- 4 dive_time_s
- 5 surface_interval_s
- 6 horizontal_distance_m
- 7 length_count
- 8 stroke_count
- 9 contraction_count (I just changed from breathhold)
- 10 breathhold_time_s
- 11 breath_up_time_s
- 12 recovery_time_s
- 13 last_hr_bpm
- 14 cycles_completed
- 15 total_hold_time_s
- 16 hold_target_s
- 20 lap_hold_s
- 21 lap_rest_s

But my code uses contraction_count at 16 and recovery_time_s at 11, last_hr at 12, etc. So the table is now inconsistent. I need to update it to match actual code. Or better, update the code to match the table. Let me update the table to match the code:

- 0 discipline
- 1 depth_m RECORD
- 2 hr_bpm RECORD
- 3 max_depth_m
- 4 dive_time_s
- 5 surface_interval_s
- 6 horizontal_distance_m
- 7 length_count
- 8 stroke_count
- 9 breathhold_time_s
- 10 breath_up_time_s
- 11 recovery_time_s
- 12 last_hr_bpm
- 13 cycles_completed
- 14 total_hold_time_s
- 15 hold_target_s
- 16 contraction_count
- 20 lap_hold_s
- 21 lap_rest_s

Let me update the table accordingly.

| ID | Field | Message type |
|----|-------|--------------|
| 0 | `discipline` | SESSION |
| 1 | `depth_m` | RECORD |
| 2 | `hr_bpm` | RECORD |
| 3 | `max_depth_m` | SESSION |
| 4 | `dive_time_s` | SESSION |
| 5 | `surface_interval_s` | SESSION |
| 6 | `horizontal_distance_m` | SESSION |
| 7 | `length_count` | SESSION |
| 8 | `stroke_count` | SESSION |
| 9 | `breathhold_time_s` | SESSION |
| 10 | `breath_up_time_s` | SESSION |
| 11 | `recovery_time_s` | SESSION |
| 12 | `last_hr_bpm` | SESSION |
| 13 | `cycles_completed` | SESSION |
| 14 | `total_hold_time_s` | SESSION |
| 15 | `hold_target_s` | SESSION |
| 16 | `contraction_count` | SESSION |
| 20 | `lap_hold_s` | LAP (Breathing trainer) |
| 21 | `lap_rest_s` | LAP (Breathing trainer) |

| ID | Field | Message type |
|----|-------|--------------|
| 0 | `discipline` | SESSION |
| 1 | `depth_m` | RECORD |
| 2 | `hr_bpm` | RECORD |
| 3 | `max_depth_m` | SESSION |
| 4 | `dive_time_s` | SESSION |
| 5 | `surface_interval_s` | SESSION |
| 6 | `horizontal_distance_m` | SESSION |
| 7 | `length_count` | SESSION |
| 8 | `stroke_count` | SESSION |
| 9 | `contraction_count` | SESSION |
| 10 | `breathhold_time_s` | SESSION |
| 11 | `breath_up_time_s` | SESSION |
| 12 | `recovery_time_s` | SESSION |
| 13 | `last_hr_bpm` | SESSION |
| 14 | `cycles_completed` | SESSION |
| 15 | `total_hold_time_s` | SESSION |
| 16 | `hold_target_s` | SESSION |
| 20 | `lap_hold_s` | LAP (Breathing trainer) |
| 21 | `lap_rest_s` | LAP (Breathing trainer) |


Could not find the exact text in /home/liph/code/garmin-freedive-app/APP_DESIGN_V2.md. The old text must match exactly including all whitespace and newlines.


The edit failed. Maybe the table wasn't exactly as I wrote. Let me read the current table.


   - Add Breathing Trainer with configurable rest series and target hold.

3. **Phase 3 — Distance mode** (works on fēnix 3 too)
   - Move the manual pool-length logic out of the old breath-hold view into `DistanceView`.
   - Add fin-type selection and manual stroke counter.

4. **Phase 4 — Depth mode** (fēnix 5 only)
   - Create fēnix 5 build.
   - Add `DepthModel`, depth UI, depth FIT fields.

5. **Phase 5 — Polish**
   - Settings menu.
   - Common FIT helper.
   - README update.

---

## 11. Open Decisions Needed from You

Before I start coding, please confirm or correct these assumptions:

1. **Fin type selection** — keep the 3 options (bi-fins, monofin, no-fins) for both Depth and Distance?
2. **Free Immersion (FIM)** — should it appear under Depth alongside Constant Weight, or as its own top-level item?
3. **Strokes vs contractions** — in Distance mode, should DOWN count **strokes** (arm pulls) or **contractions**? We can make it configurable.
4. **Speed** — skip it entirely because it is not measurable underwater, or keep a manual stroke counter and derive an approximate average speed from `distance / breathhold_time` in the FIT summary?
5. **Breathing trainer defaults** — is the rest series `3:00 → 2:30 → 2:00 → 1:30 → 1:00` OK? How many cycles? Should target hold time be editable per session or only in settings?
6. **Build strategy** — should I implement the menu + non-depth modes first so they work on your existing fēnix 3, then create the fēnix 5 depth build?
7. **Depth on Static mode** — do you want the watch to also log depth/pressure while you do static breath-holds at the surface? (Normally it stays at 0 m.)

Once you confirm, I will start with **Phase 1** and update the code.

[1 more lines in file. Use offset=375 to continue.]


It seems my earlier edit to replace the phases and open decisions didn't take effect. The file still has the old content. Let me check if my earlier edit to APP_DESIGN_V2.md actually succeeded. It seems not. I need to update the design doc properly. Let me read more to find the FIT field table.

ffgrep

FIT Field ID Allocation|contraction_count|hold_target_s|lap_hold_s


No matches found


The ffgrep isn't finding matches. Maybe the file is not indexed or the path is wrong. Let me use bash grep.

bash

grep -n "FIT Field" /home/liph/code/garmin-freedive-app/APP_DESIGN_V2.md


306:## 9. FIT Field ID Allocation


## 9. FIT Field ID Allocation

Use a single ID space so one `.iq`/`.prg` does not collide. Recommended allocation:

| ID | Field | Message type |
|----|-------|--------------|
| 0 | `discipline` | SESSION |
| 1 | `depth_m` | RECORD |
| 2 | `hr_bpm` | RECORD |
| 3 | `max_depth_m` | SESSION |
| 4 | `dive_time_s` | SESSION |
| 5 | `surface_interval_s` | SESSION |
| 6 | `horizontal_distance_m` | SESSION |
| 7 | `length_count` | SESSION |
| 8 | `stroke_count` | SESSION |
| 9 | `contraction_count` | SESSION |
| 10 | `breathhold_time_s` | SESSION |
| 10 | `breath_up_time_s` | SESSION |
| 11 | `recovery_time_s` | SESSION |
| 12 | `last_hr_bpm` | SESSION |
| 13 | `cycles_completed` | SESSION |
| 14 | `total_hold_time_s` | SESSION |
| 15 | `hold_target_s` | SESSION |
| 20 | `lap_hold_s` | LAP (Breathing trainer) |
| 21 | `lap_rest_s` | LAP (Breathing trainer) |

---

## 10. Implementation Phases

[40 more lines in file. Use offset=336 to continue.]


I see there's a duplicate ID 10 and the table needs fixing. The earlier edit partially worked (changed one 9 to contraction_count, but left breath_up at 10 and breathhold at 10). I need to fix the table. Let me use a smaller edit.

Actually, the user wants me to implement, not perfect the docs. The docs can be updated later. The table has errors but it's not critical for compilation. I'll note it and move on. However, I should at least fix the duplicate ID 10 and align with code. Let me make a targeted edit.

edit

| 8 | `stroke_count` | SESSION |
| 9 | `breathhold_time_s` | SESSION |
| 10 | `breath_up_time_s` | SESSION |
| 11 | `recovery_time_s` | SESSION |
| 12 | `last_hr_bpm` | SESSION |
| 13 | `cycles_completed` | SESSION |
| 14 | `total_hold_time_s` | SESSION |
| 15 | `hold_target_s` | SESSION |
| 16 | `contraction_count` | SESSION |
| 20 | `lap_hold_s` | LAP (Breathing trainer) |

| 8 | `stroke_count` | SESSION |
| 9 | `contraction_count` | SESSION |
| 10 | `breathhold_time_s` | SESSION |
| 10 | `breath_up_time_s` | SESSION |
| 11 | `recovery_time_s` | SESSION |
| 12 | `last_hr_bpm` | SESSION |
| 13 | `cycles_completed` | SESSION |
| 14 | `total_hold_time_s` | SESSION |
| 15 | `hold_target_s` | SESSION |
| 20 | `lap_hold_s` | LAP (Breathing trainer) |


     ...
 317 | 5 | `surface_interval_s` | SESSION |
 318 | 6 | `horizontal_distance_m` | SESSION |
 319 | 7 | `length_count` | SESSION |
 320 | 8 | `stroke_count` | SESSION |
-321 | 9 | `contraction_count` | SESSION |
-322 | 10 | `breathhold_time_s` | SESSION |
+321 | 9 | `breathhold_time_s` | SESSION |
 323 | 10 | `breath_up_time_s` | SESSION |
 324 | 11 | `recovery_time_s` | SESSION |
 325 | 12 | `last_hr_bpm` | SESSION |
 326 | 13 | `cycles_completed` | SESSION |
 327 | 14 | `total_hold_time_s` | SESSION |
 328 | 15 | `hold_target_s` | SESSION |
+328 | 16 | `contraction_count` | SESSION |
 329 | 20 | `lap_hold_s` | LAP (Breathing trainer) |
 330 | 21 | `lap_rest_s` | LAP (Breathing trainer) |
 331
 332 ---
     ...

--- /home/liph/code/garmin-freedive-app/APP_DESIGN_V2.md
+++ /home/liph/code/garmin-freedive-app/APP_DESIGN_V2.md
@@ -317,16 +317,16 @@
 | 5 | `surface_interval_s` | SESSION |
 | 6 | `horizontal_distance_m` | SESSION |
 | 7 | `length_count` | SESSION |
 | 8 | `stroke_count` | SESSION |
-| 9 | `contraction_count` | SESSION |
-| 10 | `breathhold_time_s` | SESSION |
+| 9 | `breathhold_time_s` | SESSION |
 | 10 | `breath_up_time_s` | SESSION |
 | 11 | `recovery_time_s` | SESSION |
 | 12 | `last_hr_bpm` | SESSION |
 | 13 | `cycles_completed` | SESSION |
 | 14 | `total_hold_time_s` | SESSION |
 | 15 | `hold_target_s` | SESSION |
+| 16 | `contraction_count` | SESSION |
 | 20 | `lap_hold_s` | LAP (Breathing trainer) |
 | 21 | `lap_rest_s` | LAP (Breathing trainer) |

 ---


Good. Now I need to also fix the duplicate 10 issue in the code? Wait, in the code:
- DepthView uses breath_up_time_s id 10
- DistanceView uses breath_up_time_s id 10
- StaticView uses breath_up_time_s id 10
- BreathingView uses hold_target_s id 15

And breathhold_time_s is id 9 in DistanceView and StaticView. So no duplicate IDs in actual code. Good.

Now I should update the README with v2 build info. But maybe more importantly, I should note that the code is written but not compiled/tested because monkeyc isn't installed.

Let me also think about other potential compile issues:

1. `AppSettings.breathingRestSeries` - storing an array via `app.setProperty` may or may not work on fēnix 5. Object store can store basic types including arrays? The docs say properties can be Number, Float, Double, Boolean, String, Array, Dictionary. So array should work.

2. `FitContributor.DATA_TYPE_STRING` - `setData` expects a String. We pass `discipline` which is a String. Good. For `disciplineField.setData("BREATHE")` in BreathingView, also good.

3. `String.equals()` - I used `discipline.equals("CWT")`. In Monkey C, String has `equals()` method? Let me verify. Actually, the safest way is `discipline.compareTo("CWT") == 0` or just `discipline == "CWT"` might work for string literals. Hmm.

In Monkey C, `==` on objects compares references. For String literals, the compiler might intern them so `==` could work, but it's risky. `String.equals()` should exist. Let me verify with a quick web search or just trust that it exists.

Actually, in many Monkey C examples I see `if (someString.equals("foo"))`. So it should work.

4. `ActivityRecording.SPORT_TRAINING` and `SUB_SPORT_CARDIO_TRAINING` - these exist. `SPORT_SWIMMING` etc exist.

5. `session.addLap()` - exists in ActivityRecording.Session. Good.

6. `WatchUi.switchToView` - exists. Good.

7. `dc.getHeight()` / `dc.getWidth()` - exists. Good.

8. `Graphics.FONT_NUMBER_HOT` - might not exist. Let me be safe and change to `FONT_NUMBER_MEDIUM` or `FONT_NUMBER`. Actually, the available fonts on devices:
   - FONT_XTINY, FONT_TINY, FONT_SMALL, FONT_MEDIUM, FONT_LARGE, FONT_NUMBER_MEDIUM, FONT_NUMBER_HOT, FONT_NUMBER_THAI_HOT, FONT_SYSTEM_NUMBER_MEDIUM, FONT_SYSTEM_NUMBER_HOT, FONT_SYSTEM_SMALL, FONT_SYSTEM_TINY, FONT_SYSTEM_MEDIUM, FONT_SYSTEM_LARGE

   FONT_NUMBER_HOT is available on most devices. I'll keep it.

9. `Array.size()` - yes.

10. The `Method` field in SubMenuView is not typed. That's fine in Monkey C.

11. In `DepthView`, I use `state` field which is public (var). The delegate accesses `view.state`. That's fine.

12. In `DistanceView`, I use `state` etc. The delegate accesses them. Good.

13. `MainMenuView.switchToMainMenu` is static. Good.

14. In `BreathingView.STATE_SETUP_REST`, `infoText = "CYCLES " + AppSettings.breathingRestSeries.size();` - `.size()` on array. Good.

15. In `BreathingView`, `currentRestIndex` and `cycleCount` increment. Good.

16. In `BreathingView.finishAll`, `timer.stop()` is called. But `onTimer` might still be in progress. That's fine.

17. In `DepthDelegate`, I reference `DepthModel` without importing? DepthDelegate.mc uses `DepthModel.reset()` but doesn't have `using Toybox.Activity;`. However, `DepthModel` is in the same project and Monkey C doesn't require imports for classes in the same source path. So it should find it. Same for `MainMenuView`.

Actually, Monkey C uses `using` for modules, but classes in the same package/source path are visible without import. The compiler should find them.

18. In `DepthDelegate.onKeyReleased`, I check `if (view.state == view.STATE_BREATH_UP || view.state == view.STATE_SURFACE_READY)` then switch to main menu. Good.

19. In `MainMenuView`, I use `method(:onDepthType)` etc. These are instance methods. The method reference is bound to self. Good.

20. In `SubMenuView.select()`, `onSelect.invoke(selectedIndex)`. Good.

21. The `SubMenuDelegate.onKeyPressed` has `if (view == null) { return false; }`. But view is set in constructor. Good.

22. `MainMenuDelegate` constructor takes view. Good.

23. `Fenix5App.getInitialView` returns `[view, delegate]`. Good.

I think the code is mostly OK. Let me also check for potential issues with the `has` operator in DepthModel. `info has :rawAmbientPressure` - good.

One more thing: in `DepthView.startRecording`, I use `ActivityRecording.SPORT_SWIMMING` and `SUB_SPORT_OPEN_WATER`. For fēnix 5, these might be accepted by `createSession` or might cause a fatal error if invalid for the watch model. According to the earlier research, `SPORT_SWIMMING` and `SUB_SPORT_OPEN_WATER` are basic enums and should be valid. But the forum post said some sport/subSport combos cause exceptions. `SPORT_SWIMMING` + `SUB_SPORT_OPEN_WATER` is a valid combination. `SPORT_TRAINING` + `SUB_SPORT_CARDIO_TRAINING` should be valid. `SPORT_TRAINING` + `SUB_SPORT_EXERCISE` is valid. `SPORT_SWIMMING` + `SUB_SPORT_LAP_SWIMMING` is valid. Good.

Let me now update the README to mention the v2 fēnix 5 build. Then I'll provide a summary.

Actually, before updating README, I should think about whether I should also create a settings view. The user wants settings for pool length, water type, breathing trainer, etc. Long-press BACK from breath-up currently does nothing (TODO). I should implement at least a basic settings menu. But that's a lot of code. Given the turn is already long, maybe I should implement a simple settings menu with the most important options: pool length and water type. Or I can leave it as TODO and note it.

Given the user said "make everything for fenix 5", I should try to implement the settings too. But it's a lot. Let me at least implement a basic settings picker for pool length and water type.

Actually, the existing `PoolLengthView` and `PoolLengthDelegate` could be reused, but they use the old `Settings` class, not `AppSettings`. I could either:
1. Create new settings views
2. Update PoolLengthView to use AppSettings

Since the user wants fēnix 5 focus, I'll create a simple `SettingsView` that uses AppSettings and allows changing:
- Pool length (25/50/custom)
- Water type (fresh/salt)
- Distance DOWN action (strokes/contractions)

This is manageable. Let me create it.

Actually, I realize I should also check the `PoolLengthView` - it uses `Settings.poolLengthM` and `Settings.poolLengthIndex`. If I don't update it, it won't work with the v2 app. Since I'm focusing on fēnix 5, I can leave the old v1 files alone and create new ones. But the old `Settings.mc` conflicts with `AppSettings.mc` only in name - they are separate classes. The old `PoolLengthView` uses the old `Settings` class. That's fine for the v1 fēnix 3 app.

For v2, I'll create a new `SettingsView` that uses `AppSettings`.

Let me create a simple settings view.

write

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

class SettingsView extends WatchUi.View {

    const ITEM_POOL_LENGTH = 0;
    const ITEM_WATER_TYPE  = 1;
    const ITEM_DOWN_ACTION = 2;

    const ITEMS = ["Pool length", "Water type", "Distance DOWN"];

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

        var itemY = y + 40;
        for (var i = 0; i < ITEMS.size(); i++) {
            var prefix = (i == selectedIndex) ? "> " : "  ";
            var value = "";
            if (i == ITEM_POOL_LENGTH) { value = AppSettings.poolLengthM + "m"; }
            else if (i == ITEM_WATER_TYPE) { value = (AppSettings.waterType == 1) ? "salt" : "fresh"; }
            else if (i == ITEM_DOWN_ACTION) { value = (AppSettings.distanceDownAction == 0) ? "strokes" : "contractions"; }
            dc.drawText(cx, itemY, Graphics.FONT_MEDIUM, prefix + ITEMS[i] + " " + value, Graphics.TEXT_JUSTIFY_CENTER);
            itemY += 34;
        }

        dc.drawText(cx, dc.getHeight() - 20, Graphics.FONT_TINY,
            "UP/DOWN=move START=edit BACK=back", 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_POOL_LENGTH) {
            var subView = new PoolLengthSettingsView();
            WatchUi.pushView(subView, new SettingsEditDelegate(subView), WatchUi.SLIDE_LEFT);
        } else if (selectedIndex == ITEM_WATER_TYPE) {
            var subView = new WaterTypeSettingsView();
            WatchUi.pushView(subView, new SettingsEditDelegate(subView), WatchUi.SLIDE_LEFT);
        } else if (selectedIndex == ITEM_DOWN_ACTION) {
            var subView = new DownActionSettingsView();
            WatchUi.pushView(subView, new SettingsEditDelegate(subView), WatchUi.SLIDE_LEFT);
        }
    }
}


Successfully wrote 2443 bytes to /home/liph/code/garmin-freedive-app/source/SettingsView.mc


using Toybox.WatchUi;

class SettingsEditDelegate extends WatchUi.InputDelegate {

    var view;

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

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

        var key = keyEvent.getKey();

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

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

        if (key == WatchUi.KEY_START || key == WatchUi.KEY_ENTER) {
            view.onSave();
            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 1148 bytes to /home/liph/code/garmin-freedive-app/source/SettingsEditDelegate.mc


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

class PoolLengthSettingsView extends WatchUi.View {

    const PRESET_25 = 0;
    const PRESET_50 = 1;
    const PRESET_CUSTOM = 2;
    const PRESET_VALUES = [25, 50];

    var selectedIndex;
    var customValue;
    var inCustomEdit = false;

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

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

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

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

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

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

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

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

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

    function onSave() {
        if (selectedIndex == PRESET_CUSTOM && !inCustomEdit) {
            inCustomEdit = true;
            WatchUi.requestUpdate();
        } else {
            if (selectedIndex == PRESET_CUSTOM) {
                AppSettings.poolLengthM = customValue;
            } else {
                AppSettings.poolLengthM = PRESET_VALUES[selectedIndex];
            }
            AppSettings.poolLengthIndex = selectedIndex;
            AppSettings.save();
            WatchUi.popView(WatchUi.SLIDE_RIGHT);
        }
    }
}


Successfully wrote 2984 bytes to /home/liph/code/garmin-freedive-app/source/PoolLengthSettingsView.mc


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

class WaterTypeSettingsView extends WatchUi.View {

    const ITEM_FRESH = 0;
    const ITEM_SALT  = 1;

    var selectedIndex;

    function initialize() {
        View.initialize();
        selectedIndex = AppSettings.waterType;
    }

    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, "WATER TYPE", Graphics.TEXT_JUSTIFY_CENTER);

        var freshPrefix = (selectedIndex == ITEM_FRESH) ? "> " : "  ";
        var saltPrefix  = (selectedIndex == ITEM_SALT)  ? "> " : "  ";

        dc.drawText(cx, y + 50, Graphics.FONT_MEDIUM, freshPrefix + "Fresh", Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, y + 90, Graphics.FONT_MEDIUM, saltPrefix + "Salt", Graphics.TEXT_JUSTIFY_CENTER);

        dc.drawText(cx, dc.getHeight() - 22, Graphics.FONT_TINY,
            "UP/DOWN=move START=save BACK=cancel", Graphics.TEXT_JUSTIFY_CENTER);
    }

    function onUp() {
        selectedIndex--;
        if (selectedIndex < 0) { selectedIndex = 0; }
        WatchUi.requestUpdate();
    }

    function onDown() {
        selectedIndex++;
        if (selectedIndex > 1) { selectedIndex = 1; }
        WatchUi.requestUpdate();
    }

    function onSave() {
        AppSettings.waterType = selectedIndex;
        AppSettings.save();
        WatchUi.popView(WatchUi.SLIDE_RIGHT);
    }
}


Successfully wrote 1584 bytes to /home/liph/code/garmin-freedive-app/source/WaterTypeSettingsView.mc


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

class DownActionSettingsView extends WatchUi.View {

    const ITEM_STROKES = 0;
    const ITEM_CONTRACTIONS = 1;

    var selectedIndex;

    function initialize() {
        View.initialize();
        selectedIndex = AppSettings.distanceDownAction;
    }

    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, "DISTANCE DOWN", Graphics.TEXT_JUSTIFY_CENTER);

        var strokePrefix = (selectedIndex == ITEM_STROKES) ? "> " : "  ";
        var contPrefix   = (selectedIndex == ITEM_CONTRACTIONS) ? "> " : "  ";

        dc.drawText(cx, y + 50, Graphics.FONT_MEDIUM, strokePrefix + "Count strokes", Graphics.TEXT_JUSTIFY_CENTER);
        dc.drawText(cx, y + 90, Graphics.FONT_MEDIUM, contPrefix + "Count contractions", Graphics.TEXT_JUSTIFY_CENTER);

        dc.drawText(cx, dc.getHeight() - 22, Graphics.FONT_TINY,
            "UP/DOWN=move START=save BACK=cancel", Graphics.TEXT_JUSTIFY_CENTER);
    }

    function onUp() {
        selectedIndex--;
        if (selectedIndex < 0) { selectedIndex = 0; }
        WatchUi.requestUpdate();
    }

    function onDown() {
        selectedIndex++;
        if (selectedIndex > 1) { selectedIndex = 1; }
        WatchUi.requestUpdate();
    }

    function onSave() {
        AppSettings.distanceDownAction = selectedIndex;
        AppSettings.save();
        WatchUi.popView(WatchUi.SLIDE_RIGHT);
    }
}


Successfully wrote 1649 bytes to /home/liph/code/garmin-freedive-app/source/DownActionSettingsView.mc


Now I need to wire up the settings view. I need:
1. A `SettingsDelegate` for the settings menu
2. Update DepthDelegate/DistanceDelegate/StaticDelegate/BreathingDelegate to push settings on long-press BACK

Let me create SettingsDelegate.

using Toybox.WatchUi;
using Toybox.System;

class SettingsDelegate extends WatchUi.InputDelegate {

    var view;

    function initialize(v) {
        InputDelegate.initialize();
        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 1169 bytes to /home/liph/code/garmin-freedive-app/source/SettingsDelegate.mc


Now update the delegates to push the settings view on long-press BACK. Let me edit each one.

edit

    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) {
            var settingsView = new SettingsView();
            WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
        }
    }

    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 replaced 1 block(s) in /home/liph/code/garmin-freedive-app/source/DepthDelegate.mc.

    ...
 71         backTimer = null;
 72         // Open settings from breath-up or surface states.
 73         if (view.state == view.STATE_BREATH_UP ||
 74             view.state == view.STATE_SURFACE_READY) {
-75             // TODO: push settings view.
-76             WatchUi.requestUpdate();
+75             var settingsView = new SettingsView();
+76             WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
 77         }
 78     }
 79 }

--- /home/liph/code/garmin-freedive-app/source/DepthDelegate.mc
+++ /home/liph/code/garmin-freedive-app/source/DepthDelegate.mc
@@ -71,9 +71,9 @@
         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();
+            var settingsView = new SettingsView();
+            WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
         }
     }
 }


    function onBackLongPress() {
        backTimer = null;
        if (view.state == view.STATE_BREATH_UP) {
            var settingsView = new SettingsView();
            WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
        }
    }

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


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

    ...
 76     }
 77
 78     function onBackLongPress() {
 79         backTimer = null;
-80         // TODO: open settings view.
+80         if (view.state == view.STATE_BREATH_UP) {
+81             var settingsView = new SettingsView();
+82             WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
+83         }
 81     }
 82 }

--- /home/liph/code/garmin-freedive-app/source/DistanceDelegate.mc
+++ /home/liph/code/garmin-freedive-app/source/DistanceDelegate.mc
@@ -76,7 +76,10 @@
     }

     function onBackLongPress() {
         backTimer = null;
-        // TODO: open settings view.
+        if (view.state == view.STATE_BREATH_UP) {
+            var settingsView = new SettingsView();
+            WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
+        }
     }
 }


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

    ...
 65     }
 66
 67     function onBackLongPress() {
 68         backTimer = null;
-69         // TODO: open settings view.
+69         if (view.state == view.STATE_BREATH_UP) {
+70             var settingsView = new SettingsView();
+71             WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
+72         }
 70     }
 71 }

--- /home/liph/code/garmin-freedive-app/source/StaticDelegate.mc
+++ /home/liph/code/garmin-freedive-app/source/StaticDelegate.mc
@@ -65,7 +65,10 @@
     }

     function onBackLongPress() {
         backTimer = null;
-        // TODO: open settings view.
+        if (view.state == view.STATE_BREATH_UP) {
+            var settingsView = new SettingsView();
+            WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
+        }
     }
 }


    function onBackLongPress() {
        backTimer = null;
        if (view.state == view.STATE_SETUP_TARGET || view.state == view.STATE_SETUP_REST) {
            var settingsView = new SettingsView();
            WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
        }
    }


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

    ...
 80     }
 81
 82     function onBackLongPress() {
 83         backTimer = null;
-84         // TODO: open settings view.
+84         if (view.state == view.STATE_SETUP_TARGET || view.state == view.STATE_SETUP_REST) {
+85             var settingsView = new SettingsView();
+86             WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
+87         }
 85     }
 86 }

--- /home/liph/code/garmin-freedive-app/source/BreathingDelegate.mc
+++ /home/liph/code/garmin-freedive-app/source/BreathingDelegate.mc
@@ -80,7 +80,10 @@
     }

     function onBackLongPress() {
         backTimer = null;
-        // TODO: open settings view.
+        if (view.state == view.STATE_SETUP_TARGET || view.state == view.STATE_SETUP_REST) {
+            var settingsView = new SettingsView();
+            WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
+        }
     }
 }


Now I need to also add a way to access settings from the main menu. Long-press BACK from main menu should open settings. Currently MainMenuDelegate exits on BACK short. Let me update MainMenuDelegate to handle long-press BACK for settings.

I'll add a backTimer to MainMenuDelegate.

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

class MainMenuDelegate extends WatchUi.InputDelegate {

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

    function initialize(v) {
        InputDelegate.initialize();
        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) {
            backTimer = new Timer.Timer();
            backTimer.start(method(:onBackLongPress), LONG_PRESS_MS, false);
            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;
    }

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

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

        return false;
    }

    function onBackLongPress() {
        backTimer = null;
        var settingsView = new SettingsView();
        WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
    }
}

using Toybox.WatchUi;
using Toybox.System;

class MainMenuDelegate extends WatchUi.InputDelegate {

    var view;

    function initialize(v) {
        InputDelegate.initialize();
        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 replaced 1 block(s) in /home/liph/code/garmin-freedive-app/source/MainMenuDelegate.mc.

  1 using Toybox.WatchUi;
  2 using Toybox.System;
+ 3 using Toybox.Timer;
  3
  4 class MainMenuDelegate extends WatchUi.InputDelegate {
  5
  6     var view;
+ 8     var backTimer = null;
+ 9     const LONG_PRESS_MS = 800;
  7
  8     function initialize(v) {
  9         InputDelegate.initialize();
 10         view = v;
    ...
 32             return true;
 33         }
 34
 35         if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
-36             System.exit();
+39             backTimer = new Timer.Timer();
+40             backTimer.start(method(:onBackLongPress), LONG_PRESS_MS, false);
 37             return true;
 38         }
 39
 40         return false;
    ...
 48             return true;
 49         }
 50         return false;
 51     }
+56
+57     function onKeyReleased(keyEvent) {
+58         var key = keyEvent.getKey();
+59
+60         if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
+61             if (backTimer != null) {
+62                 backTimer.stop();
+63                 backTimer = null;
+64                 System.exit();
+65             }
+66             return true;
+67         }
+68
+69         return false;
+70     }
+71
+72     function onBackLongPress() {
+73         backTimer = null;
+74         var settingsView = new SettingsView();
+75         WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
+76     }
 52 }

--- /home/liph/code/garmin-freedive-app/source/MainMenuDelegate.mc
+++ /home/liph/code/garmin-freedive-app/source/MainMenuDelegate.mc
@@ -1,10 +1,13 @@
 using Toybox.WatchUi;
 using Toybox.System;
+using Toybox.Timer;

 class MainMenuDelegate extends WatchUi.InputDelegate {

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

     function initialize(v) {
         InputDelegate.initialize();
         view = v;
@@ -32,9 +35,10 @@
             return true;
         }

         if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
-            System.exit();
+            backTimer = new Timer.Timer();
+            backTimer.start(method(:onBackLongPress), LONG_PRESS_MS, false);
             return true;
         }

         return false;
@@ -48,5 +52,26 @@
             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;
+                System.exit();
+            }
+            return true;
+        }
+
+        return false;
+    }
+
+    function onBackLongPress() {
+        backTimer = null;
+        var settingsView = new SettingsView();
+        WatchUi.pushView(settingsView, new SettingsDelegate(settingsView), WatchUi.SLIDE_LEFT);
+    }
 }


Good. Now I need to handle the BACK release from sub-menus properly too. Currently SubMenuDelegate pops on BACK short. I should add long-press detection? Or just keep it simple: BACK from sub-menu goes back. If the user long-presses BACK from sub-menu, it would also pop (since onKeyReleased fires after pop, but view is gone). Actually, SubMenuDelegate doesn't have onKeyReleased, only onKeyPressed. So BACK press immediately pops. That means a long-press from sub-menu would pop immediately on press, not wait. That's acceptable for now.

Actually, there's a subtle bug: when the user is in a sub-menu and presses BACK, it pops immediately. Then on release, the main menu's onKeyReleased might fire (because the main menu delegate is now active again), and since backTimer was started when pressing BACK in main menu to push the sub-menu... wait, no. The main menu delegate started a backTimer only when BACK is pressed. But the sub-menu was pushed because START was pressed, not BACK. So no timer. Good.

But when the user presses BACK from main menu, the main menu delegate starts a timer. If released before 800ms, it exits. If held, it opens settings. Good.

When the user presses BACK from sub-menu, SubMenuDelegate pops immediately. Then release happens on main menu, but no timer was started by main menu (because BACK press was handled by sub-menu delegate). Good.

Now, I need to also handle the case where the user long-presses BACK from a sub-menu - but SubMenuDelegate doesn't wait. That's OK for now.

One more thing: the SettingsEditDelegate uses `onSave()` which pops the view. But for pool length custom edit, onSave first sets `inCustomEdit = true` and then on next START saves and pops. Good.

Now, I should also consider if `WatchUi.pushView` with a view that's already in the stack is OK. It should be fine.

Let me now think about a potential issue with `SettingsView` and the settings sub-views. They are referenced in `MainMenuDelegate.onBackLongPress` etc. Good.

Another potential issue: in `DepthDelegate`, I have `using Toybox.Timer;` and `using Toybox.System;`. But I don't import MainMenuView or SettingsView. They are in the same source path, so the compiler should find them.

Now, let me update the README to document the v2 fēnix 5 build. I'll add a section after the fēnix 3 build instructions.

Actually, the README is currently focused on fēnix 3. I should add the v2 fēnix 5 build info. Let me read the README end to see where to add.