Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
180 changes: 170 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

![This is the version 1 layout of the App:](resources/images/AppInitial.png)
---

![This si the menu, which allows the user to select the min and max cadence zone values.](resources/images/AppMenu.png)
## ✨ Core Features

![This is the App whilst running when the user is out of the Target Zone.](resources/images/AppRunningOutZone.png)
### 🏃‍♂️ Custom Cadence Zone
- User-defined **minimum and maximum cadence**
- Clear **in-zone / out-of-zone** visual feedback

![This is the App whilst running when the user is in the Target Zone.](resources/images/AppRunningInZone.png)
### 🔔 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
9 changes: 9 additions & 0 deletions resources/layouts/layout.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,13 @@
justification="Gfx.TEXT_JUSTIFY_CENTER"
color="Gfx.COLOR_WHITE" />

<!-- Cadence Quality (Easter Egg) -->
<label id="cq_text"
x="center"
y="85%"
font="Gfx.FONT_TINY"
justification="Gfx.TEXT_JUSTIFY_CENTER"
color="Gfx.COLOR_LT_GRAY" />


</layout>
137 changes: 40 additions & 97 deletions source/Delegates/SimpleViewDelegate.mc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading