diff --git a/.DS_Store b/.DS_Store
index 4ab3da3..2033963 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/README.md b/README.md
index 4f6dfdb..5dbf962 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,180 @@
# garmin-smartwatch
-This is an app for the Garmin Forerunner 165, which allows a user to specify a cadence zone and be alerted when their running falls outside the zone.
-The watch will vibrate as well as give visual display of the runners progress.
-**Compilaton INstructions**
-*You must have generated a developer key to compile the build. Then run:*
+This project is a **custom Garmin Connect IQ watch app** designed for the **Garmin Forerunner 165**, focused on **cadence-based running feedback**.
-`monkeyc -o TestingCadence.prg -f monkey.jungle -y developer_key.der -w`
+The app allows runners to define a **target cadence zone** and receive:
-`monkeydo testingCadence.prg fr165`
+- Real-time visual feedback
+- Haptic alerts when cadence falls outside the target zone
+- Live run metrics including cadence, heart rate, distance, and time
+The goal is to support **cadence awareness and consistency during runs** without overwhelming the runner with complex data.
-
+---
-
+## ✨ Core Features
-
+### 🏃♂️ Custom Cadence Zone
+- User-defined **minimum and maximum cadence**
+- Clear **in-zone / out-of-zone** visual feedback
-
+### 🔔 Real-Time Alerts
+- Visual indicators
+- Haptic alerts when cadence drops below or exceeds the target range
+### 📊 Live Run Metrics
+- Cadence
+- Heart rate
+- Distance
+- Elapsed time
+
+### ⏺️ Activity Interaction
+- Explicit start / stop cadence monitoring
+- Visual indicator when monitoring is active
+- No background execution unless explicitly started by the user
+
+---
+
+## 🧠 Experimental Feature: Cadence Quality (CQ)
+
+This project includes an **experimental metric** called **Cadence Quality (CQ)**, designed to provide a **higher-level assessment of cadence consistency** over the course of a run.
+
+Unlike instantaneous cadence alerts, Cadence Quality evaluates cadence **over time**, capturing not just whether the runner hits the target zone, but **how consistently and smoothly** they do so.
+
+> **Cadence Quality is a pilot research-style metric**, not a clinical or prescriptive measure.
+
+---
+
+## 📐 How Cadence Quality Works
+
+Cadence Quality is a **composite score (0–100)** derived from two components:
+
+### 1️⃣ Time-in-Zone
+- The proportion of recent cadence samples that fall within the configured cadence range
+- Rewards sustained adherence to the target cadence
+
+### 2️⃣ Cadence Smoothness
+- Measures how stable cadence is between consecutive samples
+- Large fluctuations reduce the smoothness score
+
+### 🧮 Weighting Formula
+
+```text
+Cadence Quality = (Time-in-Zone × 70%) + (Cadence Smoothness × 30%)
+`````
+This weighting reflects research priorities where consistency matters more than momentary precision.
+
+---
+
+## ⏱️ Warm-Up Window
+To reduce early-run noise:
+
+- CQ is withheld during the initial warm-up period
+- A minimum data window (~30 seconds) must be collected before CQ is computed
+- During this phase, the UI displays:
+
+```text
+CQ: --
+`````
+This prevents misleading early scores caused by sensor stabilization and pacing adjustments.
+
+---
+
+## ❄️ Frozen Final Score
+- CQ is computed live during cadence monitoring
+- When monitoring stops, the final CQ score is frozen
+- This produces one evaluative score for the completed session
+
+This mirrors how higher-level performance metrics are treated in research and commercial running analytics.
+
+## 🧩 UI Integration (Easter Egg)
+Cadence Quality is intentionally designed as a secondary, low-salience metric:
+- Visible during cadence monitoring
+- Hidden during warm-up
+- Displays final frozen score after monitoring ends
+
+This positions CQ as an advanced insight for curious or research-oriented users, without distracting from core cadence feedback.
+
+## 🧪 Debugging & Diagnostics (Team Update Integration)
+Significant development time was spent on debugging, validation, and traceability of the CQ metric.
+
+### What Was Added / Refined
+- Implemented Cadence Quality (CQ) as a new metric alongside live cadence
+- Built a debug + diagnostic flow so CQ behaviour is visible and traceable in the terminal:
+ - Warm-up phase
+ - Live CQ values
+ - Final frozen summary
+- Added a warm-up phase to prevent early noisy calculations
+- Implemented final CQ freezing when cadence monitoring stops
+- Added CQ confidence levels:
+ - High
+ - Medium
+ - Low
+ Based on cadence data completeness
+- Added a CQ trend indicator:
+ - Improving
+ - Stable
+ - Declining
+ Using a rolling window of recent CQ values
+- Refactored start/stop logic so:
+ - Cadence monitoring is explicit
+ - Nothing runs in the background unintentionally
+- Ensured everything remains within Watch App constraints:
+ - No activity recording
+ - No FIT file generation
+
+## 🎯 Why Cadence Quality Matters
+
+Cadence Quality measures **how consistently and smoothly** a runner maintains cadence within an ideal range — not just how fast they step.
+
+This is important because:
+
+- Consistent cadence is linked to **running efficiency**
+- Smooth cadence transitions reduce **impact stress**
+- Variability in cadence has been associated with **injury risk**
+- Stakeholders benefit from **interpretable, higher-level insights** rather than raw sensor noise
+
+CQ is therefore positioned as a **research-aligned exploratory metric** with clear future potential.
+
+---
+
+## 🧠 Abandoned Experiment: “Hardcore Mode” (Postmortem)
+
+An attempted hidden **“hardcore mode” Easter egg** was explored, intended to:
+
+- Dynamically tighten cadence thresholds
+- Adapt difficulty for advanced users
+
+However:
+
+- This introduced **significant platform constraints**
+- Required shifting from a **Watch App → Activity App**
+- Had broader implications than initially anticipated
+- Ultimately delayed progress and was rolled back
+
+This served as a valuable lesson in **Connect IQ platform boundaries** and **app-type tradeoffs**.
+
+---
+
+## 🛠️ Compilation Instructions
+
+You must generate your own **Garmin developer key** before compiling.
+
+From the project root:
+
+```
+monkeyc -o TestingCadence.prg -f monkey.jungle -y developer_key.der -w
+```
+
+Run in the simulator:
+
+```
+monkeydo TestingCadence.prg fr165
+```
+
+If fr165 is not available in your SDK version, a similar device (e.g. venu2) can be used for simulation.
+
+## 📌 Notes
+- Cadence Quality is experimental and intended for exploration and research
+- Thresholds, confidence bands, and weightings are configurable
+- The system is designed for iteration, validation, and future expansion
diff --git a/resources/layouts/layout.xml b/resources/layouts/layout.xml
index 8aa57d8..c17a018 100644
--- a/resources/layouts/layout.xml
+++ b/resources/layouts/layout.xml
@@ -45,4 +45,13 @@
justification="Gfx.TEXT_JUSTIFY_CENTER"
color="Gfx.COLOR_WHITE" />
+
+
+
+
\ No newline at end of file
diff --git a/source/Delegates/SimpleViewDelegate.mc b/source/Delegates/SimpleViewDelegate.mc
index 75f9531..814efe8 100644
--- a/source/Delegates/SimpleViewDelegate.mc
+++ b/source/Delegates/SimpleViewDelegate.mc
@@ -5,143 +5,86 @@ class SimpleViewDelegate extends WatchUi.BehaviorDelegate {
private var _currentView = null;
- function initialize() {
+ function initialize() {
BehaviorDelegate.initialize();
}
- function onMenu(){
- //called by the timer after 1s hold
+ // Long-press MENU (optional settings)
+ function onMenu() as Boolean {
var menu = new WatchUi.Menu2({:resources => "menus/menu.xml"});
-
- WatchUi.pushView(new Rez.Menus.MainMenu(), new SelectCadenceDelegate(menu), WatchUi.SLIDE_BLINK);
-
+ WatchUi.pushView(
+ new Rez.Menus.MainMenu(),
+ new SelectCadenceDelegate(menu),
+ WatchUi.SLIDE_BLINK
+ );
return true;
-
}
+ // SELECT toggles cadence monitoring
function onSelect() as Boolean {
- // Toggle recording on/off with SELECT button
var app = getApp();
-
+
if (app.isActivityRecording()) {
- // Show stop menu
- var menu = new WatchUi.Menu2({:title => "Stop Recording?"});
- menu.addItem(new WatchUi.MenuItem("Yes", null, :stop_yes, {}));
- menu.addItem(new WatchUi.MenuItem("No", null, :stop_no, {}));
- WatchUi.pushView(menu, new StopMenuDelegate(), WatchUi.SLIDE_IMMEDIATE);
+ app.stopRecording();
+ System.println("[UI] Cadence monitoring stopped");
} else {
- // Start recording immediately
app.startRecording();
- WatchUi.requestUpdate();
+ System.println("[UI] Cadence monitoring started");
}
-
+
+ WatchUi.requestUpdate();
return true;
}
- function onKey(keyEvent as WatchUi.KeyEvent){
+ function onKey(keyEvent as WatchUi.KeyEvent) as Boolean {
var key = keyEvent.getKey();
- if(key == WatchUi.KEY_UP)//block GarminControlMenu (the triangle screen)
- {
+ // Block Garmin system menu
+ if (key == WatchUi.KEY_UP) {
return true;
}
- if(key == WatchUi.KEY_DOWN){
+ if (key == WatchUi.KEY_DOWN) {
_currentView = new AdvancedView();
-
- // Switches the screen to advanced view by clocking down button
- WatchUi.pushView(_currentView, new AdvancedViewDelegate(_currentView), WatchUi.SLIDE_DOWN);
+ WatchUi.pushView(
+ _currentView,
+ new AdvancedViewDelegate(_currentView),
+ WatchUi.SLIDE_DOWN
+ );
return true;
}
return false;
}
+ function onSwipe(event as WatchUi.SwipeEvent) as Boolean {
+ var direction = event.getDirection();
- function onSwipe(SwipeEvent as WatchUi.SwipeEvent){
- var direction = SwipeEvent.getDirection();
-
if (direction == WatchUi.SWIPE_UP) {
- _currentView = new AdvancedView();
- System.println("Swiped Down");
- WatchUi.pushView(_currentView, new AdvancedViewDelegate(_currentView), WatchUi.SLIDE_DOWN);
+ _currentView = new AdvancedView();
+ WatchUi.pushView(
+ _currentView,
+ new AdvancedViewDelegate(_currentView),
+ WatchUi.SLIDE_DOWN
+ );
return true;
}
- if(direction == WatchUi.SWIPE_LEFT){
+ if (direction == WatchUi.SWIPE_LEFT) {
_currentView = new SettingsView();
- System.println("Swiped Left");
- WatchUi.pushView(_currentView, new SettingsDelegate(_currentView), WatchUi.SLIDE_LEFT);
+ WatchUi.pushView(
+ _currentView,
+ new SettingsDelegate(_currentView),
+ WatchUi.SLIDE_LEFT
+ );
return true;
}
return false;
}
- function onBack(){
- //dont pop view and exit app
+ function onBack() as Boolean {
+ // Prevent accidental app exit
return true;
}
-
-}
-
-// Delegate for handling stop menu selection
-class StopMenuDelegate extends WatchUi.Menu2InputDelegate {
-
- function initialize() {
- Menu2InputDelegate.initialize();
- }
-
- function onSelect(item as MenuItem) as Void {
- var id = item.getId();
-
- if (id == :stop_yes) {
- // User selected YES to stop - show save menu
- var menu = new WatchUi.Menu2({:title => "Save Activity?"});
- menu.addItem(new WatchUi.MenuItem("Save", null, :save_yes, {}));
- menu.addItem(new WatchUi.MenuItem("Discard", null, :save_no, {}));
- WatchUi.pushView(menu, new SaveMenuDelegate(), WatchUi.SLIDE_IMMEDIATE);
- } else if (id == :stop_no) {
- // User selected NO - continue recording
- WatchUi.popView(WatchUi.SLIDE_IMMEDIATE);
- }
- }
-
- function onBack() as Void {
- // BACK button cancels
- WatchUi.popView(WatchUi.SLIDE_IMMEDIATE);
- }
-}
-
-// Delegate for handling save menu selection
-class SaveMenuDelegate extends WatchUi.Menu2InputDelegate {
-
- function initialize() {
- Menu2InputDelegate.initialize();
- }
-
- function onSelect(item as MenuItem) as Void {
- var id = item.getId();
- var app = getApp();
-
- if (id == :save_yes) {
- // Save the activity
- app.stopRecording();
- app.saveRecording();
- } else if (id == :save_no) {
- // Discard the activity
- app.stopRecording();
- app.discardRecording();
- }
-
- // Pop both menus (save and stop)
- WatchUi.popView(WatchUi.SLIDE_IMMEDIATE);
- WatchUi.popView(WatchUi.SLIDE_IMMEDIATE);
- WatchUi.requestUpdate();
- }
-
- function onBack() as Void {
- // BACK button goes back to stop menu
- WatchUi.popView(WatchUi.SLIDE_IMMEDIATE);
- }
}
diff --git a/source/GarminApp.mc b/source/GarminApp.mc
index c030c3c..8523f29 100644
--- a/source/GarminApp.mc
+++ b/source/GarminApp.mc
@@ -4,15 +4,16 @@ import Toybox.WatchUi;
import Toybox.Timer;
import Toybox.Activity;
import Toybox.System;
-import Toybox.ActivityRecording;
+
class GarminApp extends Application.AppBase {
const MAX_BARS = 60;
const BASELINE_AVG_CADENCE = 160;
const MAX_CADENCE = 190;
+ const MIN_CQ_SAMPLES = 30;
+ const DEBUG_MODE = true;
var globalTimer;
- var session as ActivityRecording.Session?;
var isRecording as Boolean = false;
enum {
@@ -32,12 +33,19 @@ class GarminApp extends Application.AppBase {
private var _cadenceIndex = 0;
private var _cadenceCount = 0;
private var _cadenceHistory as Array = new [MAX_BARS];
+ private var _finalCQ = null;
+ private var _missingCadenceCount = 0;
+ private var _finalCQConfidence = null;
+ private var _finalCQTrend = null;
private var _userHeight = null;//>>cm
private var _userSpeed = null;//>>m/s
private var _experienceLvl = null;
private var _userGender = null;
+ private var _cqHistory as Array = [];
+
+
function dummyValueTesting() as Void {
_userHeight = 170;
_userSpeed = 3.8;
@@ -68,10 +76,7 @@ class GarminApp extends Application.AppBase {
function onStop(state as Dictionary?) as Void {
System.println("[INFO] App stopping");
- // Stop recording if active
- if (isRecording && session != null) {
- stopRecording();
- }
+
if(globalTimer != null){
globalTimer.stop();
@@ -83,75 +88,152 @@ class GarminApp extends Application.AppBase {
}
function startRecording() as Void {
- if (!isRecording) {
- System.println("[INFO] Starting activity recording");
-
- // Create a new session
- session = ActivityRecording.createSession({
- :name => "Cadence Training",
- :sport => ActivityRecording.SPORT_RUNNING,
- :subSport => ActivityRecording.SUB_SPORT_GENERIC
- });
-
- if (session != null) {
- session.start();
- isRecording = true;
- System.println("[INFO] Recording started successfully");
- } else {
- System.println("[ERROR] Failed to create session");
- }
- }
+
+ if (isRecording) {
+ return;
}
+ System.println("[INFO] Starting cadence monitoring");
+
+ _finalCQ = null;
+ _finalCQConfidence = null;
+ _finalCQTrend = null;
+ _cqHistory = [];
+ _cadenceCount = 0;
+ _missingCadenceCount = 0;
+
+ isRecording = true;
+
+}
+
+
function stopRecording() as Void {
- if (isRecording && session != null) {
- System.println("[INFO] Stopping activity recording");
- session.stop();
- isRecording = false;
- System.println("[INFO] Recording stopped");
- }
- }
- function saveRecording() as Void {
- if (session != null) {
- System.println("[INFO] Saving activity");
- session.save();
- session = null;
- isRecording = false;
- System.println("[INFO] Activity saved");
- }
+ if (!isRecording) {
+ return;
}
- function discardRecording() as Void {
- if (session != null) {
- System.println("[INFO] Discarding activity");
- session.discard();
- session = null;
- isRecording = false;
- System.println("[INFO] Activity discarded");
- }
+ System.println("[INFO] Stopping cadence monitoring");
+
+ var cq = computeCadenceQualityScore();
+
+ if (cq >= 0) {
+ _finalCQ = cq;
+ _finalCQConfidence = computeCQConfidence();
+ _finalCQTrend = computeCQTrend();
+
+ System.println(
+ "[CADENCE QUALITY] Final CQ frozen at " +
+ cq.format("%d") + "% (" +
+ _finalCQTrend + ", " +
+ _finalCQConfidence + " confidence)"
+ );
+
+ writeDiagnosticLog();
}
+ isRecording = false;
+}
+
function isActivityRecording() as Boolean {
return isRecording;
}
function updateCadence() as Void {
- var info = Activity.getActivityInfo();
-
- if (info != null && info.currentCadence != null) {
- var newCadence = info.currentCadence;
- _cadenceHistory[_cadenceIndex] = newCadence.toFloat();
- _cadenceIndex = (_cadenceIndex + 1) % MAX_BARS;
- if (_cadenceCount < MAX_BARS) { _cadenceCount++; }
+ if (!isRecording) {
+ return; // ignore samples when not actively monitoring
+ }
+
+ var info = Activity.getActivityInfo();
+
+ // ----- Cadence sample handling -----
+ if (info != null && info.currentCadence != null) {
+ var newCadence = info.currentCadence;
+
+ // Store cadence sample
+ _cadenceHistory[_cadenceIndex] = newCadence.toFloat();
+ _cadenceIndex = (_cadenceIndex + 1) % MAX_BARS;
+
+ if (_cadenceCount < MAX_BARS) {
+ _cadenceCount++;
}
-
- // Log memory every 60 seconds
- if (_cadenceIndex % 60 == 0 && _cadenceIndex > 0) {
- Logger.logMemoryStats("Runtime");
+
+ if (DEBUG_MODE) {
+ System.println("[CADENCE] " + newCadence);
+}
+ } else {
+ // Track missing cadence samples (sensor dropouts)
+ _missingCadenceCount++;
+ }
+
+ // ----- Cadence Quality computation -----
+ var cq = computeCadenceQualityScore();
+
+ if (cq < 0) {
+ System.println(
+ "[CADENCE QUALITY] Warming up (" +
+ _cadenceCount.toString() + "/" +
+ MIN_CQ_SAMPLES.toString() + " samples)"
+ );
+ } else {
+ if (DEBUG_MODE) {
+ System.println("[CADENCE QUALITY] CQ = " + cq.format("%d") + "%");
+}
+
+ // Record CQ history for trend analysis
+ _cqHistory.add(cq);
+
+ // Keep sliding window small and recent
+ if (_cqHistory.size() > 10) {
+ _cqHistory.remove(0);
}
}
+ // ----- Memory logging (approx once per minute) -----
+ if (_cadenceIndex % 60 == 0 && _cadenceIndex > 0) {
+ Logger.logMemoryStats("Runtime");
+ }
+
+
+}
+
+
+// Cadence Quality
+function computeTimeInZoneScore() as Number {
+
+ // Not enough data yet
+ if (_cadenceCount < MIN_CQ_SAMPLES) {
+ return -1; // sentinel value meaning "not ready"
+ }
+
+ var minZone = _idealMinCadence;
+ var maxZone = _idealMaxCadence;
+
+ var inZoneCount = 0;
+ var validSamples = 0;
+
+ for (var i = 0; i < MAX_BARS; i++) {
+ var c = _cadenceHistory[i];
+
+ if (c != null) {
+ validSamples++;
+
+ if (c >= minZone && c <= maxZone) {
+ inZoneCount++;
+ }
+ }
+ }
+
+ if (validSamples == 0) {
+ return -1;
+ }
+
+ var ratio = inZoneCount.toFloat() / validSamples.toFloat();
+ return (ratio * 100).toNumber();
+}
+
+
+
function idealCadenceCalculator() as Void {
var referenceCadence = 0;
var finalCadence = 0;
@@ -182,37 +264,144 @@ class GarminApp extends Application.AppBase {
_idealMinCadence = finalCadence - 5;
}
- function idealCadenceCalculator() as Void {
- var referenceCadence = 0;
- var finalCadence = 0;
- var userLegLength = _userHeight * 0.53;
-
+ function computeSmoothnessScore() as Number {
- //reference cadence
- switch (_userGender) {
- case Male:
- referenceCadence = (-1.268 * userLegLength) + (3.471 * _userSpeed) + 261.378;
- break;
- case Female:
- referenceCadence = (-1.190 * userLegLength) + (3.705 * _userSpeed) + 249.688;
- break;
- default:
- referenceCadence = (-1.251 * userLegLength) + (3.665 * _userSpeed) + 254.858;
- break;
+ // Not enough data yet
+ if (_cadenceCount < MIN_CQ_SAMPLES) {
+ return -1; // not ready
+ }
+
+ var totalDiff = 0.0;
+ var diffCount = 0;
+
+ for (var i = 1; i < MAX_BARS; i++) {
+ var prev = _cadenceHistory[i - 1];
+ var curr = _cadenceHistory[i];
+
+ if (prev != null && curr != null) {
+ totalDiff += abs(curr - prev);
+ diffCount++;
}
+ }
- //experience adjustment
- referenceCadence = referenceCadence * _experienceLvl;
+ if (diffCount == 0) {
+ return -1;
+ }
- //apply threshold
- referenceCadence = Math.round(referenceCadence);
- finalCadence = max(BASELINE_AVG_CADENCE,min(referenceCadence,MAX_CADENCE)).toNumber();
+ var avgDiff = totalDiff / diffCount;
- //set new min max ideal cadence
- _idealMaxCadence = finalCadence + 5;
- _idealMinCadence = finalCadence - 5;
+ /*
+ Interpret avgDiff:
+ - ~0–1 → very smooth
+ - ~2–3 → normal
+ - >5 → erratic
+ */
+
+ var rawScore = 100 - (avgDiff * 10);
+
+ // Clamp to 0–100
+ if (rawScore < 0) { rawScore = 0; }
+ if (rawScore > 100) { rawScore = 100; }
+
+ return rawScore;
+}
+
+function computeCadenceQualityScore() as Number {
+
+ var timeInZone = computeTimeInZoneScore();
+ var smoothness = computeSmoothnessScore();
+
+ // Not ready yet
+ if (timeInZone < 0 || smoothness < 0) {
+ return -1;
+ }
+
+ // Weighted combination
+ var cq =
+ (timeInZone * 0.7) +
+ (smoothness * 0.3);
+
+ return cq.toNumber();
+}
+
+
+function computeCQConfidence() as String {
+
+ // Not enough data → low confidence
+ if (_cadenceCount < MIN_CQ_SAMPLES) {
+ return "Low";
}
+ var missingRatio = _missingCadenceCount.toFloat() /
+ (_cadenceCount + _missingCadenceCount).toFloat();
+
+ if (missingRatio > 0.2) {
+ return "Low";
+ } else if (missingRatio > 0.1) {
+ return "Medium";
+ } else {
+ return "High";
+ }
+}
+
+function computeCQTrend() as String {
+
+ if (_cqHistory.size() < 5) {
+ return "Stable";
+ }
+
+ var first = _cqHistory[0];
+ var last = _cqHistory[_cqHistory.size() - 1];
+
+ var delta = last - first;
+
+ if (delta < -5) {
+ return "Declining";
+ } else if (delta > 5) {
+ return "Improving";
+ } else {
+ return "Stable";
+ }
+}
+
+function writeDiagnosticLog() as Void {
+
+ if (!DEBUG_MODE) {
+ return;
+ }
+
+ System.println("===== DIAGNOSTIC RUN SUMMARY =====");
+
+ System.println("Final CQ: " +
+ (_finalCQ != null ? _finalCQ.format("%d") + "%" : "N/A"));
+
+ System.println("CQ Confidence: " +
+ (_finalCQConfidence != null ? _finalCQConfidence : "N/A"));
+
+ System.println("CQ Trend: " +
+ (_finalCQTrend != null ? _finalCQTrend : "N/A"));
+
+ System.println("Cadence samples collected: " + _cadenceCount.toString());
+ System.println("Missing cadence samples: " + _missingCadenceCount.toString());
+
+ var totalSamples = _cadenceCount + _missingCadenceCount;
+ if (totalSamples > 0) {
+ var validRatio =
+ (_cadenceCount.toFloat() / totalSamples.toFloat()) * 100;
+
+ System.println("Valid data ratio: " +
+ validRatio.format("%d") + "%");
+ }
+
+ System.println("Ideal cadence range: " +
+ _idealMinCadence.toString() + "-" +
+ _idealMaxCadence.toString());
+
+ System.println("===== END DIAGNOSTIC SUMMARY =====");
+}
+
+
+
function getMinCadence() as Number {
return _idealMinCadence;
@@ -283,6 +472,23 @@ class GarminApp extends Application.AppBase {
return (a > b) ? a : b;
}
+ function abs(x) {
+ return (x < 0) ? -x : x;
+ }
+
+ function getFinalCadenceQuality() {
+ return _finalCQ;
+
+ }
+
+ function getFinalCQConfidence() {
+ return _finalCQConfidence;
+ }
+
+ function getFinalCQTrend() {
+ return _finalCQTrend;
+ }
+
// Return the initial view of your application here
function getInitialView() as [Views] or [Views, InputDelegates] {
@@ -292,4 +498,6 @@ class GarminApp extends Application.AppBase {
function getApp() as GarminApp {
return Application.getApp() as GarminApp;
-}
\ No newline at end of file
+}
+
+
diff --git a/source/Views/SimpleView.mc b/source/Views/SimpleView.mc
index ff004b8..db9d268 100644
--- a/source/Views/SimpleView.mc
+++ b/source/Views/SimpleView.mc
@@ -15,6 +15,10 @@ class SimpleView extends WatchUi.View {
private var _cadenceZoneDisplay;
private var _lastZoneState = 0; // -1 = below, 0 = inside, 1 = above
private var _vibeTimer = new Timer.Timer();
+ private var _cqDisplay;
+ private var _hardcoreDisplay;
+
+
function _secondVibe() as Void {
// Haptics not available on this target SDK/device in this workspace.
@@ -35,6 +39,10 @@ class SimpleView extends WatchUi.View {
_heartrateDisplay = findDrawableById("heartrate_text");
_distanceDisplay = findDrawableById("distance_text");
_timeDisplay = findDrawableById("time_text");
+ _cqDisplay = findDrawableById("cq_text");
+ _hardcoreDisplay = findDrawableById("hardcore_text");
+
+
}
// Called when this View is brought to the foreground. Restore
@@ -172,6 +180,25 @@ class SimpleView extends WatchUi.View {
}else{
_timeDisplay.setText("--:--:--");
}
+
+ /// --- Cadence Quality (Easter Egg) ---
+ if (_cqDisplay != null) {
+ var app = getApp();
+ var frozenCQ = app.getFinalCadenceQuality();
+
+ if (frozenCQ != null) {
+ _cqDisplay.setText("CQ: " + frozenCQ.format("%d") + "%");
+ } else {
+ var cq = app.computeCadenceQualityScore();
+
+ if (cq < 0) {
+ _cqDisplay.setText("CQ: --");
+ } else {
+ _cqDisplay.setText("CQ: " + cq.format("%d") + "%");
+ }
+ }
+ }
+
}