400 "The prompt is too long: 274006, model maximum context length: 262144 (ref: 7a91da85-2d33-4e1b-95ed-defa99d845fe)"


Summary:
## Goal
- Create a personal, sideloaded Garmin Connect IQ freediving/snorkeling app for the user’s existing **Garmin fēnix 3 / fēnix 3 HR**, scoped to breath-hold / surface heart-rate logging because depth measurement is unavailable on that hardware.
- Answer the user’s questions about which fēnix watch supports raw-ambient-pressure depth measurement, and provide a finalized implementation plan for adding manual pool-length horizontal-distance tracking to the fēnix 3 app.

## Constraints & Preferences
- **Current target hardware: fēnix 3 / fēnix 3 HR** (API Level 1.4.0, 10 ATM pressure-equivalent, ANT+ external HR support, 5 physical buttons, Memory-In-Pixel 218×218 screen).
- **Depth-capable fēnix recommendation: fēnix 5 / 5S / 5X / 5 Plus / 5S Plus / 5X Plus** — these support Connect IQ API 2.4.0+ (`Activity.Info.ambientPressure` / `rawAmbientPressure`), have a 10 ATM water rating, and forum/developer reports indicate they report accurate depth without artificial underwater pressure caps.
- **fēnix 6 / 7 / 8 and Tactix are unsuitable for depth measurement** despite supporting the pressure API, because forum reports say Garmin artificially caps pressure readings below ~3 m underwater.
- **Venu 2 is out of scope** because it lacks ANT+ support for the user’s HRM‑Tri chest strap and its touchscreen is unusable underwater.
- Max depth **30 m is not feasible on fēnix 3** because API 1.4 only exposes `Sensor.Info.pressure` (sea-level calibrated).
- Underwater HR: user owns **HRM‑Tri** ANT+ chest strap, which pairs with fēnix 3 and fēnix 5.
- Sideload-only distribution; no Connect IQ Store submission (freediving apps are banned).
- App type: **Watch App** with `ActivityRecording` + `FitContributor`.
- fēnix 3 is not dive-rated by Garmin (surface swimming only); fēnix 5 is also not dive-rated despite 10 ATM rating.
- **“Underwater distance” on a fēnix 5 means depth (vertical distance) only**; horizontal/3-D underwater distance cannot be measured because GPS does not work underwater and accelerometer dead-reckoning drifts too fast.
- **Pool-length horizontal distance on fēnix 3 must be manual** because third-party apps cannot access Garmin’s native stroke/turn-detection accelerometer pipeline.

## Progress
### Done
- [x] Researched and documented water-resistance ratings and diving limitations of Venu 2, fēnix 3, and fēnix 5.
- [x] Identified Connect IQ policy constraints and sideloading requirements.
- [x] Researched underwater HR limitations and compatible chest straps.
- [x] Drafted a full structured implementation plan (`code/garmin-freedive-app/IMPLEMENTATION_PLAN.md`) covering goal, constraints, approach, steps, verification, risks, and open questions.
- [x] Determined the fēnix 3 / Venu 2 Connect IQ API levels and pressure API support:
  - Venu 2: API 5.0.0, supports `Activity.Info.rawAmbientPressure` (API 2.4.0+).
  - fēnix 3 / fēnix 3 HR: API 1.4.0, does **not** support `rawAmbientPressure` or `ambientPressure`; only `Sensor.Info.pressure` (sea-level calibrated) is exposed.
- [x] Collected user decisions via questionnaire:
  - Water type: both (switchable in app).
  - HR: user has HRM‑Tri, which works with fēnix 3 but not Venu 2.
  - fēnix 3 scope: breath-hold / surface only (no depth).
  - Code path: focus on fēnix 3 with sideloading instructions.
- [x] Implemented the fēnix 3 breath-hold Watch App:
  - `code/garmin-freedive-app/manifest.xml`
  - `code/garmin-freedive-app/monkey.jungle`
  - `code/garmin-freedive-app/source/BreathHoldApp.mc`
  - `code/garmin-freedive-app/source/BreathHoldView.mc`
  - `code/garmin-freedive-app/source/BreathHoldDelegate.mc`
  - `code/garmin-freedive-app/resources/resources.xml`
  - `code/garmin-freedive-app/build.sh`
  - `code/garmin-freedive-app/.gitignore`
  - `code/garmin-freedive-app/README.md`
- [x] Verified and clarified the ambient-pressure distinction:
  - fēnix 3 hardware has a barometric altimeter and a native “Ambient Press.” data field.
  - Connect IQ API 1.4 does **not** expose that raw ambient pressure to third-party apps.
  - Only `Sensor.Info.pressure` (sea-level calibrated) is available in CIQ on fēnix 3.
- [x] Re-checked and confirmed fēnix 5 depth capability:
  - fēnix 5 / 5S / 5X / 5 Plus / 5S Plus / 5X Plus run Connect IQ API 3.1/3.3, which includes `Activity.Info.rawAmbientPressure`.
  - Forum/developer evidence shows fēnix 5 depth readings accurate to ~0.1 m vs a dive computer, usable to ~40 m, with no artificial underwater cap.
  - fēnix 6 / 7 / 8 / Tactix reportedly cap pressure readings around 3 m underwater.
- [x] Updated project docs with fēnix depth findings:
  - `code/garmin-freedive-app/IMPLEMENTATION_PLAN.md`
  - `code/garmin-freedive-app/README.md`
- [x] Fixed a missing `using Toybox.System;` import in `code/garmin-freedive-app/source/BreathHoldDelegate.mc`.
- [x] Added a `Which fēnix for depth?` section to README with the depth-vs-distance clarification and a sample `rawAmbientPressure` depth-calculation snippet.
- [x] Drafted and finalized a structured plan for manual pool-length horizontal-distance tracking:
  - Added as **Appendix D** in `code/garmin-freedive-app/IMPLEMENTATION_PLAN.md`.
  - Captured user decisions: **UP = length/lap**, **DOWN = contraction**, **25 m / 50 m / custom** presets, **long-press BACK** for settings, **custom developer FIT field**, **manual counting**.
- [x] Verified the final plan was written to `IMPLEMENTATION_PLAN.md`.

### In Progress
- (none — awaiting user go-ahead)

### Blocked
- (none)

## Key Decisions
- **Use fēnix 3 / fēnix 3 HR instead of Venu 2**: HRM‑Tri is ANT+ and does not pair with Venu 2; fēnix 3 physical buttons work underwater.
- **No depth measurement on fēnix 3**: fēnix 3 Connect IQ API 1.4 lacks `rawAmbientPressure`/`ambientPressure` (introduced in API 2.4.0); `Sensor.Info.pressure` is sea-level calibrated and auto-corrected for weather/altitude, making depth computation unreliable.
- **App scope is breath-hold / surface HR logging only**: prep → hold → recovery timer, contraction counter, FIT recording of hold time and last HR.
- **Sideload-only distribution**: Because Garmin prohibits freediving apps in the Connect IQ Store.
- **Fresh/salt toggle retained but not used for depth**: Kept in the code for future extensibility if a depth-capable device is later targeted.
- **Hardware CAN measure ambient pressure, CIQ app CANNOT read it raw on fēnix 3**: This distinction is now documented precisely.
- **Recommended depth-capable fēnix: fēnix 5 / 5S / 5X / 5 Plus / 5S Plus / 5X Plus**: These support API 2.4.0+ pressure fields, have a 10 ATM water rating, support ANT+ HR straps, and forum reports indicate they do not artificially cap underwater pressure readings.
- **Avoid fēnix 6 / 7 / 8 / Tactix for depth-based freediving app**: Forum reports indicate these models artificially cap pressure sensor readings underwater around 3 m.
- **fēnix 5 depth measurement is vertical depth only**: Horizontal/3-D underwater distance is not possible because GPS does not work underwater and accelerometer dead-reckoning is too unreliable.
- **Pool-length horizontal distance will be manual on fēnix 3**: Third-party apps cannot access native stroke/turn detection; distance = `lengthCount × poolLengthM`.
- **Pool-length button mapping**: **UP = length/lap**, **DOWN = contraction**.
- **Pool-length presets**: **25 m**, **50 m**, **custom**.
- **Pool-length settings access**: **Long-press BACK** from idle state.
- **Pool-length distance FIT field**: **Custom developer field** only (`horizontal_distance_m` and `length_count`); do not write Garmin’s standard `distance` field.

## Next Steps
1. **Implement the pool-length horizontal-distance feature** per Appendix D in `code/garmin-freedive-app/IMPLEMENTATION_PLAN.md` when the user gives the go-ahead.
   - Add pool-length storage and picker (`PoolLengthView.mc`, `PoolLengthDelegate.mc`).
   - Route **UP = length/lap**, **DOWN = contraction** in `BreathHoldDelegate.mc`.
   - Add `lengthCount` / `horizontalDistanceM` state to `BreathHoldView.mc`.
   - Add custom FIT fields `horizontal_distance_m` and `length_count`.
   - Update UI and README.
2. Build for `fenix3` / `fenix3_hr` and verify in the simulator.
3. Sideload to the real fēnix 3 and test in a pool.
4. If the user later acquires a fēnix 5-series watch (or a Descent), port the depth logic using `Activity.Info.rawAmbientPressure`.

## Critical Context
- **fēnix 3 pressure API reality**: Hardware measures ambient pressure and the native watch UI exposes it as “Ambient Press.”, but Connect IQ API 1.4 only gives `Sensor.Info.pressure`, which is documented as “barometric pressure calibrated to sea level.”
- **API level gating**: `Activity.Info.ambientPressure` and `Activity.Info.rawAmbientPressure` require **API Level 2.4.0+**; fēnix 3 / fēnix 3 HR are capped at **API Level 1.4.0**.
- **fēnix 5 API level**: fēnix 5 / 5S / 5X = API 3.1.x; fēnix 5 Plus / 5S Plus / 5X Plus = API 3.2.x/3.3.x. All support `rawAmbientPressure` (introduced in API 2.4.0).
- **fēnix 5 depth evidence**: Forum reports (Dive IQ, Apnea.me, etc.) indicate fēnix 5 gives accurate freediving depth (~0.1 m vs a dive computer, usable to ~40 m) without the artificial cap that newer models have.
- **fēnix 6/7/8/Tactix depth cap**: Forum/developer reports say these models deliberately limit pressure sensor readings below ~3 m underwater, likely to protect the Descent dive-computer product line.
- **Correct statement to document**: “The fēnix 3 hardware measures ambient pressure and the native data field shows it, but a Connect IQ app on the fēnix 3 cannot access that raw value; it only sees sea-level-calibrated pressure.”
- **Water resistance caveat**: fēnix 3 and fēnix 5 are rated 10 ATM (pressure equivalent to 100 m), but Garmin explicitly states they are for surface swimming, not dive-rated. Using them for repeated 30 m freediving is outside the stated use case and may void warranty / risk damage.
- **ANT+ HR**: fēnix 5 officially supports ANT+ chest straps such as HRM‑Tri, matching the user’s existing strap.
- **Code fix applied**: Added `using Toybox.System;` to `code/garmin-freedive-app/source/BreathHoldDelegate.mc` to resolve the `System.exit()` compile error.
- **Build status**: The `.PRG` was not built because the Connect IQ SDK is not installed in this environment (`monkeyc` not found).
- **Pool-length plan location**: Finalized plan is **Appendix D** of `code/garmin-freedive-app/IMPLEMENTATION_PLAN.md`.
- **Project location**: All code and plans are in `~/code/garmin-freedive-app/`.

<modified-files>
code/garmin-freedive-app/.gitignore
code/garmin-freedive-app/IMPLEMENTATION_PLAN.md
code/garmin-freedive-app/README.md
code/garmin-freedive-app/build.sh
code/garmin-freedive-app/manifest.xml
code/garmin-freedive-app/monkey.jungle
code/garmin-freedive-app/resources/resources.xml
code/garmin-freedive-app/source/BreathHoldApp.mc
code/garmin-freedive-app/source/BreathHoldDelegate.mc
code/garmin-freedive-app/source/BreathHoldView.mc
</modified-files>

---

**Turn Context (split turn):**

## Original Request
The user asked the assistant to start implementing a pool-length horizontal distance feature for a Garmin Connect IQ freediving/breath-hold app targeting fēnix 3 / fēnix 3 HR (API 1.4.0).

## Early Progress
- Confirmed the implementation approach from the prior plan: manual length counting, pool-length picker with 25 m / 50 m / custom presets, long-press BACK from idle to open settings, UP = length during hold, DOWN = contraction, custom FIT fields only.
- Created/edited files:
  - `source/Settings.mc` — pool-length persistence via app object store.
  - `source/PoolLengthView.mc` and `source/PoolLengthDelegate.mc` — picker UI and input handling.
  - `source/BreathHoldApp.mc` — loads settings on start.
  - `source/BreathHoldDelegate.mc` — long-press BACK detection, UP/DOWN routing.
  - `source/BreathHoldView.mc` — length/distance state, display updates, custom FIT fields.
  - `README.md` and `IMPLEMENTATION_PLAN.md` — updated usage and marked feature as implemented.

## Context for Suffix
- The assistant is reviewing the edits for correctness.
- A bug was found in `BreathHoldDelegate.onBackLongPress()`: it originally pushed one `PoolLengthView` and passed a *different* `PoolLengthView` to the delegate. The assistant is fixing this by creating a single picker view instance and reusing it for both the pushed view and the delegate.
- The assistant is also sanity-checking whether Monkey C supports calling `Settings.load()` / `Settings.save()` as class-level methods on instance methods, and whether static class fields work as expected, and was running a web search to verify. The build/test step is pending because the Connect IQ SDK is not installed in this environment.

<read-files>
/home/liph/code/garmin-freedive-app/source/BreathHoldView.mc
~/code/garmin-freedive-app/build.sh
~/code/garmin-freedive-app/manifest.xml
~/code/garmin-freedive-app/monkey.jungle
~/code/garmin-freedive-app/resources/resources.xml
</read-files>

<modified-files>
/home/liph/code/garmin-freedive-app/IMPLEMENTATION_PLAN.md
/home/liph/code/garmin-freedive-app/README.md
code/garmin-freedive-app/.gitignore
code/garmin-freedive-app/IMPLEMENTATION_PLAN.md
code/garmin-freedive-app/README.md
code/garmin-freedive-app/build.sh
code/garmin-freedive-app/manifest.xml
code/garmin-freedive-app/monkey.jungle
code/garmin-freedive-app/resources/resources.xml
code/garmin-freedive-app/source/BreathHoldApp.mc
code/garmin-freedive-app/source/BreathHoldDelegate.mc
code/garmin-freedive-app/source/BreathHoldView.mc
~/code/garmin-freedive-app/IMPLEMENTATION_PLAN.md
~/code/garmin-freedive-app/README.md
~/code/garmin-freedive-app/source/BreathHoldApp.mc
~/code/garmin-freedive-app/source/BreathHoldDelegate.mc
~/code/garmin-freedive-app/source/BreathHoldView.mc
~/code/garmin-freedive-app/source/PoolLengthDelegate.mc
~/code/garmin-freedive-app/source/PoolLengthView.mc
~/code/garmin-freedive-app/source/Settings.mc
</modified-files>


Let me read the current state of the files to understand what needs to be fixed and verified.


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

class BreathHoldDelegate extends WatchUi.InputDelegate {

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

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

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

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

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

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

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

        return false;
    }

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

        if (key == WatchUi.KEY_ESC || key == WatchUi.KEY_LAP) {
            if (backTimer != null) {
                // Released before the long-press threshold: short press, exit the app.
                backTimer.stop();
                backTimer = null;
                System.exit();
            }
            return true;
        }

        return false;
    }

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


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

class PoolLengthView extends WatchUi.View {

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

    var selectedIndex;
    var customValue;
    var inCustomEdit = false;
    var createdTime;

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

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

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

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

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

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

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

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

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

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

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


using Toybox.WatchUi;
using Toybox.System;

class PoolLengthDelegate extends WatchUi.InputDelegate {

    var view;

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

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

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

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

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

        return false;
    }

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

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

        return false;
    }
}


using Toybox.Application;

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

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

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


The delegate bug is already fixed - a single pickerView instance is created and reused. Now I need to verify a few things:

1. In Settings.mc, `load()` and `save()` are instance methods but called as `Settings.load()` and `Settings.save()` in other files. In Monkey C, can you call instance methods on a class name? No - that would be a compile error. They should be `static function load()` and `static function save()`. However, Monkey C might have some quirks. Let me check the current BreathHoldApp.mc and BreathHoldView.mc to see how they're called.

2. Static class fields in Monkey C - yes, they work with `static var`.

3. The `Settings.load()` call in BreathHoldApp needs to be checked.

Let me read those files.


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

class BreathHoldApp extends Application.AppBase {

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

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

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

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

    function getInitialView() {
        var view = new BreathHoldView();
        var delegate = new BreathHoldDelegate(view);
        return [view, delegate];
    }
}


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

class BreathHoldView extends WatchUi.View {

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

    var state = STATE_IDLE;
    var timer = null;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        updateHeartRate();
        WatchUi.requestUpdate();
    }

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

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

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

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

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

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

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

            session.start();
        }
    }

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

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

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

        state = STATE_DONE;
        WatchUi.requestUpdate();
    }

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

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

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

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


<?xml version="1.0"?>
<iq:manifest xmlns:iq="http://www.garmin.com/connect-iq" version="1">
    <iq:application entry="BreathHoldApp" id="b896271ba7e04e12988fd03fb8714fc3" minApiLevel="1.4.0" type="watch-app" name="@Strings.AppName">
        <iq:products>
            <iq:product id="fenix3"/>
            <iq:product id="fenix3_hr"/>
        </iq:products>
        <iq:permissions>
            <iq:permission id="Sensor"/>
            <iq:permission id="ActivityRecording"/>
            <iq:permission id="FitContributor"/>
        </iq:permissions>
        <iq:languages>
            <iq:language>eng</iq:language>
        </iq:languages>
    </iq:application>
</iq:manifest>


project.manifest = manifest.xml
base.sourcePath = source
base.resourcePath = resources


Now I see the issue clearly. In `Settings.mc`:

```monkeyc
class Settings {
    static var poolLengthM = 25;
    static var poolLengthIndex = 0;

    function load() { ... }
    function save() { ... }
}
```

But `BreathHoldApp.mc` calls `Settings.load();` and `PoolLengthView.mc` calls `Settings.save();`. In Monkey C, you cannot call instance methods on a class name like that. They need to be declared as `static function load()` and `static function save()`.

Also, `Settings.load()` in `onStart` - in Monkey C, `AppBase.onStart` receives a `state` dictionary, but that's fine.

Wait, also in `BreathHoldApp.mc`, `Settings.load()` is called but `Settings` class is not imported with `using`. Actually since it's in the same source directory, Monkey C will find it automatically without an import, so that's fine.

The main fix needed: make `load()` and `save()` static in `Settings.mc`.

Let me also double-check if there are any other issues:

1. In `PoolLengthView.mc`, `PRESET_VALUES = [25, 50]` - array indexing with static const array should work.
2. In `PoolLengthView.onUpdate()`, `Graphics.FONT_NUMBER_MEDIUM` - need to verify this exists in API 1.4. Looking at Garmin docs, FONT_NUMBER_MEDIUM might not exist. The standard ones are FONT_XTINY, FONT_TINY, FONT_SMALL, FONT_MEDIUM, FONT_LARGE, FONT_NUMBER_SMALL, FONT_NUMBER_MEDIUM, FONT_NUMBER_HOT, FONT_NUMBER_THAI_HOT. Actually FONT_NUMBER_MEDIUM should exist. Let me not worry about this since it was in the original code.
3. In `BreathHoldView.mc`, `FitContributor.DATA_TYPE_UINT16`, `FitContributor.DATA_TYPE_UINT8` - need to verify. Actually in Connect IQ, the FitContributor module uses `FitContributor.DATA_TYPE_*` constants. For API 1.4, FitContributor might not exist? Wait, looking at the manifest, it requests `FitContributor` permission. The FitContributor module was introduced later. Let me check when FitContributor was available.

Actually, I should verify this. The `FitContributor` module was introduced in API Level 2.2.0? Or maybe earlier. Let me think. For Connect IQ API 1.4.0, FIT contributor might not be available. Hmm, this could be a real problem.

But the original app was already using FitContributor according to the summary. So either it was known to work or not. Let me check the history... The summary says the original app created FIT fields. So we'll assume it works or is intended.

Actually, wait. Let me search the web to verify Monkey C static method syntax and FitContributor availability for API 1.4.0. But I can also just fix the obvious static method issue.

Actually, the main task from the context is: the assistant was reviewing edits and found the pickerView bug (already fixed in current file), and was sanity-checking whether Monkey C supports calling Settings.load()/save() as class-level methods and whether static class fields work.

The answer: static class fields work, but instance methods cannot be called on class names. They must be static methods.

So I need to change `function load()` to `static function load()` and `function save()` to `static function save()`.

Let me make that fix.

edit

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

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

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

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


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

    ...
  3 class Settings {
  4     static var poolLengthM = 25;
  5     static var poolLengthIndex = 0; // 0=25m, 1=50m, 2=custom
  6
- 7     function load() {
+ 7     static function load() {
  8         var app = Application.getApp();
  9         var storedLength = app.getProperty("poolLengthM");
 10         var storedIndex = app.getProperty("poolLengthIndex");
 11         if (storedLength != null && storedIndex != null) {
    ...
 13             poolLengthIndex = storedIndex;
 14         }
 15     }
 16
-17     function save() {
+17     static function save() {
 18         var app = Application.getApp();
 19         app.setProperty("poolLengthM", poolLengthM);
 20         app.setProperty("poolLengthIndex", poolLengthIndex);
 21     }
    ...

--- /home/liph/code/garmin-freedive-app/source/Settings.mc
+++ /home/liph/code/garmin-freedive-app/source/Settings.mc
@@ -3,9 +3,9 @@
 class Settings {
     static var poolLengthM = 25;
     static var poolLengthIndex = 0; // 0=25m, 1=50m, 2=custom

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

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


Good, fixed the static method issue. Now let me verify the full Settings file and check for any other issues. Let me also run a quick syntax sanity check if possible. Since monkeyc isn't installed, I can't do a real build, but I can review the files for other Monkey C issues.

Let me check a few more things:

1. In `BreathHoldApp.mc`, `Sensor has :setEnabledSensors` - this is valid syntax.
2. In `BreathHoldView.mc`, the `FitContributor` module usage. Actually, I should verify if `FitContributor` constants are correct. In Connect IQ, the field data type constants might be in `FitContributor` as `FitContributor.DATA_TYPE_UINT8`, etc. Let me think about API 1.4...

Actually, the `FitContributor` module was introduced in SDK 2.2 (API level 2.2) maybe. But the manifest says minApiLevel 1.4. This could be a real problem. However, since this was pre-existing code, perhaps the original implementation had this issue too. But it's worth flagging.

Actually, I realize I should check the Garmin SDK documentation. But since I don't have internet access limitations mentioned for web research, let me use web_explore to verify FitContributor availability and the static method syntax.

Actually, the user asked originally to implement the pool-length feature. The static method bug is the main one I just fixed. The FitContributor issue is pre-existing and not part of this turn's changes. But I should still mention it as a risk.

Let me also double check the `PoolLengthView.onUpdate()` - the y positions might overlap, but that's UI polish, not a compile bug.

Let me also verify `BreathHoldDelegate.onBackLongPress()` - the picker view bug is fixed. Good.

One more thing: In `PoolLengthView`, `onUp()` when not in custom edit does `selectedIndex--`. Since PRESET_25=0, PRESET_50=1, PRESET_CUSTOM=2, pressing UP decreases the index, meaning 25→can't go below 0. Pressing DOWN increases 25→50→custom. That's a reasonable mapping.

But when in custom edit, UP increases customValue and DOWN decreases it - that's also fine.

When selectedIndex is custom but !inCustomEdit, pressing START enters custom edit. Then UP/DOWN adjust, START saves. Good.

Now, one potential issue: `PoolLengthView.onUp()` - when `selectedIndex == PRESET_CUSTOM && inCustomEdit`, pressing UP increments. Good.

Another issue: `PRESET_VALUES[selectedIndex]` when selectedIndex is 0 or 1. That's fine. If it's 2 (custom), the else branch handles it.

I think the main fix is done. Let me also run web_explore to verify the Monkey C static method and FitContributor API level, so I can give a complete report.

Question: Garmin Connect IQ Monkey C static function class method call syntax

Question: Garmin Connect IQ FitContributor module API level availability minimum


Reviewed 4 sources · synthesized answer with 5 findings

Reference Guides

Monkey C

- [web_fetch] Community/practical context: Sample apps, libraries and code snippets for use with Garmin's Connect IQ SDK and the Monkey C programming language. NOTE: Although the repo may contain contributions from Garmin e
- [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
- [web_explore] 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

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

Findings
- [web_fetch] Community/practical context: Sample apps, libraries and code snippets for use with Garmin's Connect IQ SDK and the Monkey C programming language. NOTE: Although the repo may contain contributions from Garmin e
- [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
- [web_explore] 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

Sources
- [web_fetch] GitHub - douglasr/connectiq-samples: Connect IQ sample apps, libraries and code snippets: https://github.com/douglasr/connectiq-samples
- [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/

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

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

Reference Guides

Monkey C


Toybox.FitContributor

Toybox

- [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: source https://developer.garmin.com/connect-iq/core-topics/manifest-and-permissions/ sdk 9.1.0 fetched 2026-05-30 generated auto-converted from SDK doc/docs/Core_Topics/Manifest_an
- [web_fetch] Community/practical context: Overview The FitContributor module allows Applications and Data Fields to record Field data into FIT files on the device's file system during an activity. This is useful for record
- [web_fetch] Community/practical context: Module: Toybox Overview Modules Under Namespace Modules: Toybox.Activity, Toybox.ActivityMonitor, Toybox.ActivityPrompts, Toybox.ActivityRecording, Toybox.Ant, Toybox.AntPlus, Toyb
- [web_explore] Community/practical context: An MCP (Model Context Protocol) server that provides comprehensive offline access to the complete Garmin Connect IQ SDK 8.2.3 documentation ecosystem for AI assistants like Claude.

Internal research: web_search ×4, web_fetch ×13, 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: source https://developer.garmin.com/connect-iq/core-topics/manifest-and-permissions/ sdk 9.1.0 fetched 2026-05-30 generated auto-converted from SDK doc/docs/Core_Topics/Manifest_an
- [web_fetch] Community/practical context: Overview The FitContributor module allows Applications and Data Fields to record Field data into FIT files on the device's file system during an activity. This is useful for record
- [web_fetch] Community/practical context: Module: Toybox Overview Modules Under Namespace Modules: Toybox.Activity, Toybox.ActivityMonitor, Toybox.ActivityPrompts, Toybox.ActivityRecording, Toybox.Ant, Toybox.AntPlus, Toyb
- [web_explore] Community/practical context: An MCP (Model Context Protocol) server that provides comprehensive offline access to the complete Garmin Connect IQ SDK 8.2.3 documentation ecosystem for AI assistants like Claude.

Sources
- [web_fetch] GitHub - garmin/connectiq-apps: A collection of Connect IQ apps.: https://github.com/garmin/connectiq-apps
- [web_fetch] garmin-dev-skills/skills/garmin-dev/references/connect-iq-docs/portal/core-topics/manifest-and-permissions.md at main · athitupakan/garmin-dev-skills: https://github.com/athitupakan/garmin-dev-skills/blob/main/skills/garmin-dev/references/connect-iq-docs/portal/core-topics/manifest-and-permissions.md
- [web_fetch] Toybox.FitContributor: https://developer.garmin.com/connect-iq/api-docs/Toybox/FitContributor.html
- [web_fetch] Toybox: https://developer.garmin.com/connect-iq/api-docs/

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

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

Toybox.FitContributor

Toybox


The web_explore results didn't show the actual content. Let me use ollama_web_search instead.

Question: Garmin Connect IQ Monkey C static function class method call

Question: Garmin Connect IQ FitContributor module API level minimum version


1. Monkey C: How to simulate abstract static methods? - Connect IQ App Development Discussion - Connect IQ - Garmin Forums
   URL: https://forums.garmin.com/developer/connect-iq/f/discussion/410404/monkey-c-how-to-simulate-abstract-static-methods
   Monkey C: How to simulate abstract static methods? - Connect IQ App Development Discussion - Connect IQ - Garmin Forums

- State Not Answered
- Replies 12 replies
- Subscribers 18 subscribers
- Views 2099 views
- Users 0 members are here

Options

Related

# Monkey C: How to simulate abstract static methods?

A question for the Monkey C experts out there:

I'm looking for some advice on handling a design pattern issue in Monkey C.

Even though Monkey C doesn’t officially support abstract classes, I like to simulate them by defining "abstract" methods that simply throw an exception. This is especially handy when a method is supposed to return an object, and I don't want to create a dummy return value. With this approach, I can safely have code in the abstract class that calls the method, while ensuring that each subclass implements it properly.

This strategy works fine for instance methods. However, I’m running into problems with static methods.

Here's the situation:

I have a static function`A.doMore()` in an abstract-like class`A`.

Inside`doMore()`, I want to call an abstract static method`A.doSomething()`, which would actually be implemented in each subclass, like`B.doSomething()`.

So when I call`B.doMore()`, it should internally call`B.doSomething()`- not`A.doSomething()`.

But when I try this:

Simply calling`doSomething()` inside`A.doMore()` just calls`A.doSomething()`.

Using`self.doSomething()` doesn't solve it either.

And I can’t hard-code`B.doSomething()` in`A`, because I want it to work generically across multiple subclasses.

Is this just a limitation of Monkey C’s type system and static method handling, or is there some workaround I'm missing?

Thanks for any insight you can share!

Below is some sample code illustrating the issue:

```
class A {    public static function doSomething() as Void {        throw new AbstractMethodException( "A.doSomething" );    }     public static function doMore() as Void { // !! This is the issue !!        doSomething();    }}class B extends A {    public static function doSomething() as Void {        // this is what should get done    } }class C {    public function doItAll() as Void {        B.doMore();    }}
```

- Sign in to reply
- Cancel

Is this just a limitation of Monkey C’s type system and static method handling, or is there some workaround I'm missing?

This is how C++, Java, and C# work (where static methods are resolved *statically* - at compile time, not run time), so I'd say it's by design. Whether it's considered a "limitation" might be in the eye of the beholder.

(I realize that what you're asking for is possible in js and python tho.)

Simply calling`doSomething()` inside`A.doMore()` just calls`A.doSomething()`.

Using`self.doSomething()` doesn't solve it either.

For me, if I write code similar to your example and call doSomething() or self.doSomething() in A.doMore(), I get a symbol not found error at run time (although the type checker is happy, even at the strictest level).

Not sure why we're seeing different behaviour, unless your actual implementation of class C also has a function doSomething(), and it's actually C.doSomething() which is being called.

I would consider the type checker thing a bug tho.

As for the implementation details/reasons behind this, if create an instance of C and call C.doItall(), and you print out the value of `self` (or `this`) in C.doItAll() and A.doMore(), you'll find that it has the same value in both cases (meaning it points to the instance of C in both cases).

If you call A.doMore() from outside of a class instance (like a global function), you will find that trying to print the value of self causes a crash, with a "symbol not found" error.

In other words, `self` only has meaning in the context of a non-static class method, so there's no way for A.doMore() to know that it was invoked as B.doMore().

As for workarounds, you could change your static functions so that they take a "classDef" as an arg (this goes back to the other discussion about whether it's possible to do this "correctly" in the type system, regardless of the fact the Method.initialize() does it.

Whenever you call a static function, you would pass the class itself as the first arg:

```
import Toybox.Lang;
import Toybox.System;

class A {
    public static function doSomething() as Void {
        System.println("A");
    }
    public static function doMore(staticClass as A) as Void {
        // yes it's bad to type "staticClass" as A, because if
        // A or B is passed in, as intended, the type checker would
        // allow `staticClass.someInstanceMethodOrVariable` to be accessed,
        // which would cause a crash at runtime
        //
        // don't blame me, blame the type checker / language design
        staticClass.doSomething();
    }
    private static function getClass() as A {
        return A;
    }
}

class B extends A {
    function initialize() {
        A.initialize();
    }
    public static function doSomething() as Void {
        System.println("B");
    }
}

class C {
    public function doItAll() as Void {
        B.doMore(B);
    }
}

function test() as Void {
    new C().doItAll(); // prints "B"
}
```

Specifically, the problem is the type system seemingly doesn't really distinguish between the name of a class (or "classdef") and an instance of said class, for the purpose of variable / arg types.

Not sure why we're seeing different behaviour, unless your actual implementation of class C also has a function doSomething(), and it's actually C.doSomething() which is being called.

As for the implementation details/reasons behind this, if create an instance of C and call C.doItall(), and you print out the value of `self` (or `this`) in C.doItAll() and A.doMore(), you'll find that it has the same value in both cases (meaning it points to the instance of C in both cases).

To illustrate what I'm talking about:

```
import Toybox.Lang;
import Toybox.System;

class A {
    public static function doSomething() as Void {
        System.println("A");
    }
    public static function doMore() as Void {
        // when called from instance of C, calls C.doSomething().
        // when called from global scope or some instance which doesn't
        // have doSomething, crashes with "symbol not found error"
        doSomething(); // [*] line 12
    }
    private static function getClass() as A {
        return A;
    }
}

class B extends A {
    function initialize() {
        A.initialize();
    }
    public static function doSomething() as Void {
        System.println("B");
    }
}

class C {
    public function doItAll() as Void {
        B.doMore();
    }
    public function doSomething() as Void {
        System.println("C");
    }
}

function test() as Void {
    new C().doItAll(); // prints "C" :/
    B.doMore(); // crash at line 12 [*]: "symbol not found error"
}
```

I don't claim that it's good design that a static function in A can somehow call a function in an instance of C, when an instance of C called A's static function in the first place - even the typechecker agrees with me, as it thinks A.doSomething() is actually being called.

I opened a couple of bug reports for this:

Type checker allows static method to be called without qualifier from other static method in same class, but this fails at runtime with a "symbol not found error"

if instance method of one class calls static method of another class, the 2nd class can inadvertently call instance methods on the 1st class

Thanks for diving into this issue again!

I modified by code for the to take the classdef, but when I call the static method from the classdef (line 15 in your example), I get an "Error: Not Enough Arguments Error". I am running it with SDK 8.1.1.

In my admittedly not-so-great example, only doMore() takes the classdef as an arg, not doSomething(). The example as written should compile and run tho.

If you changed *all* static functions to take the classdef (which would be more consistent), then you would call doSomething() as follows:

staticClass.doSomething(staticClass);

Not sure if this workaround is a good idea tho.

Why do you need methods that are both static and abstract? As mentioned, this is not possible even in java. If you already have an object (i.e you have at least 1 non static method in the class) then maybe it's not that big of an overhead to turn the static method into non-static. Wouldn't that solve all the problems?

In my admittedly not-so-great example, only doMore() takes the classdef as an arg, not doSomething(). The example as written should compile and run tho.

OK, I need to do some more testing to understand why this isn't working for me.

Why do you need methods that are both static and abstract? As mentioned, this is not possible even in java. If you already have an object (i.e you have at least 1 non static method in the class) then maybe it's not that big of an overhead to turn the static method into non-static. Wouldn't that solve all the problems?

I have static logic in an abstract class that is shared across all derived classes. However, it also needs to incorporate static behavior that differs depending on which derived class is calling the abstract class's static logic.

behavior that differs depending on which derived class is calling

If it depends on the caller then you'll anyway need to send the caller. Or just use the somewhat strange behaviour of Monkey C that if I understood from this thread and the 2 bug reports kind of does exactly that.

But I'll ask this again:

1. what is the reason to use static methods?

2. am I right in my assumption that if you decided to not use static methods then you wouldn't have these problems? Or maybe I am still missing something.

If it depends on the caller then you'll anyway need to send the caller. Or just use the somewhat strange behaviour of Monkey C that if I understood from this thread and the 2 bug reports kind of does exactly that.

No, the bug/quirk in Monkey C is that if an instance method of a class X calls a static method of class Y (which is unrelated to X), then the "self" reference will point to X, meaning that any unqualified function calls in Y's static method can actually resolve to an instance method in X. This is not what the-ninth wants (or what any reasonable dev could possibly want).

What the-ninth wants is:

- given: class B that extends A

- given: that a static method on B calls a static method on A, and A's static method calls another static method (call it M) which is defined on both A and B (in this case, it's called without using a qualifier, only using the name of the function - i.e. "M()")

- then: M is resolved on B, not A

This won't work for several reasons:

1) In general, M can't be called without a qualifier. e.g. "M()" won't work, only "A.M()" or "B.M()". So a plain call to "M()" wouldn't be resolved to *either* A.M() or B.M().

2) In the quirky case that "M()" could "work", it wouldn't be calling a *static* method on A or B, it would be calling an *instance* method on some other unrelated class

The reason for 1) is that calling "M()" without a qualifier would only work if the self reference is set to either A or B. Even though the type checker believes that calling "M()" from another static method in A would cause it to be resolved to "A.M()", this not the case at runtime. (Surely there's room for improvement here for the compiler to resolve such a call at compile-time to "A.M()", but it still wouldn't achieve what the-ninth wants)

The reason for 2) is the quirk which incorrectly retains the self pointer when an instance method of an class calls a static method of another unrelated class.

EDIT: the one edge case that might fulfill the desired use case:

- an *instance method* of B calls a static method in A, which calls a unqualified function which exists as a static method on both A and B

While this would work, it requires an instance of B which defeats the whole purpose of having static methods in the first place

2. if instance method of one class calls static method of another class, the 2nd class can inadvertently call instance methods on the 1st class - Connect IQ Bug Reports - Connect IQ - Garmin Forums
   URL: https://forums.garmin.com/developer/connect-iq/i/bug-reports/if-instance-function-of-one-class-calls-a-static-function-of-another-class-the-2nd-class-can-inadvertently-call-instance-methods-of-the-1st-class
   if instance method of one class calls static method of another class, the 2nd class can inadvertently call instance methods on the 1st class - Connect IQ Bug Reports - Connect IQ - Garmin Forums

Acknowledged

- View Status History
- More
- Cancel

CIQQA-3186

# if instance method of one class calls static method of another class, the 2nd class can inadvertently call instance methods on the 1st class

In this example, an instance of C calls a static function on A called doMore(). A.doMore() calls doSomething() which is a static function on A and an instance function on C. At runtime, C.doSomething() as actually called, which is contrary to both the user's and the type checker's expectations. (The type checker thinks A.doSomething() is called, based on the hover info).

Furthermore, if A.doMore() is called outside of the context of C (or outside of the context of any object that has a doSomething method()), then it will crash when it tries to call doSomething().

```
import Toybox.Lang;
import Toybox.System;

class A {
    public static function doSomething() as Void {
        System.println("A");
    }
    public static function doMore() as Void {
        // when called from instance of C, calls C.doSomething().
        // when called from global scope or some instance which doesn't
        // have doSomething, crashes with "symbol not found error"
        doSomething(); // [*] line 12
    }
    private static function getClass() as A {
        return A;
    }
}

class B extends A { // B is unnecessary for this example, it's just here to match a code sample in a related bug report
    function initialize() {
        A.initialize();
    }
    public static function doSomething() as Void {
        System.println("B");
    }
}

class C {
    public function doItAll() as Void {
        B.doMore();
    }
    public function doSomething() as Void {
        System.println("C");
    }
}

function test() as Void {
    new C().doItAll(); // prints "C" :/
    B.doMore(); // crash at line 12 [*]: "symbol not found error"
}
```

Same problem if `doSomething()` is replaced by `self.doSomething()`.

I think the problem is that when C.doItAll() is called, `self` is set to the instance of C [as it should be], but when A.doSomething() [static function on A] is called, `self` is STILL set to the instance of C, instead of being "non-existent" [*] (as it would be if a global function was called instead).

[*] "non-existent" in the sense that trying to access it would be a "symbol not found error".

Indeed:

- if you print the value of `self` in C.doItAll() and A.doMore(), they will print the same value [in the case that A.doSomething() was invoked from C.doItAll()]. If B.doMore() / A.doMore() is invoked from a global context, the access of self in A.doMore() will cause a symbol lookup error

- this bug does not occur if A.doMore() is replaced with a global function that just calls `doSomething()`. In this case, a symbol not found error occurs, and that is expected behaviour

Expected behaviour:

- The type checker recognizes that doSomething() on line 12 is *not* of type A.doSomething()

- doSomething() on line 12 always crashes with a symbol not found error, regardless of the context from which A.doMore() is called

Related: https://forums.garmin.com/developer/connect-iq/i/bug-reports/type-checker-allow-static-method-to-be-called-without-qualification-from-other-static-method-in-some-class-but-this-fails-at-runtime-with-a-symbol-not-found-error

^ sorry this is a plain text url instead of a clickable link, but the forum won't let me post it as a link

- Apr 29, 2025
- View Status History
- More
- Cancel

Sorry, there's a bunch of typos / unclear writing in the OP but I can't edit it anymore. Hopefully the code speaks for itself

I thought the point was to make things as hard as possible for users of the forum, to make sure that everyone knows how bad the bugs are????? You should've posted that link as a screenshot of base64-encoded text

Hover info shows that the type checker thinks A.doSomething() is being called on line 12

3. Sdk 4.1.6 can't lookup static methods from within the class - Connect IQ Bug Reports - Connect IQ - Garmin Forums
   URL: https://forums.garmin.com/developer/connect-iq/i/bug-reports/sdk-4-1-6-can-t-lookup-static-methods-from-within-the-class
   Sdk 4.1.6 can't lookup static methods from within the class - Connect IQ Bug Reports - Connect IQ - Garmin Forums

Acknowledged

- View Status History
- More
- Cancel

# Sdk 4.1.6 can't lookup static methods from within the class

Given:

```
module Statics {
    var ok as Boolean = false;
    class C {
        function initialize() {
            C.foo();
        }
        static function bar() as Void {
            C.foo();
        }

        private static function foo() as Void {
            ok = true;
        }
    }

    (:test)
    function static_from_initialize(logger as Logger) as Boolean {
        ok = false;
        var c = new C();
        return ok;
    }
    (:test)
    function static_from_static(logger as Logger) as Boolean {
        ok = false;
        C.bar();
        return ok;
    }
}

```

The code compiles and works as expected with sdk-4.1.5 and earlier (ie C.foo() is successfully called, both from C.initialize() and from C.bar()),

With sdk-4.1.6 the type checker is happy (it compiles with Strict type checking), but fails at runtime with:

```
Executing test Statics.static_from_initialize...

Error: Symbol Not Found Error
Details: Could not find symbol 'foo'
Stack:
  - initialize() at /Users/mwilliams/www/git/monkeyc-optimizer/test/OptimizerTests/source/OptimizerTestsLookup.mc:288 0x10003151
  - static_from_initialize() at /Users/mwilliams/www/git/monkeyc-optimizer/test/OptimizerTests/source/OptimizerTestsLookup.mc:302 0x10003181
  - evaluate_test_entries_30_to_39() at UnitTests.mc:182 0x10001bc6
  - runTest() at UnitTests.mc:215 0x10001d92
ERROR
------------------------------------------------------------------------------
Executing test Statics.static_from_static...

Error: Symbol Not Found Error
Details: Could not find symbol 'foo'
Stack:
  - bar() at /Users/mwilliams/www/git/monkeyc-optimizer/test/OptimizerTests/source/OptimizerTestsLookup.mc:291 0x1000312f
  - static_from_static() at /Users/mwilliams/www/git/monkeyc-optimizer/test/OptimizerTests/source/OptimizerTestsLookup.mc:308 0x100031b2
  - evaluate_test_entries_30_to_39() at UnitTests.mc:185 0x10001bed
  - runTest() at UnitTests.mc:215 0x10001d92
ERROR

```

I caught this building the Meditate folder in the open source project https://github.com/vtrifonov-esfiddle/Meditate. The built project runs correctly with 4.1.5 and earlier, but fails when you hit the start button with 4.1.6.

It also looks like the 4.2.0-beta sdk has the same issue - and in fact it seems to be another compiler2 vs compiler1 issue, because compiling with 4.1.6 or 4.2.0-beta at -O0 fixes the problem.

EDIT:

The bug only affects private and protected references. And if I look at the output with -g, I see this with from sdk-4.1.5:

```
        lgetv 0
        spush C
        getv
        spush <globals/Statics/C/<>foo>
        getv
        frpush
        invokem 1
        popv

```

But this from sdk-4.1.6

```
    lgetv 0
    spush C
    getv
    spush foo
    getv
    frpush
    invokem 1
    popv
```

So 4.1.5 is correctly pushing the "private" symbol for foo, while 4.1.6 is incorrectly pushing the public symbol.

Also, if I make foo protected, instead of private, and create a class D which extends C, and have that try to call C.foo, it fails the same way with 4.1.6 AND 4.1.5 (but again, the type checker is happy with both compiler versions).

- Nov 10, 2022
- View Status History
- More
- Cancel

Seems to be fixed in 4.1.7

Thanks.

Just FYI, the ping was 2 days ago, after it had sat without comment or a status update for 2 days. And it was only acknowledged earlier today...

I've contacted the relevant party and we agree that this appears to be a bug.Just FYI, when the status line above your post says "Acknowledged", this means we've created an internal ticket to reproduce the issue and will file a report to fix it. No need to ping me directly.

Ping

Travis.ConnectIQ This seems like a pretty serious change in behavior, especially as it's not caught by the type checker. I'm seeing more and more open source projects that are broken by this.

4. Monkey C
   URL: https://developer.garmin.com/connect-iq/monkey-c/functions/
   Monkey C

MenuMonkey C

Monkey C
[Garmin Developers](https://developer.garmin.com/)
#
* [Overview](https://developer.garmin.com/connect-iq/overview/)
*
* [Compatible Devices](https://developer.garmin.com/connect-iq/compatible-devices/)
*
* [API Docs](https://developer.garmin.com/connect-iq/api-docs/)
*
* [Get the SDK](https://developer.garmin.com/connect-iq/sdk/)
*
* [Submit an App](https://developer.garmin.com/connect-iq/submit-an-app/)
*
* [Stay Informed](https://developer.garmin.com/connect-iq/stay-informed/)
*
* [Connect IQ Basics](https://developer.garmin.com/connect-iq/connect-iq-basics/)
*
* [Monkey C](https://developer.garmin.com/connect-iq/monkey-c/)
*
* [Functions](https://developer.garmin.com/connect-iq/monkey-c/functions/)
* [Objects and Memory](https://developer.garmin.com/connect-iq/monkey-c/objects-and-memory/)
* [Containers](https://developer.garmin.com/connect-iq/monkey-c/containers/)
* [Monkey Types](https://developer.garmin.com/connect-iq/monkey-c/monkey-types/)
* [Exceptions and Errors](https://developer.garmin.com/connect-iq/monkey-c/exceptions-and-errors/)
* [Annotations](https://developer.garmin.com/connect-iq/monkey-c/annotations/)
* [Coding Conventions](https://developer.garmin.com/connect-iq/monkey-c/coding-conventions/)
* [Compiler Options](https://developer.garmin.com/connect-iq/monkey-c/compiler-options/)
* [Core Topics](https://developer.garmin.com/connect-iq/core-topics/)
*
* [User Experience Guidelines](https://developer.garmin.com/connect-iq/user-experience-guidelines/)
*
* [Personality Library](https://developer.garmin.com/connect-iq/personality-library/)
*
* [Connect IQ FAQ](https://developer.garmin.com/connect-iq/connect-iq-faq/)
*
* [Reference Guides](https://developer.garmin.com/connect-iq/reference-guides/)
*
* [App Review Guidelines](https://developer.garmin.com/connect-iq/app-review-guidelines/)
*
* [Monetization](https://developer.garmin.com/connect-iq/monetization/)
*
* [Device Reference](https://developer.garmin.com/connect-iq/device-reference/)
*
MenuMonkey C

5. COMPILER BUG: cannot compile classes with static functions - Connect IQ Bug Reports - Connect IQ - Garmin Forums
   URL: https://forums.garmin.com/developer/connect-iq/i/bug-reports/compiler-bug-cannot-compile-classes-with-static-functions
   COMPILER BUG: cannot compile classes with static functions - Connect IQ Bug Reports - Connect IQ - Garmin Forums

Acknowledged

- View Status History
- More
- Cancel

# COMPILER BUG: cannot compile classes with static functions

There are several issues with the available compilers (SDK 4.1.7 and SDK 4.2.0 Beta 1) if a class contains a static function and the parameters of the function are Monkey Typed with the compiler set to "Gradual" or higher.

1. Cannot find symbol errors for functions on core classes (eg. Position.Location); and
2. Error parsing the global namespace ($.) when specifying the Monkey Type for a function parameter.

I tried the broken, replicateable code on SDK 4.1.5, SDK 4.1.6, SDK 4.1.7 and SDK 4.2.0 Beta 1.

Error #1 occurs only on SDK 4.1.7 (ie. it works fine on SDK 4.1.5, SDK 4.1.6, SDK 4.2.0 Beta 1).

Error #2 occurs on all the SDKs listed above -- SDK 4.1.5, SDK 4.1.6, SDK 4.1.7 and SDK 4.2.0 Beta 1.

These errors may or may not be related to this issue: https://forums.garmin.com/developer/connect-iq/i/bug-reports/bug-report-static-lang-dictionary-class-variable-broken-in-sdk-4-1-6

I am not able to attach my test project that will easily replicate both issues/bugs but I have emailed the ZIP to the Connect IQ team. If anyone would like the project file to validate the issue or use it as the basis for a similar sounding bug or just out of curiosity, then DM me. For reference, the function within the class looks like this:

```
    static function compilerErrorOne(location as Position.Location, waypoint) as BrokenClass {
        var decimalDeg = location.toDegrees();
        var wpDecDeg = waypoint.getLocation().toDegrees();
        var brokenClass = new BrokenClass(null);
        return (brokenClass);
    }

    static function compilerErrorTwo(location, waypoint as $.Waypoint) as BrokenClass {
        var decimalDeg = location.toDegrees();
        var wpDecDeg = waypoint.getLocation().toDegrees();
        var brokenClass = new BrokenClass(null);
        return (brokenClass);
    }
```

- Dec 6, 2022
- View Status History
- More
- Cancel

Do you have an idea on how to avoid the compilation warning ?

I think your issue is related to mine: forums.garmin.com/.../6-2-0-cannot-call-instance-methods-on-objects-from-static-functions-defined-on-the-same-class

Really ? I still have an issue:

```
module test {
    class Test {
    private var _a as Number;

    public function initialize(a as Number) {
    _a = a;
    }

    public function a() as Number { return _a; }

    public static function dummy(first as Test, second as Test) as Test {
    var temp = new Test(first.a() * second.a();
    return temp;
    }
    }
    }
```

This does not compile because the compiler does not find the symbol ":a" on txpe $.test.Test

Do you have any idea ?

Yes, it has been fixed. It's also metioned in the readme in the SDK:

Compiler Changes

- Fix symbol not found error when using types on static class functions.

My test cases above will now compile without error or warning when using SDK 6.2.0.

Monkey C


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

# Module: Toybox.FitContributor

## Overview

The FitContributor module allows Applications and Data Fields to record Field data into FIT files on the device's file system during an activity. This is useful for recording data that is not already calculated by the device, which can be synced to a service like Garmin Connect.

There are three FitContributor message types available:

MESG_TYPE_SESSION

Session data is written once per recording session at the end of the recording, and is used for data that pertains to the entire session (e.g. average speed).

MESG_TYPE_LAP

Lap data is written once for every lap in the session, and used for data that pertains to each lap (e.g. average lap speed).

MESG_TYPE_RECORD

Depending on the device, record data is written once per second or when new data is available (Smart Recording), but is never written faster than once per second. This message type is used for instantaneous values (e.g. current speed).

Data type constants are also available for use with the createField() method.

## See Also:

Learn more about the FIT format

## Classes Under Namespace

## Constant Summary

### MessageType

API Level 1.3.0

The message type for session messages.

API Level 1.3.0

The message type for lap messages.

API Level 1.3.0

The message type for record messages.

| Name | Value | Since | Description |
| --- | --- | --- | --- |
| MESG_TYPE_SESSION | 18 |
| MESG_TYPE_LAP | 19 |
| MESG_TYPE_RECORD | 20 |

### DataType

API Level 1.3.0

API Level 1.3.0

API Level 1.3.0

API Level 1.3.0

API Level 1.3.0

API Level 1.3.0

API Level 1.3.0

API Level 1.3.0

API Level 1.3.0

| Name | Value | Since | Description |
| --- | --- | --- | --- |
| DATA_TYPE_SINT8 | 1 |
| DATA_TYPE_UINT8 | 2 |
| DATA_TYPE_SINT16 | 3 |
| DATA_TYPE_UINT16 | 4 |
| DATA_TYPE_SINT32 | 5 |
| DATA_TYPE_UINT32 | 6 |
| DATA_TYPE_STRING | 7 |
| DATA_TYPE_FLOAT | 8 |
| DATA_TYPE_DOUBLE | 9 |

---

Generated Jun 8, 2026, 3:42:16 PM

2. Toybox.FitContributor.Field
   URL: https://developer.garmin.com/connect-iq/api-docs/Toybox/FitContributor/Field.html
   Toybox.FitContributor.Field

# Class: Toybox.FitContributor.Field

Inherits: Toybox.Lang.Object

Toybox.Lang.Object

Toybox.FitContributor.Field

show all

## Overview

A Field records custom FIT data from an Application or Data Field to a FIT file on the device's file system.

Once a Field is created with the createField() method, you can submit the next Field value with setData(), which will get written to the FIT file at the next opportunity. Depending on the device, writes to the FIT file may occur once per second or when new data is available (Smart Recording). Best practice is to only call setData() when values have changed to accommodate Smart Recording.

If setData() is called before the previous data is written out, the previous value will be lost and replaced by the current data. For this reason, we do not recommend using this feature for time-sensitive data requiring sub-second granularity.

## See Also:

Session.createField()

Smart Recording vs. Every Second Recording

Learn more about the FIT format

## Instance Method Summary collapse

setData(input as Lang.Object) as Void

Set the value to write to this Field.

## Instance Method Details

### setData(input as Lang.Object) as Void

https://developer.garmin.com/connect-iq/api-docs/Toybox/Lang/Object.html

Set the value to write to this Field.

---

Generated Jun 8, 2026, 3:42:16 PM

3. Documentation and example on FitContributor - Connect IQ App Development Discussion - Connect IQ - Garmin Forums
   URL: https://forums.garmin.com/developer/connect-iq/f/discussion/3533/documentation-and-example-on-fitcontributor
   Documentation and example on FitContributor - Connect IQ App Development Discussion - Connect IQ - Garmin Forums

- Replies 23 replies
- Subscribers 15 subscribers
- Views 7459 views
- Users 0 members are here

Options

Related

# Documentation and example on FitContributor

I just today switched to SDK 2.1 and I would like to start playing with the fit files through the FitContributor method. Where can I find some clear documentation or working example to start? Thanks in advance.

- Sign in to reply
- Cancel

You can start by looking at Toybox::FitContributor and Toybox::ActivityRecording::Session in the API docs, and we also have some information published in the Programmer's Guide. There is actually a sample app we put together, but it's not being distributed with the SDK right now (I'd like to include something like it in a future release). I'd be happy to forward a sample to you for you to study--just contact me directly at [email] [email protected] [/email].

Thank you very much for your interest Brandon...ok so far so good,I did the followings steps and it looks like it's working because the compiler ends the job...I still have no check if the field I added is really in the fit file but I'll do soon.1 - session = Record.createSession({:name=>"XXXXXX", :sport=>Record.SPORT_TRAINING , :subSport=>Record.SUB_SPORT_CARDIO_TRAINING}); first create a new session (ok of course)2 - field = session.createField("field", 0, FitContributor.DATA_TYPE_UINT16, {:mesgType => FitContributor.MESG_TYPE_SESSION}); from the documentation I understand that with the MESG_TYPE_SESSION option the field will be added once at the end of the session,and it's what I need3 - field.setData(myVar);here is where I set the var that will be saved in the field on the fit file4 - session.save(); at lastnow from the point 2 I assume that I must call point 3 just before to save the session on orders to have the last value of myVar saved in the fit file...am I correct or I'm missing something?thank you on advance,

Hi Brandon. I will start to take a look at the new features and I would appreciate if you send the Fit Contributor sample to me. Just sent a mail to you. Cheers, Robinson

The MoxyDataField example shows what you need to do.

I've been able to verify that my data is making it to the FIT file using FIT SDK, but I haven't been able to get the monkeygraph tool working and I don't see any mention of my data on connect.garmin.com when I upload my workouts. Is connect.garmin.com currently processing user data from fit developer fields? Rob

> I've been able to verify that my data is making it to the FIT file using FIT SDK, but I haven't been able to get the monkeygraph tool working and I don't see any mention of my data on connect.garmin.com when I upload my workouts. Is connect.garmin.com currently processing user data from fit developer fields?Rob

Do you have the metadata for the FitContributor Data Entered in to the Resources file like in the sample in the SDK below?This is why you need to load a .iq file into MonkeyGraph and this is why Garmin Connect only shows Fit Contributor Data from Apps that have been loaded in the App store. They both need this metadata.` `

> Do you have the metadata for the FitContributor Data Entered in to the Resources file like in the sample in the SDK below?

Yeah I do:` ` Also, I can see the fields show up in the Connect mobile app under "Connect IQ" of any given workout. Maybe I'm just don't know where to look on the Connect website. I'll keep poking around. Also, as far as I can tell the monkeygraph tool only works with .iq files not .prg and it's only ever crashed on me. YMMV.UPDATE: I'm now seeing my Fit developer fields on Garmin Connect under the activity details. I must have missed it before, sorry for wasting time here. Either way it's working now.Rob

Dynamic chart unit label for FIT Developer Fields I've got my FIT Developer fields working and showing up in the mobile apps and web site, but I'm now wondering how to create dynamic chart unit labels based off of user modified properties. I've tried a few things: 1. In the block, reference the @Properties.ID of the property that I want to use:`

` I get the following error: BUILD: ERROR: Fit contribution id=0's 'unitLabel' references non existent string ID '@Properties.ItemName' BUILD: WARNING: String reference '@Properties.ItemName' should use the format '@Strings. ' (@ [email protected]) 2. Reference a string resource that then references the property I'd like to use:`
 @Properties.ItemName

 Pizza

` This compiles and even saves things to the FIT file as expected. Using the FitSDK, I can examine all aspects of the FIT file and see that in the Session data and all other data, "Pizza" is now the unit label for my developer field. When I upload the data to Connect.Garmin.com I see "@Properties...." listed as my unit label. @Properties.ItemName is not saved any where in the FIT file, so the Garmin website must be using my resources.xml file to generate this label. I guess the site is not fully parsing the resource file correctly. Also, I've specified the unit label when creating the data field:`itemField = SimpleDataField.createField(App.getApp().getProperty(Ui.loadResource(Rez.Strings.ItemName)), ITEM_FIELD_ID, FitContributor.DATA_TYPE_FLOAT, { :mesgType=>FitContributor.MESG_TYPE_SESSION, :units=> labelString });` Any ideas on how I might make this work?

When you have FitContrib data, the definition you create is included as a .json file in the .iq file. When Garmin Connect or GCM gets a .fit from your app, it gets the data in the .json from the app store, as it's not in the .fit. So, what's displayed is based on what you submitted in the .iq, and and can't be changed in the app. While you called it "@property..." That's now the string is was in the .iq sent to the store, and will be used as the label. (this is why monkeygraph needs a .iq and not a .prg.) You can't dynamically change a label.

I had concluded (I'm not positive I'm correct) that having dynamic chart unit labels, data labels, or chart titles in Garmin Connect can't be done as it is. My understanding is that the information in the resources.xml file is not automatically written in each .FIT file. It only gets communicated to GC when you upload the App to the App Store. It would be a nice feature to have an option in the resources file to direct Garmin Connect to use data from the .FIT file for these parameters instead of using a fixed string. - For the dataLabel or ChartTitle, have the option to use the createField(name) string from the FIT file - For the unitsLabel, have the option to use the createField(:units) option string from the FIT file For Moxy, I write the Sensor Number and the Sensor Location to the createField(name) string. I don't see anyway to display this with the Moxy data graph in GC. We're planning to support multiple sensors at once so it would be helpful to see this string because it will show which sensor was on the left or right leg for example.

4. Connect IQ Fields not showing in the app - Connect IQ App Development Discussion - Connect IQ - Garmin Forums
   URL: https://forums.garmin.com/developer/connect-iq/f/discussion/367311/connect-iq-fields-not-showing-in-the-app/1759812
   Connect IQ Fields not showing in the app - Connect IQ App Development Discussion - Connect IQ - Garmin Forums

- Replies 18 replies
- Subscribers 20 subscribers
- Views 4742 views
- Users 0 members are here

Options

Related

# Connect IQ Fields not showing in the app

I've got an app in the appstore Squasht It!, and at some point my Connect IQ fields stopped working. As in they won't show up anymore in the phone app.

And it seems to only be broken on the newer versions of the api. I've got a Venu 2 with api level 4.2.4 and I've got no problems but a friend of mine has a venu 3 and he has api level 5.0.0 and for him they're not registering anymore.

And for the life of me I can't figure out how to fix this. I've updated to the latest SDK and it does work when I create the fit file from the emultator.

Hope somebody can help me out. Since more people are starting to ask me what's wrong.

- Sign in to reply
- Cancel

## Top Replies

bug-reports/connect-iq-fields-are-missing-on-lap-view-on-api-level-5. Sadly I couldn't post any pictures in there so I added a link to this thread. Hopefully this works.

…

## All Replies

Ok I did go up and down on this one, I'm sorry it's just giving me sleepless nights trying to figure this out.

here's the recap.

I have an app in the store which is saving data to the session and laps through fit fields. This was working fine and then all the sudden it was not on newer api versions. Unfortunately it still works with my device but devices running on api lvl 5.0.0 are missing these fields. with no change to the fitcontributions and both devices running the same app version. So the 2 images I showed are both with the same fit fields only mine with api lvl 4.2.4 is showing the correct data. And the other one is api lvl 5.0 which is not showing the correct data.

I tried fixing this by updating the sdk version to 6.2.0 but this also didn't seem to work then I came here for help.

One feedback from a user was (hopefully this helps):

```
Hi I am using version 3 .0 of Squash It! and I have been a happy user. Unfourtnlally then the result from the sets are not carried over to the app anymore. teh same happened in version 2.0.7. I think that it is after Garmin changed from FW 15.77 to FW 16.22. Hope you can fix it?
```

The forum is full of reports similar to this, that about a month ago when Garmin started to publish beta versions suddenly existing apps started to behave strangely.

I am still a bit confused because you consistently write that data is not "showing" correctly. That's why I went to check my DF's output in Garmin Connect app (and as you see I did find a bug, but that looks like another one, not the one your users have) But the 2 pictures you included are not from Garmin Connect. And also I don't see something not "showing" on those pictures, just strange values for the steps.

How were the pictures created, and how did you get the fit files? From the watch? From Garmin Connect web?

Also maybe I am also tired :) but can you draw a red circle around whatever is "not there"?

Alrighty then. This morning I played with my colleague same app version different api level versions.

These are my results (4.2.4) in the connect app all the fields are there. The landscape part of the image are the laps.

As you can see there's Connect Iq showing

This is what the Connect App of my colleague (api level 5.0.0) shows. In the red box should be the missing fields

And this is the bit of code that's creating the fields

```
session_field_game_score_player_1 = session.createField("game_score_you", FIT_GAME_SCORE_PLAYER_1_ID, Contributor.DATA_TYPE_SINT8, {:mesgType => Contributor.MESG_TYPE_LAP, :units => Ui.loadResource(Rez.Strings.fit_score_unit_label)});
session_field_game_score_player_2 = session.createField("game_score_opponent", FIT_GAME_SCORE_PLAYER_2_ID, Contributor.DATA_TYPE_SINT8, {:mesgType => Contributor.MESG_TYPE_LAP, :units => Ui.loadResource(Rez.Strings.fit_score_unit_label)});
session_field_game_time = session.createField("game_time", FIT_GAME_TIME_ID, Contributor.DATA_TYPE_DOUBLE, {:mesgType => Contributor.MESG_TYPE_LAP, :units => Ui.loadResource(Rez.Strings.fit_game_time_unit)});

session_field_stats_steps = session.createField("steps", FIT_STATS_STEPS_FIELD_ID, Contributor.DATA_TYPE_SINT32, {:mesgType => Contributor.MESG_TYPE_SESSION, :units => Ui.loadResource(Rez.Strings.fit_steps_unit)});
session_field_match_score_player_1 = session.createField("match_score_you", FIT_MATCH_SCORE_PLAYER_1_ID, Contributor.DATA_TYPE_SINT8, {:mesgType => Contributor.MESG_TYPE_SESSION, :units => Ui.loadResource(Rez.Strings.fit_score_unit_label)});
session_field_match_score_player_2 = session.createField("match_score_opponent", FIT_MATCH_SCORE_PLAYER_2_ID, Contributor.DATA_TYPE_SINT8, {:mesgType => Contributor.MESG_TYPE_SESSION, :units => Ui.loadResource(Rez.Strings.fit_score_unit_label)});
```

Hopefully this information is slightly more understandable. If any more information is needed please let me know.

Now at least I can see the problem and I think Garmin will also understand. Open a bug report with this so they can fix it in the firmware.

The next question is if anyone here can come up with a workaround that you could do...

Ok I've created a bug bug-reports/connect-iq-fields-are-missing-on-lap-view-on-api-level-5. Sadly I couldn't post any pictures in there so I added a link to this thread. Hopefully this works.

And yes a workaround would be very nice,

That's a known bug (probably misconfiguration) of the forum. You can post pictures in the comments

Hello, I got an app called Padel AI and one of my users reprted to me very similiar behavior. On his Fenix7x custom fields in Stats are not showing up while on my Fenix7 are still being reported. On both watches Charts are working fine though.

I think some earlier commented about 2 separate failure modes. I have one example where the same watch failed separate times, in each of the modes. 1) no customer fields 2) custom fields present but they have no data in them.

When i get time i will see if i can reproduce any of this in the simulator as i do not have the effected models.

5. mistakes in the documentation of the FitContributor - Connect IQ Bug Reports - Connect IQ - Garmin Forums
   URL: https://forums.garmin.com/developer/connect-iq/i/bug-reports/mistakes-in-the-documentation-of-the-fitcontributor
   mistakes in the documentation of the FitContributor - Connect IQ Bug Reports - Connect IQ - Garmin Forums

Acknowledged

- View Status History
- More
- Cancel

# mistakes in the documentation of the FitContributor

I was struggling to use the FirContributor for one of my apps and I believe the are some mistakes in this documentation: https://developer.garmin.com/connect-iq/api-docs/Toybox/FitContributor.html

```
createField needs to be called on a session created by session = ActivityRecording.createSession
```

Fit.MESG_TYPE_RECORD does not work because there is no `Fit` in the import/use statements. It has to be FitContributor.MESG_TYPE_RECORD or using Toybox.FitContributor as Fit;

1. https://developer.garmin.com/connect-iq/core-topics/activity-recording/#fitdeveloperfields mentions a similar line that is confusing:mNamasteField = session.createField("current_namastes", NAMASTE_FIELD_ID, FitContributor.DATA_TYPE_FLOAT, { :mesgType=>Fit.MESG_TYPE_RECORD, :units=>"N" });To make it clearer I would suggest in both cases ether to use using Toybox.FitContributor as Fit; and then use `Fit` everywhere OR even better just use `FitContributor` everywhere

- Mar 13, 2023
- View Status History
- More
- Cancel

Toybox.FitContributor

Toybox.FitContributor.Field


Excellent. The web search confirms:

1. **Monkey C static methods**: `static function name()` is the syntax, and they can be called as `ClassName.method()`. My fix to make `load()` and `save()` static is correct.

2. **FitContributor**: Available since API Level 1.3.0, so it's fine for fēnix 3 / API 1.4.0. The constants `FitContributor.DATA_TYPE_UINT16`, `FitContributor.DATA_TYPE_UINT8`, `FitContributor.MESG_TYPE_SESSION` are correct.

One issue from the search: the `mesgType` should use `FitContributor.MESG_TYPE_SESSION` not `Fit.MESG_TYPE_SESSION`. Let me check the current code... In `BreathHoldView.mc` it uses `{:mesgType => Fit.MESG_TYPE_SESSION, ...}`. That's potentially wrong according to the docs. It should be `FitContributor.MESG_TYPE_SESSION`.

Wait, but `Fit` might be imported via `using Toybox.FitContributor as Fit;` or there might be a `Fit` module. In the current code, `BreathHoldView.mc` has `using Toybox.FitContributor;` but not `using Toybox.FitContributor as Fit;`. So `Fit.MESG_TYPE_SESSION` would be undefined.

Actually, wait - there is a `Toybox.Fit` module? Let me check. The search result says "It has to be FitContributor.MESG_TYPE_RECORD or using Toybox.FitContributor as Fit;". So `Fit.MESG_TYPE_*` would only work if there's a `Toybox.Fit` module. In API 1.4, maybe `Fit` module exists. But to be safe, the code should use `FitContributor.MESG_TYPE_SESSION`.

Let me check the original code in the summary. The original fields used `{:mesgType => Fit.MESG_TYPE_SESSION, ...}`. Hmm, maybe `Fit` is a valid module. Let me verify with another search.

Actually, from the Garmin docs, `FitContributor` has `MESG_TYPE_SESSION`, `MESG_TYPE_LAP`, `MESG_TYPE_RECORD`. There is no separate `Fit` module mentioned in the search result. The docs say `Fit.MESG_TYPE_RECORD` doesn't work. So I should change `Fit.MESG_TYPE_SESSION` to `FitContributor.MESG_TYPE_SESSION` in `BreathHoldView.mc`.

But wait, this might have been pre-existing in the original code. The user asked to implement pool-length distance. This is a bug I should fix since it would cause a compile error.

Actually, let me be careful. Maybe in older API, `Fit` was the module name. Let me search more specifically.

Actually, looking at the current BreathHoldView.mc, it has `using Toybox.FitContributor;` and uses `Fit.MESG_TYPE_SESSION`. If `Fit` is not defined, this will fail. Let me change it to `FitContributor.MESG_TYPE_SESSION`.

But I should also check if `Fit` is imported somewhere. It isn't. The file has:
- using Toybox.WatchUi;
- using Toybox.Graphics;
- using Toybox.System;
- using Toybox.Lang;
- using Toybox.Timer;
- using Toybox.ActivityRecording;
- using Toybox.FitContributor;
- using Toybox.Sensor;

No `using Toybox.Fit;`. So `Fit.MESG_TYPE_SESSION` would be undefined. I need to change these to `FitContributor.MESG_TYPE_SESSION`.

Let me make that fix.

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

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

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

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

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

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

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

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