From 12ef72fd2c90172a05df737fd274311ee02a846d Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 27 Jan 2026 11:26:51 +1100 Subject: [PATCH 1/5] updated technical Documentation --- TECHNICAL_DOCUMENTATION_v1.md | 1714 ------------ TECHNICAL_DOCUMENTATION_v2.md | 2480 +++++++++++++++++ .../SelectBarChartDelegate.mc | 50 - .../SelectCustomizableDelegate.mc | 47 - .../SelectAudibleDelegate.mc | 50 - .../SelectFeedbackDelegate.mc | 66 - .../FeedbackDelegates/SelectHapticDelegate.mc | 50 - .../ProfileDelegates/ProfilePickerDelegate.mc | 39 - .../ProfileDelegates/ProfilePickerFactory.mc | 64 - .../SelectExperienceDelegate.mc | 61 - .../ProfileDelegates/SelectGenderDelegate.mc | 52 - .../ProfileDelegates/SelectProfileDelegate.mc | 103 - .../SettingsDelegates/SettingsDelegate.mc | 101 - source/Logger.mc | 24 - source/SensorManager.mc | 58 - source/Views/SettingsView.mc | 64 - 16 files changed, 2480 insertions(+), 2543 deletions(-) delete mode 100644 TECHNICAL_DOCUMENTATION_v1.md create mode 100644 TECHNICAL_DOCUMENTATION_v2.md delete mode 100644 source/Delegates/SettingsDelegates/CustomizableDelegates/SelectBarChartDelegate.mc delete mode 100644 source/Delegates/SettingsDelegates/CustomizableDelegates/SelectCustomizableDelegate.mc delete mode 100644 source/Delegates/SettingsDelegates/FeedbackDelegates/SelectAudibleDelegate.mc delete mode 100644 source/Delegates/SettingsDelegates/FeedbackDelegates/SelectFeedbackDelegate.mc delete mode 100644 source/Delegates/SettingsDelegates/FeedbackDelegates/SelectHapticDelegate.mc delete mode 100644 source/Delegates/SettingsDelegates/ProfileDelegates/ProfilePickerDelegate.mc delete mode 100644 source/Delegates/SettingsDelegates/ProfileDelegates/ProfilePickerFactory.mc delete mode 100644 source/Delegates/SettingsDelegates/ProfileDelegates/SelectExperienceDelegate.mc delete mode 100644 source/Delegates/SettingsDelegates/ProfileDelegates/SelectGenderDelegate.mc delete mode 100644 source/Delegates/SettingsDelegates/ProfileDelegates/SelectProfileDelegate.mc delete mode 100644 source/Delegates/SettingsDelegates/SettingsDelegate.mc delete mode 100644 source/Logger.mc delete mode 100644 source/SensorManager.mc delete mode 100644 source/Views/SettingsView.mc diff --git a/TECHNICAL_DOCUMENTATION_v1.md b/TECHNICAL_DOCUMENTATION_v1.md deleted file mode 100644 index bd75857..0000000 --- a/TECHNICAL_DOCUMENTATION_v1.md +++ /dev/null @@ -1,1714 +0,0 @@ -# Rebback ooperation Garmin App - Technical Documentation - -## Table of Contents -0. [prequisites] (#prequisites) -0.5 [Build Process](#Build-Process) -1. [Architecture Overview](#architecture-overview) -2. [Core Components](#core-components) -3. [Data Flow](#data-flow) -4. [State Management](#state-management) -5. [Activity Recording System](#activity-recording-system) -6. [Cadence Quality Algorithm](#cadence-quality-algorithm) -7. [User Interface](#user-interface) -8. [Settings System](#settings-system) -9. [Features Reference](#features-reference) - ---- -## prequisites -- Garmin Connect IQ SDK 8.3.0+ -- Visual Studio Code with Connect IQ extension -- Forerunner 165/165 Music device or simulator - -## Build-Process -1. Clone repository -2. Configure project settings in `monkey.jungle` -3. Build for target device: - ```bash - monkeyc -o bin/app.prg -f monkey.jungle -y developer_key.der - -## Architecture Overview - -### Application Type -- **Type**: Garmin Watch App (not data field or widget) -- **Target Devices**: Forerunner 165, Forerunner 165 Music -- **SDK Version**: Minimum API Level 5.2.0 -- **Architecture**: MVC (Model-View-Controller/Delegate pattern) - -### High-Level Structure -``` -GarminApp (Application Core) - ├── Views - │ ├── SimpleView (Main activity view) - │ └── AdvancedView (Chart visualization) - ├── Delegates (Input handlers) - │ ├── SimpleViewDelegate (Main controls) - │ ├── AdvancedViewDelegate (Chart controls) - │ └── Settings Delegates (Configuration) - ├── Managers - │ ├── SensorManager (Cadence sensor) - │ └── Logger (Memory tracking) - └── Data Processing - ├── Cadence Quality Calculator - └── Activity Recording Session -``` - ---- - -## Core Components - -### 1. GarminApp.mc -**Purpose**: Central application controller and data manager - -**Key Responsibilities**: -- Activity session lifecycle management (start/pause/resume/stop/save/discard) -- Cadence data collection and storage -- Cadence quality score computation -- State machine management -- Timer management -- Integration with Garmin Activity Recording API - -**Important Constants**: -```monkey-c -MAX_BARS = 280 // Maximum cadence samples to store -BASELINE_AVG_CADENCE = 160 // Minimum acceptable cadence -MAX_CADENCE = 190 // Maximum cadence for calculations -MIN_CQ_SAMPLES = 30 // Minimum samples for CQ calculation -DEBUG_MODE = true // Enable debug logging -``` - -**State Variables**: -- `_sessionState`: Current session state (IDLE/RECORDING/PAUSED/STOPPED) -- `activitySession`: Garmin ActivityRecording session object -- `_cadenceHistory`: Circular buffer storing 280 cadence samples -- `_cadenceBarAvg`: Rolling average buffer for chart display -- `_cqHistory`: Last 10 CQ scores for trend analysis - ---- - -## Data Flow - -### 1. Cadence Data Collection Pipeline - -``` -Cadence Sensor - ↓ -Activity.getActivityInfo().currentCadence - ↓ -updateCadenceBarAvg() [Every 1 second] - ↓ -_cadenceBarAvg buffer (accumulates samples) - ↓ -When buffer full (chart duration samples) - ↓ -Calculate bar average - ↓ -updateCadenceHistory(average) - ↓ -_cadenceHistory circular buffer [280 samples] - ↓ -computeCadenceQualityScore() - ↓ -_cqHistory [Last 10 scores] -``` - -### 2. Timer System - -**Global Timer** (`globalTimer`): -- Frequency: Every 1 second -- Callback: `updateCadenceBarAvg()` -- Runs: Always (from app start to stop) -- Purpose: Collect cadence data when recording - -**View Refresh Timers**: -- SimpleView: Refresh every 1 second -- AdvancedView: Refresh every 1 second -- Purpose: Update UI elements - -### 3. Data Averaging System - -The app uses a two-tier averaging system: - -**Tier 1: Bar Averaging** -``` -Chart Duration = 6 seconds (ThirtyminChart default) -↓ -Collect 6 cadence readings (1 per second) -↓ -Calculate average of these 6 readings -↓ -Store as single bar value -``` - -**Tier 2: Historical Storage** -``` -280 bar values stored -↓ -Each bar = average of 6 seconds -↓ -Total history = 280 × 6 = 1680 seconds = 28 minutes -``` - -**Chart Duration Options**: -- FifteenminChart = 3 seconds per bar -- ThirtyminChart = 6 seconds per bar (default) -- OneHourChart = 13 seconds per bar -- TwoHourChart = 26 seconds per bar - ---- - -## State Management - -### Session State Machine - -``` -┌──────┐ -│ IDLE │ ← Initial state, no session -└──┬───┘ - │ startRecording() - ↓ -┌───────────┐ -│ RECORDING │ ← Activity running, timer active -└─┬───────┬─┘ - │ │ - │ │ pauseRecording() - │ ↓ - │ ┌────────┐ - │ │ PAUSED │ ← Activity paused, timer stopped - │ └───┬────┘ - │ │ - │ │ resumeRecording() - │ ↓ - │ (back to RECORDING) - │ - │ stopRecording() - ↓ -┌─────────┐ -│ STOPPED │ ← Activity stopped, awaiting save/discard -└────┬────┘ - │ - │ saveSession() or discardSession() - ↓ - (back to IDLE) -``` - -### State Transition Rules - -**IDLE → RECORDING**: -- User presses START/STOP button -- Creates new ActivityRecording session -- Starts Garmin timer -- Resets all cadence data arrays -- Initializes timestamps - -**RECORDING → PAUSED**: -- User selects "Pause" from menu -- Stops Garmin timer (timer pauses) -- Records pause timestamp -- Data collection stops - -**PAUSED → RECORDING**: -- User selects "Resume" from menu -- Restarts Garmin timer -- Accumulates paused time -- Data collection resumes - -**RECORDING/PAUSED → STOPPED**: -- User selects "Stop" from menu -- Stops Garmin timer -- Computes final CQ score -- Freezes all metrics -- Awaits save/discard decision - -**STOPPED → IDLE**: -- User selects "Save": Saves to FIT file -- User selects "Discard": Deletes session -- Resets all data structures -- Ready for new session - ---- - -## Activity Recording System - -### Garmin ActivityRecording Integration - -**Session Creation** (`startRecording()`): -```monkey-c -activitySession = ActivityRecording.createSession({ - :name => "Running", - :sport => ActivityRecording.SPORT_RUNNING, - :subSport => ActivityRecording.SUB_SPORT_GENERIC -}); -activitySession.start(); -``` - -**What This Does**: -- Creates official Garmin activity -- Starts timer (visible in UI) -- Records GPS, heart rate, cadence automatically -- Manages distance calculation -- Handles sensor data collection - -**Pause/Resume** (`pauseRecording()` / `resumeRecording()`): -```monkey-c -// Pause -activitySession.stop(); // Pauses timer - -// Resume -activitySession.start(); // Resumes timer -``` - -**Save** (`saveSession()`): -```monkey-c -activitySession.save(); -``` -- Writes FIT file to device -- Syncs to Garmin Connect -- Appears in activity history -- Includes all sensor data - -**Discard** (`discardSession()`): -```monkey-c -activitySession.discard(); -``` -- Deletes session completely -- No FIT file created -- No sync to Garmin Connect - ---- - -## Cadence Quality Algorithm - -### Overview -The Cadence Quality (CQ) score is a composite metric (0-100%) evaluating running efficiency. - -### Components - -#### 1. Time in Zone Score (70% weight) - -**Purpose**: Measures percentage of time spent in ideal cadence range - -**Algorithm**: -``` -idealMin = 120 spm (default) -idealMax = 150 spm (default) - -inZoneCount = 0 -validSamples = 0 - -for each sample in _cadenceHistory: - if sample exists: - validSamples++ - if sample >= idealMin AND sample <= idealMax: - inZoneCount++ - -timeInZone = (inZoneCount / validSamples) × 100 -``` - -**Example**: -- 200 valid samples -- 140 samples in zone (120-150) -- Score = (140/200) × 100 = 70% - -#### 2. Smoothness Score (30% weight) - -**Purpose**: Measures cadence consistency (less variation = better) - -**Algorithm**: -``` -totalDiff = 0 -diffCount = 0 - -for i = 1 to MAX_BARS: - prev = _cadenceHistory[i-1] - curr = _cadenceHistory[i] - if both exist: - totalDiff += abs(curr - prev) - diffCount++ - -avgDiff = totalDiff / diffCount -rawScore = 100 - (avgDiff × 10) -smoothness = clamp(rawScore, 0, 100) -``` - -**Interpretation**: -- avgDiff = 0-1: Very smooth (score ~90-100) -- avgDiff = 2-3: Normal (score ~70-80) -- avgDiff > 5: Erratic (score < 50) - -#### 3. Final CQ Score - -**Formula**: -``` -CQ = (timeInZone × 0.7) + (smoothness × 0.3) -``` - -**Example Calculation**: -``` -timeInZone = 75% -smoothness = 85% - -CQ = (75 × 0.7) + (85 × 0.3) - = 52.5 + 25.5 - = 78% -``` - -### CQ Confidence Level - -**Purpose**: Indicates reliability of CQ score - -**Factors**: -1. **Sample Count**: Need minimum 30 samples -2. **Missing Data Ratio**: Sensor dropout rate - -**Algorithm**: -``` -if samples < 30: - confidence = "Low" -else: - missingRatio = missingCount / (validCount + missingCount) - if missingRatio > 0.2: - confidence = "Low" - else if missingRatio > 0.1: - confidence = "Medium" - else: - confidence = "High" -``` - -### CQ Trend Analysis - -**Purpose**: Shows if cadence quality is improving during run - -**Algorithm**: -``` -Uses last 10 CQ scores (_cqHistory) - -if scores < 5: - trend = "Stable" -else: - delta = lastScore - firstScore - if delta < -5: - trend = "Declining" - else if delta > 5: - trend = "Improving" - else: - trend = "Stable" -``` - -### Ideal Cadence Calculator - -**Purpose**: Calculate personalized ideal cadence based on user profile - -**Formula** (gender-specific): - -**Male**: -``` -referenceCadence = (-1.268 × legLength) + (3.471 × speed) + 261.378 -``` - -**Female**: -``` -referenceCadence = (-1.190 × legLength) + (3.705 × speed) + 249.688 -``` - -**Other**: -``` -referenceCadence = (-1.251 × legLength) + (3.665 × speed) + 254.858 -``` - -**Experience Adjustment**: -``` -Beginner: multiplier = 1.06 (6% higher cadence) -Intermediate: multiplier = 1.04 (4% higher) -Advanced: multiplier = 1.02 (2% higher) - -finalCadence = referenceCadence × multiplier -idealMin = finalCadence - 5 -idealMax = finalCadence + 5 -``` - -**Example**: -``` -User: Male, 170cm height, 10 km/h speed, Intermediate - -legLength = 170 × 0.53 = 90.1 cm -speed = 10 / 3.6 = 2.78 m/s - -referenceCadence = (-1.268 × 90.1) + (3.471 × 2.78) + 261.378 - = -114.25 + 9.65 + 261.378 - = 156.78 - -adjusted = 156.78 × 1.04 = 163.05 -final = round(163.05) = 163 -clamped = max(160, min(163, 190)) = 163 - -idealMin = 163 - 5 = 158 spm -idealMax = 163 + 5 = 168 spm -``` - ---- - -## User Interface - -### View Architecture - -#### SimpleView.mc -**Purpose**: Main activity tracking screen - -**Display Elements**: -1. **Timer**: HH:MM:SS format from Activity.getActivityInfo().timerTime -2. **Heart Rate**: BPM with heart icon (red) -3. **Cadence**: Current spm with cadence icon (green/red based on zone) -4. **Distance**: Kilometers with 2 decimals -5. **Cadence Zone**: Text showing if in/out of ideal range -6. **CQ Score**: Cadence Quality percentage -7. **State Indicator**: Visual recording state (REC/PAUSE/STOP) - -**Layout** (from top to bottom): -``` -[Timer: 00:00:00] [REC ●] - - ❤️ [Heart Rate] 🏃 [Cadence] - - [Distance] km - - [Zone: In/Out (120-150)] - - CQ: [Score]% -``` - -**State Visual Indicators**: -- **IDLE**: "Press START/STOP to start" text -- **RECORDING**: Red dot + "REC" in top-right -- **PAUSED**: Yellow dot + "PAUSE" + flashing "PAUSED" text -- **STOPPED**: Green dot + "STOP" + "Activity Complete!" message - -#### AdvancedView.mc -**Purpose**: Real-time cadence visualization chart - -**Display Elements**: -1. **Timer**: Simplified format (H:MM) at top in yellow -2. **Heart Rate Circle**: Dark red circle on left with BPM -3. **Distance Circle**: Dark green circle on right with km -4. **Current Cadence**: Large centered text with spm -5. **Cadence Chart**: Histogram showing last 280 bars -6. **Chart Duration Label**: Shows time range (e.g., "Last 30 Minutes") - -**Chart Visualization**: -``` -Height of screen - ↓ -┌─────────────────────────┐ -│ Time: 1:30 │ -│ │ -│ ●HR Cadence Dist●│ -│ 150 170 2.5 │ -│ │ -│ ┌───────────────────┐ │ -│ │ ▂▃▅▇█▇▅▃▂▁▃▅▇█▇ │ │ ← Chart -│ │ ▁▂▃▅▇█▇▅▃▂▁▃▅▇█ │ │ -│ └───────────────────┘ │ -│ Last 30 Minutes │ -└─────────────────────────┘ -``` - -**Color Coding**: -- **Green** (0x00bf63): Cadence in ideal zone -- **Blue** (0x0cc0df): Below zone but within 20 spm -- **Grey** (0x969696): More than 20 spm below zone -- **Orange** (0xff751f): Above zone but within 20 spm -- **Red** (0xFF0000): More than 20 spm above zone - -**Chart Algorithm**: -``` -For each bar in cadenceHistory: - barHeight = (cadence / MAX_CADENCE_DISPLAY) × chartHeight - x = barZoneLeft + (barIndex × barWidth) - y = barZoneBottom - barHeight - - color = determineColor(cadence, idealMin, idealMax) - drawRectangle(x, y, barWidth, barHeight, color) -``` - -### Navigation Flow - -``` -SimpleView (Main) - ↓ Swipe UP / Press DOWN -AdvancedView (Chart) - ↓ Swipe DOWN / Press UP -SimpleView (Main) - ↓ Swipe LEFT / Press MENU -Settings Menu - ├── Profile - ├── Customization - ├── Feedback - └── Cadence Range -``` - -### Button Mapping (Forerunner 165) - -**Physical Buttons**: -``` - [LIGHT/MENU] - ↓ - Settings - - [UP] ← WATCH → [DOWN] -Settings AdvancedView - - [START/STOP] - ↓ - Main Control - - [BACK] - ↓ - Exit (if idle) -``` - -**START/STOP Button Behavior**: -| Current State | Action | Result | -|---------------|--------|--------| -| IDLE | Press | Start activity | -| RECORDING | Press | Show menu: Resume/Pause/Stop | -| PAUSED | Press | Show menu: Resume/Stop | -| STOPPED | Press | Show menu: Save/Discard | - ---- - -## Settings System - -### Architecture - -Settings use a hierarchical menu system with specialized delegates: - -``` -Settings Menu (SettingsMenuDelegate) - ├── Profile (SelectProfileDelegate) - │ ├── Height (ProfilePickerDelegate) - │ ├── Speed (ProfilePickerDelegate) - │ ├── Experience (SelectExperienceDelegate) - │ └── Gender (SelectGenderDelegate) - ├── Customization (SelectCustomizableDelegate) - │ └── Chart Duration (SelectBarChartDelegate) - ├── Feedback (SelectFeedbackDelegate) - │ ├── Haptic (SelectHapticDelegate) - │ └── Audible (SelectAudibleDelegate) - └── Cadence Range - ├── Min Cadence (Picker) - └── Max Cadence (Picker) -``` - -### Profile Settings - -#### Height Setting -**Purpose**: Calculate leg length for ideal cadence -**Range**: 100-250 cm -**Default**: 170 cm -**UI**: Number picker with " cm" label - -**Implementation**: -```monkey-c -ProfilePickerFactory(100, 250, 1, {:label=>" cm"}) -Callback: ProfilePickerDelegate(:prof_height) -Storage: app.setUserHeight(value) -``` - -#### Speed Setting -**Purpose**: Running pace for cadence calculation -**Range**: 3-30 km/h -**Default**: 10 km/h -**UI**: Number picker with " km/h" label - -#### Experience Level -**Purpose**: Adjust ideal cadence by fitness level -**Options**: -- Beginner (1.06 multiplier) -- Intermediate (1.04 multiplier) -- Advanced (1.02 multiplier) -**Default**: Beginner -**UI**: Menu selection - -**Rationale**: Less experienced runners typically benefit from slightly higher cadence to reduce impact forces. - -#### Gender -**Purpose**: Gender-specific cadence formulas -**Options**: Male, Female, Other -**Default**: Male -**UI**: Menu selection - -### Customization Settings - -#### Chart Duration -**Purpose**: Set time range for cadence chart -**Options**: -- 15 Minutes (3 sec/bar) -- 30 Minutes (6 sec/bar) [Default] -- 1 Hour (13 sec/bar) -- 2 Hours (26 sec/bar) -**Effect**: Changes `_chartDuration` which affects bar averaging - -### Feedback Settings - -#### Haptic Feedback -**Purpose**: Vibration alerts for zone crossing -**Options**: On/Off -**Behavior**: -- Single pulse: Dropped below min cadence -- Double pulse: Exceeded max cadence - -#### Audible Feedback -**Purpose**: Audio alerts for zone crossing -**Options**: On/Off -**Behavior**: Beep patterns for zone events - -### Cadence Range Settings - -**Purpose**: Manually override ideal cadence zone - -**Min Cadence**: -- Range: 100-180 spm -- Default: 120 spm -- UI: Number picker - -**Max Cadence**: -- Range: 120-200 spm -- Default: 150 spm -- UI: Number picker - -**Use Case**: Advanced users who want custom zones based on training goals. - ---- - -## Features Reference - -### 1. Activity Session Management - -**Feature**: Full lifecycle control of running activities - -**Components**: -- Start: Begin new activity with Garmin session -- Pause: Temporarily stop timer and data collection -- Resume: Continue paused activity -- Stop: End activity (awaiting save/discard) -- Save: Write to FIT file and sync to Garmin Connect -- Discard: Delete activity without saving - -**User Flow**: -``` -Press START/STOP - ↓ -Activity starts (timer runs) - ↓ -Press START/STOP → Select "Pause" - ↓ -Activity paused (timer stops) - ↓ -Press START/STOP → Select "Resume" - ↓ -Activity resumes (timer continues) - ↓ -Press START/STOP → Select "Stop" - ↓ -Activity stopped (timer frozen) - ↓ -Select "Save" or "Discard" - ↓ -Return to IDLE (ready for new activity) -``` - -### 2. Real-Time Cadence Monitoring - -**Feature**: Live cadence tracking with visual feedback - -**Data Source**: Activity.getActivityInfo().currentCadence -**Update Frequency**: Every 1 second -**Storage**: Circular buffer (280 samples = ~28 mins at default) - -**Visual Feedback**: -- **SimpleView**: Large cadence number with zone text -- **AdvancedView**: Color-coded histogram chart -- **Zone Indicator**: "In Zone" or "Out of Zone" text - -### 3. Cadence Quality Scoring - -**Feature**: Composite metric evaluating running efficiency - -**Algorithm**: Weighted combination of: -- Time in Zone (70%): Percentage in ideal range -- Smoothness (30%): Consistency of cadence - -**Output**: -- CQ Score: 0-100% -- Confidence: Low/Medium/High -- Trend: Improving/Stable/Declining - -**Update**: Real-time during recording, frozen when stopped - -### 4. Personalized Ideal Cadence - -**Feature**: Calculate optimal cadence based on user profile - -**Inputs**: -- Height (cm) -- Speed (km/h) -- Experience Level -- Gender - -**Output**: -- Ideal Min Cadence (spm) -- Ideal Max Cadence (spm) - -**Formula**: Gender-specific biomechanical equation with experience adjustment - -### 5. Historical Data Visualization - -**Feature**: Real-time cadence chart showing last 28 minutes - -**Chart Type**: Histogram (bar chart) -**Data Points**: 280 bars (each = average of 6 seconds) -**Color Coding**: 5-color gradient based on zone proximity -**Update**: Real-time (every second when recording) - -**Chart Duration Modes**: -- 15 min: Higher resolution (3 sec/bar) -- 30 min: Default (6 sec/bar) -- 1 hour: Lower resolution (13 sec/bar) -- 2 hours: Lowest resolution (26 sec/bar) - -### 6. Multi-Sensor Integration - -**Feature**: Display all relevant running metrics - -**Sensors**: -- **Cadence**: Steps per minute from cadence pod or wrist sensor -- **Heart Rate**: BPM from optical HR or chest strap -- **GPS**: Distance and speed -- **Timer**: Elapsed time (pauses with activity) - -**Display**: -- SimpleView: All metrics in text format -- AdvancedView: Heart rate and distance in circles - -### 7. Zone-Based Haptic Alerts - -**Feature**: Vibration feedback when leaving ideal zone - -**Triggers**: -- Single pulse: Cadence drops below min -- Double pulse: Cadence exceeds max - -**Implementation**: -- Tracks zone state (-1/0/1) -- Only triggers on state change -- Second pulse delayed 240ms - -### 8. Session Persistence - -**Feature**: Save activities to Garmin ecosystem - -**Save Format**: FIT file (Flexible and Interoperable Transfer) -**Storage Location**: Device internal storage -**Sync**: Automatic to Garmin Connect when synced -**Data Included**: -- Timer duration (excluding paused time) -- GPS track -- Heart rate -- Cadence samples -- Distance -- Speed/pace -- Custom CQ score (if supported) - -### 9. Memory Management - -**Feature**: Track and log memory usage - -**Implementation**: Logger.mc module -**Frequency**: -- On startup -- On shutdown -- Every ~60 seconds during runtime - -**Output**: System.println with stats -**Format**: `[MEMORY] Tag: used/total bytes (X% used)` - -### 10. Debug Logging - -**Feature**: Comprehensive logging for development - -**Enabled**: `DEBUG_MODE = true` -**Categories**: -- [INFO]: App lifecycle events -- [DEBUG]: Button presses, state changes -- [UI]: User interactions -- [CADENCE]: Cadence samples -- [CADENCE QUALITY]: CQ calculations -- [MEMORY]: Memory statistics - -**Toggle**: Set `DEBUG_MODE = false` for production - ---- - -## Data Structures - -### Circular Buffers - -**_cadenceHistory**: -``` -Type: Array[280] -Purpose: Store last 280 cadence bar averages -Access: Circular (wraps at 280) -Index: _cadenceIndex (0-279) -Count: _cadenceCount (0-280) -``` - -**_cadenceBarAvg**: -``` -Type: Array[_chartDuration] -Purpose: Temporary buffer for bar averaging -Access: Circular (wraps at chart duration) -Index: _cadenceAvgIndex -Count: _cadenceAvgCount -``` - -**_cqHistory**: -``` -Type: Array[10] -Purpose: Store last 10 CQ scores for trend -Access: Array (removes oldest when > 10) -``` - -### Session Metadata - -```monkey-c -_sessionStartTime: Number // System.getTimer() at start -_sessionPausedTime: Number // Total ms spent paused -_lastPauseTime: Number? // When current pause began -_finalCQ: Number? // Frozen CQ score when stopped -_finalCQConfidence: String? // Frozen confidence -_finalCQTrend: String? // Frozen trend -``` - ---- - -## Performance Considerations - -### Timer Efficiency -- **Global timer**: 1 second interval (low overhead) -- **View timers**: Only run when view is visible -- **Data collection**: O(1) operations (circular buffer) - -### Memory Usage -- **Cadence history**: 280 × 4 bytes = 1120 bytes -- **Bar average**: 6 × 4 bytes = 24 bytes (default) -- **CQ history**: 10 × 4 bytes = 40 bytes -- **Total data**: ~1200 bytes (negligible on modern watches) - -### CPU Usage -- **Cadence update**: O(n) where n = chart duration (typically 6) -- **CQ calculation**: O(280) = O(1) for fixed size -- **Chart rendering**: O(280) bars drawn per frame - -### Battery Impact -- **GPS**: Major drain (handled by Garmin OS) -- **Sensors**: Minimal (optical HR, cadence) -- **Screen refresh**: 1 Hz (low power) -- **Recommendation**: Use with GPS activities (already optimized) - ---- - -## Future Enhancement Ideas - -### Visualization Enhancements - -#### 1. **Current Cadence Marker** ⭐ -**Priority**: High -**Complexity**: Low -**Effort**: 1-2 hours - -**Description**: Add a horizontal line or marker showing current real-time cadence on the chart - -**Implementation**: -```monkey-c -// In drawChart() after drawing bars: -if (info != null && info.currentCadence != null) { - var currentCadence = info.currentCadence; - var currentY = barZoneBottom - ((currentCadence / MAX_CADENCE_DISPLAY) * chartHeight); - - // Draw yellow horizontal line - dc.setColor(Graphics.COLOR_YELLOW, Graphics.COLOR_TRANSPARENT); - dc.drawLine(chartLeft, currentY, chartRight, currentY); - - // Optional: Draw small arrow or label - dc.fillCircle(chartRight + 5, currentY, 3); // Dot at end -} -``` - -**Benefits**: -- Instant visual reference for current performance -- Easy to see if cadence is trending up or down relative to history -- Helps runners maintain target cadence by comparing to past bars - -**User Experience**: -- Glanceable feedback during run -- No need to look at number - just see if line is in green zone - ---- - -#### 2. **Smooth Bars (Moving Average)** ⭐ -**Priority**: Medium -**Complexity**: Medium -**Effort**: 3-4 hours - -**Description**: Apply exponential moving average (EMA) or simple moving average to reduce visual "jumpiness" from sensor noise - -**Implementation Options**: - -**Option A: Simple Moving Average (SMA)** -```monkey-c -// Average last N bars for smoother display -private var _smoothedHistory as Array = new [MAX_BARS]; - -function smoothBars(windowSize as Number) as Void { - for (var i = 0; i < _cadenceCount; i++) { - var sum = 0.0; - var count = 0; - - // Average surrounding bars - for (var j = -windowSize; j <= windowSize; j++) { - var idx = (i + j + MAX_BARS) % MAX_BARS; - if (_cadenceHistory[idx] != null) { - sum += _cadenceHistory[idx]; - count++; - } - } - - _smoothedHistory[i] = (count > 0) ? (sum / count) : 0; - } -} -``` - -**Option B: Exponential Moving Average (EMA)** (Recommended) -```monkey-c -// Weighted average favoring recent data -private var _emaHistory as Array = new [MAX_BARS]; -private const SMOOTHING_FACTOR = 0.3; // α (0-1), lower = smoother - -function updateEMA(newValue as Float, index as Number) as Void { - if (index == 0 || _emaHistory[index-1] == null) { - _emaHistory[index] = newValue; - } else { - _emaHistory[index] = SMOOTHING_FACTOR * newValue + - (1 - SMOOTHING_FACTOR) * _emaHistory[index-1]; - } -} -``` - -**Configurable Settings**: -``` -Smoothing: Off / Low (α=0.5) / Medium (α=0.3) / High (α=0.1) -``` - -**Benefits**: -- Cleaner visual representation -- Reduces noise from sensor fluctuations -- Easier to spot genuine trends vs. random variation -- More professional appearance - -**Tradeoffs**: -- Slightly delayed response to actual changes -- May hide brief cadence spikes/drops -- Recommendation: Make it toggleable - ---- - -#### 3. **Fade Old Bars** ⭐ -**Priority**: Low -**Complexity**: Low -**Effort**: 1-2 hours - -**Description**: Apply opacity/alpha gradient to bars based on age - recent bars full opacity, older bars gradually fade - -**Implementation**: -```monkey-c -// Calculate fade based on bar age -for (var i = 0; i < numBars; i++) { - var index = (startIndex + i) % MAX_BARS; - var cadence = cadenceHistory[index]; - - // Calculate age factor (0.0 = oldest, 1.0 = newest) - var ageFactor = i / numBars.toFloat(); - - // Map to opacity (50% fade for oldest → 100% for newest) - var minOpacity = 0.5; // Don't fade below 50% - var opacity = minOpacity + (ageFactor * (1.0 - minOpacity)); - - // Get base color - var baseColor = getColorForCadence(cadence); - - // Apply opacity (note: not all Garmin devices support alpha) - // Fallback: lighten color instead - dc.setColor(applyFade(baseColor, opacity), Graphics.COLOR_TRANSPARENT); - dc.fillRectangle(x, y, barWidth, barHeight); -} - -function applyFade(color as Number, opacity as Float) as Number { - // Blend color with background (black) based on opacity - // For devices without alpha channel support - var r = ((color >> 16) & 0xFF) * opacity; - var g = ((color >> 8) & 0xFF) * opacity; - var b = (color & 0xFF) * opacity; - - return ((r.toNumber() << 16) | (g.toNumber() << 8) | b.toNumber()); -} -``` - -**Benefits**: -- Emphasizes recent data (what matters now, i think anyways) -- Creates visual depth/perspective -- Easier to focus on current performance -- More aesthetically pleasing - -**Configuration**: -``` -Fade: Off / Subtle (70-100%) / Medium (50-100%) / Strong (30-100%) -``` - ---- - -#### 4. **Zone Boundary Lines** -**Priority**: Medium -**Complexity**: Low -**Effort**: 30 minutes - -**Description**: Draw horizontal lines at idealMinCadence and idealMaxCadence for immediate visual reference - -**Implementation**: -```monkey-c -// Draw zone boundaries -var minY = barZoneBottom - ((idealMinCadence / MAX_CADENCE_DISPLAY) * chartHeight); -var maxY = barZoneBottom - ((idealMaxCadence / MAX_CADENCE_DISPLAY) * chartHeight); - -// Green dashed lines for zone boundaries -dc.setColor(0x00bf63, Graphics.COLOR_TRANSPARENT); // Green -dc.drawLine(chartLeft, minY, chartRight, minY); -dc.drawLine(chartLeft, maxY, chartRight, maxY); - -// Optional: Fill zone area with semi-transparent green -// (if device supports) -dc.setColor(0x00bf63, 0x20); // Green with alpha -dc.fillRectangle(chartLeft, maxY, chartWidth, minY - maxY); -``` - -**Benefits**: -- Clear visual target zone -- No need to remember numbers while running -- Instant feedback if bars cross boundaries -- Reduces cognitive load - ---- - -### Chart Optimization & Performance - -#### 5. **Reduce Redraw Cost (Battery Optimization)** ⭐⭐⭐ -**Priority**: High -**Complexity**: Medium -**Effort**: 4-6 hours - -**Description**: Implement intelligent redraw strategies to minimize unnecessary screen updates - -**Strategy 1: Dirty Region Tracking** -```monkey-c -private var _lastDrawnCadence = 0; -private var _lastDrawnBarCount = 0; -private var _lastDrawnZone = [0, 0]; // [min, max] - -function needsRedraw() as Boolean { - var currentCadence = getCurrentCadence(); - - // Redraw if: - // 1. New bar added (every ~6 seconds) - if (_cadenceCount != _lastDrawnBarCount) { return true; } - - // 2. Current cadence changed significantly (>2 spm) - if (Math.abs(currentCadence - _lastDrawnCadence) > 2) { return true; } - - // 3. Zone settings changed - if (_idealMinCadence != _lastDrawnZone[0] || - _idealMaxCadence != _lastDrawnZone[1]) { return true; } - - return false; -} - -function onUpdate(dc as Dc) as Void { - if (needsRedraw()) { - View.onUpdate(dc); - drawElements(dc); - - // Update tracking - _lastDrawnCadence = getCurrentCadence(); - _lastDrawnBarCount = _cadenceCount; - _lastDrawnZone = [_idealMinCadence, _idealMaxCadence]; - } -} -``` - -**Strategy 2: Partial Chart Updates** -```monkey-c -// Only redraw new bars, not entire chart -private var _lastRenderedBarIndex = 0; - -function drawNewBarsOnly(dc as Dc) as Void { - // Calculate how many new bars since last draw - var newBars = _cadenceIndex - _lastRenderedBarIndex; - - if (newBars <= 0) { return; } // No new data - - // Set clip region to only new bar area - var newBarX = chartLeft + (_lastRenderedBarIndex * barWidth); - var clipWidth = newBars * barWidth; - - dc.setClip(newBarX, chartTop, clipWidth, chartHeight); - - // Draw only new bars - for (var i = _lastRenderedBarIndex; i < _cadenceIndex; i++) { - drawSingleBar(dc, i); - } - - dc.clearClip(); - _lastRenderedBarIndex = _cadenceIndex; -} -``` - -**Strategy 3: Adaptive Refresh Rate** -```monkey-c -private var _refreshRate = 1000; // Default 1 Hz - -function updateRefreshRate() as Void { - if (_sessionState == PAUSED) { - _refreshRate = 5000; // 0.2 Hz when paused - } else if (_sessionState == STOPPED) { - _refreshRate = 10000; // 0.1 Hz when stopped - } else { - _refreshRate = 1000; // 1 Hz when recording - } - - // Restart timer with new rate - if (_simulationTimer != null) { - _simulationTimer.stop(); - _simulationTimer.start(method(:refreshScreen), _refreshRate, true); - } -} -``` - -**Expected Impact**: -- **10-20% battery improvement** during long activities -- **30-40% reduction** in unnecessary screen updates -- **Smoother performance** on lower-end devices - ---- - -#### 6. **Chart Rendering Optimization** -**Priority**: High -**Complexity**: Medium -**Effort**: 3-4 hours - -**Description**: Optimize the chart drawing loop using cached calculations and efficient rendering - -**Optimization 1: Pre-calculate Bar Positions** -```monkey-c -private var _barPositions as Array = new [MAX_BARS]; - -function precalculateBarPositions() as Void { - var barWidth = (barZoneWidth / MAX_BARS).toNumber(); - - for (var i = 0; i < MAX_BARS; i++) { - var x = barZoneLeft + i * barWidth; - _barPositions[i] = [x, barWidth]; // Store x and width - } -} - -// In drawChart(): -for (var i = 0; i < numBars; i++) { - var x = _barPositions[i][0]; - var barWidth = _barPositions[i][1]; - // ... rest of drawing -} -``` - -**Optimization 2: Color Lookup Table** -```monkey-c -private var _colorCache as Dictionary = {}; - -function getColorCached(cadence as Number) as Number { - var key = cadence.toNumber(); // Round to integer - - if (_colorCache.hasKey(key)) { - return _colorCache[key]; - } - - var color = calculateColor(cadence); - _colorCache.put(key, color); - return color; -} -``` - -**Optimization 3: Batch Drawing Operations** -```monkey-c -// Group bars by color to reduce setColor() calls -var colorGroups = {}; - -for (var i = 0; i < numBars; i++) { - var color = getColor(cadence); - if (!colorGroups.hasKey(color)) { - colorGroups[color] = []; - } - colorGroups[color].add(barData); -} - -// Draw all bars of same color together -foreach (var color in colorGroups.keys()) { - dc.setColor(color, Graphics.COLOR_TRANSPARENT); - foreach (var bar in colorGroups[color]) { - dc.fillRectangle(bar.x, bar.y, bar.width, bar.height); - } -} -``` - -**Impact**: **50-70% reduction** in chart draw time - ---- - -### Advanced Chart Features - -#### 7. **Auto-Adjust Averaging Based on Zone Width** -**Priority**: Low -**Complexity**: Medium -**Effort**: 2-3 hours - -**Description**: Automatically adjust chart duration (samples per bar) based on cadence zone range - -**Logic**: -```monkey-c -function calculateOptimalChartDuration() as Number { - var zoneRange = _idealMaxCadence - _idealMinCadence; - - // Narrow zone → Higher resolution - if (zoneRange <= 5) { - return 3; // 3 sec/bar (high detail for precision) - } - // Normal zone → Default resolution - else if (zoneRange <= 15) { - return 6; // 6 sec/bar (balanced) - } - // Wide zone → Lower resolution (smoother) - else if (zoneRange <= 30) { - return 13; // 13 sec/bar (reduce noise) - } - // Very wide zone → Overview mode - else { - return 26; // 26 sec/bar (big picture) - } -} - -// Call when zone changes: -function onZoneChanged() as Void { - _chartDuration = calculateOptimalChartDuration(); - resizeAveragingBuffer(_chartDuration); -} -``` - -**Benefits**: -- Optimal granularity for any zone width -- Narrow zones (e.g., 148-152) get fine detail -- Wide zones (e.g., 120-160) get smoothed data -- Automatic - no user configuration needed - -**Example**: -- Zone 98-99 (range=1): 3 sec/bar → 280 bars = 14 min history -- Zone 140-155 (range=15): 6 sec/bar → 280 bars = 28 min history -- Zone 100-150 (range=50): 26 sec/bar → 280 bars = 121 min history - ---- - -#### 8. **Statistical Overlays** -**Priority**: Medium -**Complexity**: Medium -**Effort**: 3-4 hours - -**Description**: Display statistical information on chart (mean, median, trend line) - -**Implementation**: -```monkey-c -function calculateStats() as Dictionary { - var sum = 0.0; - var count = 0; - var sortedData = []; - - for (var i = 0; i < _cadenceCount; i++) { - if (_cadenceHistory[i] != null) { - sum += _cadenceHistory[i]; - count++; - sortedData.add(_cadenceHistory[i]); - } - } - - var mean = count > 0 ? sum / count : 0; - - // Calculate median - sortedData = sortData(sortedData); - var median = sortedData[count / 2]; - - // Calculate standard deviation - var variance = 0.0; - for (var i = 0; i < count; i++) { - variance += Math.pow(_cadenceHistory[i] - mean, 2); - } - var stdDev = Math.sqrt(variance / count); - - return { - :mean => mean, - :median => median, - :stdDev => stdDev - }; -} - -// Draw mean line on chart -var stats = calculateStats(); -var meanY = barZoneBottom - ((stats[:mean] / MAX_CADENCE_DISPLAY) * chartHeight); -dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); -dc.drawLine(chartLeft, meanY, chartRight, meanY); // Dashed line - -// Draw std dev band -var stdDevTop = meanY - (stats[:stdDev] / MAX_CADENCE_DISPLAY) * chartHeight; -var stdDevBottom = meanY + (stats[:stdDev] / MAX_CADENCE_DISPLAY) * chartHeight; -dc.setColor(0xFFFFFF, 0x40); // Semi-transparent white -dc.fillRectangle(chartLeft, stdDevTop, chartWidth, stdDevBottom - stdDevTop); -``` - -**Display**: -``` -┌──────────────────┐ -│ ▄▄▄ ▄▄ │ -│ ▄████████ ▄ │ ← Std dev band (±5 spm) -├──────────────────┤ ← Mean line (148 spm) -│ ████████████ │ -└──────────────────┘ - -Text overlay: "Avg: 148±5 spm" -``` - ---- - -### User Experience Enhancements - -#### 9. **Configurable Refresh Rate** -**Priority**: Medium -**Complexity**: Low -**Effort**: 1-2 hours - -**Description**: User setting for screen update frequency to balance responsiveness vs. battery - -**Settings Options**: -``` -Display Refresh Rate: -[ ] Battery Saver (0.5 Hz - every 2 sec) -[✓] Balanced (1 Hz - every 1 sec) [DEFAULT] -[ ] Performance (2 Hz - twice per sec) -``` - -**Implementation**: -```monkey-c -enum RefreshRate { - BATTERY_SAVER = 2000, // 0.5 Hz - BALANCED = 1000, // 1 Hz - PERFORMANCE = 500 // 2 Hz -} - -private var _refreshRate = RefreshRate.BALANCED; - -function setRefreshRate(rate as RefreshRate) as Void { - _refreshRate = rate; - if (_simulationTimer != null) { - _simulationTimer.stop(); - _simulationTimer.start(method(:refreshScreen), rate, true); - } -} -``` - -**Benefits**: -- Ultra-runners can extend battery life -- Interval trainers get higher responsiveness -- User control over performance/battery tradeoff - ---- - -#### 10. **Smart Alerts (Context-Aware)** -**Priority**: Medium -**Complexity**: High -**Effort**: 4-5 hours - -**Description**: Intelligent haptic feedback that considers context - -**Features**: -```monkey-c -// Don't alert during warm-up period -private const WARMUP_DURATION = 300000; // 5 minutes - -// Gradient alert intensity -function triggerCadenceAlert(cadence as Number) as Void { - var elapsed = System.getTimer() - _sessionStartTime; - - // Suppress during warm-up - if (elapsed < WARMUP_DURATION) { return; } - - var deviation = 0; - if (cadence < _idealMinCadence) { - deviation = _idealMinCadence - cadence; - } else if (cadence > _idealMaxCadence) { - deviation = cadence - _idealMaxCadence; - } - - // Intensity based on deviation - if (deviation > 10) { - tripleVibration(); // Urgent - } else if (deviation > 5) { - doubleVibration(); // Warning - } else if (deviation > 0) { - singleVibration(); // Gentle reminder - } -} - -// Consider terrain (if GPS elevation available) -function adjustZoneForTerrain() as Void { - var grade = calculateGrade(); // From GPS - - if (grade > 5) { // Uphill > 5% - _adjustedMin = _idealMinCadence - 5; - _adjustedMax = _idealMaxCadence - 5; - } else if (grade < -5) { // Downhill > 5% - _adjustedMin = _idealMinCadence + 5; - _adjustedMax = _idealMaxCadence + 5; - } -} -``` - ---- - -### Data & Analytics - -#### 11. **Export to CSV** -**Priority**: Low -**Complexity**: Medium -**Effort**: 3-4 hours - -**Description**: Generate CSV file for external analysis - -**Format**: -```csv -Timestamp,Cadence,Zone,HeartRate,Distance,CQ_Score -00:00:06,145,Below,152,0.02,-- -00:00:12,148,In,155,0.05,-- -00:00:18,151,In,158,0.08,67 -... -``` - -**Implementation**: -```monkey-c -function exportToCSV() as String { - var csv = "Timestamp,Cadence,Zone,HeartRate,Distance,CQ_Score\n"; - - for (var i = 0; i < _cadenceCount; i++) { - var time = formatTime(i * _chartDuration); - var cadence = _cadenceHistory[i]; - var zone = getZoneLabel(cadence); - - csv += time + "," + cadence + "," + zone + "," + - getHR(i) + "," + getDist(i) + "," + getCQ(i) + "\n"; - } - - return csv; -} -``` - -**Note**: Garmin devices have limited file I/O. May need to: -- Store in string and copy via Garmin Connect IQ -- Or upload to companion app via Bluetooth - ---- - -### Performance Metrics - -#### 12. **Dynamic Memory Management** -**Priority**: Medium -**Complexity**: High -**Effort**: 5-6 hours - -**Description**: Adapt buffer sizes based on available memory - -**Implementation**: -```monkey-c -function initializeWithMemoryCheck() as Void { - var stats = System.getSystemStats(); - var freeMemory = stats.freeMemory; - var totalMemory = stats.totalMemory; - var usagePercent = (totalMemory - freeMemory) / totalMemory.toFloat(); - - // Conservative if low memory - if (freeMemory < 50000 || usagePercent > 0.75) { - MAX_BARS = 140; // 14 min @ 6 sec/bar - System.println("[MEMORY] Low memory mode: 140 bars"); - } - // Aggressive if plenty of memory - else if (freeMemory > 200000) { - MAX_BARS = 560; // 56 min @ 6 sec/bar - System.println("[MEMORY] Extended mode: 560 bars"); - } - // Standard - else { - MAX_BARS = 280; // 28 min @ 6 sec/bar - } - - _cadenceHistory = new [MAX_BARS]; -} -``` - -**Benefits**: -- Prevents out-of-memory crashes -- Better device compatibility -- Graceful degradation on constrained devices - ---- - -## Implementation Priority Matrix - -### 🔴 High Priority ? Maybe -1. **Current Cadence Marker** -2. **Battery Optimization** -3. **Chart Rendering Optimization** - -### 🟡 Medium Priority -4. **Smooth Bars** -5. **Zone Boundary Lines** -6. **Configurable Refresh Rate** -7. **Statistical Overlays** -8. **Smart Alerts** - -### 🟢 Low Priority -9. **Fade Old Bars** -10. **Auto-Adjust Chart Duration** -11. **CSV Export** -12. **Dynamic Memory** - ---- - -## Technical Debt & Code Quality & other ramblign thoughts - -### Refactoring Needed -- [ ] Extract chart rendering to `ChartRenderer.mc` class -- [ ] Create `CircularBuffer.mc` reusable class -- [ ] Consolidate color constants into `Colors.mc` -- [ ] Add input validation layer for all settings -- [ ] Document all public methods with JSDoc-style comments - -### Testing & Quality -- [ ] Add unit tests for CQ algorithm -- [ ] Add integration tests for state machine -- [ ] Profile memory usage during 2+ hour activities -- [ ] Benchmark chart rendering on FR165 vs FR165 Music -- [ ] Test sensor disconnection recovery - -### Performance Profiling Targets -- [ ] Chart draw time: <50ms per frame -- [ ] Memory usage: <5% of total device memory -- [ ] Battery drain: <5% per hour (GPS active) - ---- - -## Debugging Guide - -### Common Issues - -**Issue**: Timer not pausing -**Cause**: ActivityRecording session not properly controlled -**Solution**: Check `activitySession.stop()` is called on pause - -**Issue**: Cadence data not collecting -**Cause**: State not RECORDING or sensor not connected -**Solution**: Verify `_sessionState == RECORDING` and sensor paired - -**Issue**: CQ always shows "--" -**Cause**: Less than MIN_CQ_SAMPLES (30) collected -**Solution**: Wait 30 seconds after starting, check sensor connection - -**Issue**: Chart not updating -**Cause**: View timer not running or data not flowing -**Solution**: Check `_simulationTimer` started in `onShow()` - -### Debug Checklist - -1. ✓ `DEBUG_MODE = true` in GarminApp.mc -2. ✓ Watch console for `[INFO]`, `[DEBUG]`, `[CADENCE]` messages -3. ✓ Verify state transitions match expected flow -4. ✓ Check `_cadenceCount` increments when recording -5. ✓ Confirm `activitySession != null` when active -6. ✓ Validate sensor pairing in Garmin Connect app - ---- - -## Version History - -**Current Version**: 1.0 (January 2026) - -**Changes from Original**: -- ✓ Fixed: Uncommented critical recording check (line 270) -- ✓ Added: Full state machine (IDLE/RECORDING/PAUSED/STOPPED) -- ✓ Added: Pause/Resume functionality -- ✓ Added: Save/Discard workflow -- ✓ Added: Garmin ActivityRecording integration -- ✓ Added: Menu system for activity control -- ✓ Fixed: Timer now properly pauses/resumes -- ✓ Added: Visual state indicators -- ✓ Added: Comprehensive documentation - -**Known Limitations**: -- No persistent storage of CQ history -- No lap/split functionality -- No custom alert thresholds -- No data export capability -- Haptic feedback placeholder (device-dependent) - ---- - -## Glossary - -**CQ**: Cadence Quality - composite score measuring running efficiency -**FIT File**: Flexible and Interoperable Transfer - Garmin's activity file format -**SPM**: Steps Per Minute - cadence measurement unit -**Circular Buffer**: Fixed-size buffer that wraps when full -**Activity Session**: Garmin's ActivityRecording instance managing timer/sensors -**State Machine**: System that transitions between defined states based on events -**Delegate Pattern**: Separation of input handling from view logic -**MVC**: Model-View-Controller architecture pattern - ---- - -## Credits - -**Application**: Garmin Cadence Monitoring App for Forerunner 165 -**Platform**: Garmin Connect IQ SDK 8.3.0 -**Language**: Monkey C -**Target API**: 5.2.0+ -**Documentation Version**: 1.0 -**Last Updated**: January 2026 -** S -## Special Mentions -**Dom -**Chum -**jack -**Kyle -**Jin - - ---- - - diff --git a/TECHNICAL_DOCUMENTATION_v2.md b/TECHNICAL_DOCUMENTATION_v2.md new file mode 100644 index 0000000..ddecca4 --- /dev/null +++ b/TECHNICAL_DOCUMENTATION_v2.md @@ -0,0 +1,2480 @@ +# Redback Operation Garmin App - Technical Documentation + +## Concept & Vision +A Garmin watch-app that turns the built-in cadence sensor into a **real-time running-efficiency coach that can be used for rehabilitation or hard core trainers alike**. +While you run it shows: + +* Live cadence vs. your **personal ideal zone** (calculated from height, speed, gender, experience) +* A **28-minute rolling histogram** that colour-codes every stride +* A **0-100 % "Cadence Quality" (CQ)** score that is frozen when you stop and is written into the FIT file so it follows the activity into Garmin Connect +* **Smart haptic alerts** that vibrate when you drift out of your optimal cadence zone + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Build Process](#build-process) +3. [GitHub Workflow & Collaboration](#github-workflow--collaboration) +4. [Architecture Overview](#architecture-overview) +5. [Core Components](#core-components) +6. [Data Flow](#data-flow) +7. [State Management](#state-management) +8. [Activity Recording System](#activity-recording-system) +9. [Haptic Feedback System](#haptic-feedback-system) +10. [Cadence Quality Algorithm](#cadence-quality-algorithm) +11. [User Interface](#user-interface) +12. [Settings System](#settings-system) +13. [Documentation Reference](#documentation-reference) +14. [Features Reference](#features-reference) + +--- + +## Prerequisites +- Garmin Connect IQ SDK 8.3.0+ +- Visual Studio Code with Connect IQ extension +- Forerunner 165/165 Music device or simulator + +## Build Process +1. Clone repository +2. Configure project settings in `monkey.jungle` +3. Build for target device: + ```bash + monkeyc -o bin/app.prg -f monkey.jungle -y developer_key.der + **You need to have generated a devopler key for this** + ``` + +--- + +## GitHub Workflow & Collaboration + +**Note:** This workflow represents one approach to Git collaboration. Other valid philosophies exist (Git Flow, Trunk-Based Development, etc.). This document is written based on professional development experience and some parts may not directly apply to this specific project, but should help build understanding of collaborative Git workflows. + +### My Philosophy: "Main is Sacred, Feature Branches are Disposable" + +This project follows a **rebase-focused, fast-merge workflow** designed to keep the repository history clean and minimize merge conflicts. This approach is particularly beneficial for teams new to Git and GitHub, as it creates a linear, easy-to-understand history while preventing the dreaded "merge hell." + +### Core Principles + +| Principle | Meaning | Why It Matters | +|-----------|---------|----------------| +| Main is always green | If it's on `main`, it works and deploys | Broken code never reaches production | +| Rebase, don't merge | Linear history = readable git log | Easy to track down bugs, understand changes | +| Small, fast PRs | Less than 400 lines, less than 3 days open | Easier reviews, less conflict potential | +| Automated enforcement | Machines check; humans review logic | Catches issues before review | +| Sync early, sync often | Rebase on `main` daily | Prevents large, scary conflicts | + +### Why Rebase? The Visual Comparison + +**MERGE (creates complex history):** +``` +A---B---C-------F---G main + \ / + D---E---/ feature + (creates merge commit bubble) +``` + +**REBASE (clean linear history):** +``` +A---B---C---F---G---D'---E' main + (feature commits replayed on top) +``` + +**Benefits of Rebasing:** +- History reads like a book: chronological, no tangles +- `git bisect` works reliably to find bugs +- Each commit can be individually tested +- Code reviews focus on logic, not merge artifacts + +--- + +## Daily Branch Synchronization (Preventing "Out-of-Date" Branches) + +### The Problem Scenario + +```mermaid +sequenceDiagram + participant You + participant YourBranch as feature/vibration + participant Main + participant Teammate + + Note over You,Main: Monday 9am + You->>YourBranch: Create branch from main@100 + + Note over You,Main: Tuesday 3pm + Teammate->>Main: Merge feature/graph-fix (main@105) + + Note over You,Main: Wednesday 10am + You->>YourBranch: Open Pull Request + YourBranch-->>Main: "Warning: Branch is out-of-date" + + Note over You,Main: Thursday 2pm + Teammate->>Main: Merge feature/settings (main@110) + + Note over You,Main: Friday 11am + You->>Main: Attempt merge + Note over Main: Conflicts! Tests fail! Team sad! +``` + +### The Solution: (My) Daily Rebase Ritual + +**Run this every morning** (or after getting coffee): + +```bash / powershell +# Step 1: Fetch latest changes from GitHub +# (Downloads new commits without modifying your files) +git fetch origin + +# Step 2: Optional - See what changed on main +# (Shows commits that have been added since you branched) +git log --oneline --graph origin/main..HEAD + +# Step 3: Save any uncommitted work +# (Rebase requires a clean working directory) +git stash push -m "WIP: morning sync $(date +%Y-%m-%d)" + +# Step 4: Replay your commits on top of latest main +# (This is the key step - makes your branch current) +git rebase origin/main + +# Step 5: Handle conflicts (if any appear) +# Git will pause and show conflicting files +# Edit the files, then: +git add +git rebase --continue + +# If you get stuck or panic: +# git rebase --abort # Returns to pre-rebase state + +# Step 6: Restore your uncommitted work +git stash pop + +# Step 7: Update your remote branch +# (--force-with-lease is safer than --force) +git push --force-with-lease +``` + +### When to Sync Your Branch + +| Timing | Action | Command | +|--------|--------|---------| +| **Start work** | Branch from latest main | `git fetch && git checkout -b feature/name origin/main` | +| **Daily** (minimum) | Rebase on main | `git fetch && git rebase origin/main && git push --force-with-lease` | +| **Before opening PR** | Final rebase + cleanup | `git rebase -i origin/main` (squash "WIP" commits if needed) | +| **After PR approved** | Last sync before merge | `git fetch && git rebase origin/main` | +| **After PR merged** | Delete branch | `git push origin --delete feature/name && git branch -D feature/name` | + +--- + +## Common Situations & Solutions + +### Situation 1: GitHub Shows "This branch is out-of-date" + +**Don't click "Update branch" button** - it creates a merge commit! + +**Instead, use terminal:** +```bash +git fetch origin +git rebase origin/main +git push --force-with-lease +``` +Then refresh the PR page - warning disappears. + +### Situation 2: Someone Else Pushed to Your Branch + +If `--force-with-lease` fails with "rejected", someone else modified your branch: + +```bash +# Fetch their changes +git fetch origin + +# Rebase on your remote branch first (get their work) +git rebase origin/feature/your-branch + +# Then rebase on main +git rebase origin/main + +# Communicate with teammate before force-pushing! +git push --force-with-lease +``` + +### Situation 3: Rebase Goes Wrong +**I have done this more times than i care to remember** + +If you make a mistake during rebase: + +```bash +# Abort and return to pre-rebase state +git rebase --abort + +# Check the reflog to see history +git reflog + +# Jump back to a specific commit if needed +git reset --hard HEAD@{5} # Numbers from reflog +``` + +--- + +## Branch Protection Rules +**some of these are active on the bramch** + +Our repository enforces these rules on the `main` branch (Settings → Branches): + +| Setting | Value | Why | +|---------|-------|-----| +| **Require pull request** | Enabled | Prevents accidental `git push origin main` | +| **Required approvals** | 1 reviewer | Ensures code review before merge | +| **Dismiss stale approvals** | Enabled | New commits after approval require re-review | +| **Require status checks** | CI must pass | Code must build and pass tests | +| **Require branches up to date** | **Critical** | PR must include latest `main` commits before merge | +| **Allow force pushes** | Disabled | Protects main's history | +| **Allow deletions** | Disabled | Prevents accidental branch deletion | + +**Why "Require branches up to date" is critical:** +This forces you to rebase before merging, ensuring the final merge result was actually tested in CI. Without this, two "green" PRs can be merged sequentially and break `main`. + +--- + +## Branch Naming Conventions +**this is my preffered naming convention - i try an incorparte naming conventions into everything, even university submissions, so instead of SIT782_5_4HDV3r5edit8 i have a meaning name. + +Use these prefixes to keep branches organized: + +| Prefix | Purpose | Example | +|--------|---------|---------| +| `feature/` | New functionality | `feature/vibration-alerts` | +| `fix/` | Bug fixes | `fix/timer-crash` | +| `refactor/` | Code improvement (no behavior change) | `refactor/extract-chart-renderer` | +| `docs/` | Documentation only | `docs/api-examples` | +| `hotfix/` | Urgent production fix | `hotfix/memory-leak` | + +**Format:** `prefix/descriptive-name-in-kebab-case` + +**Examples:** +- Good: `feature/haptic-feedback` +- Good: `fix/null-pointer-crash` +- Bad: `my-branch` (no prefix) +- Bad: `Feature/VibrationStuff` (wrong case) + +--- + +## Pull Request Best Practices + +### PR Size Guidelines + +| Lines Changed | Status | Recommendation | +|---------------|--------|----------------| +| Less than 200 lines | Excellent | Ideal for fast review | +| 200-400 lines | Large | Consider splitting | +| 400+ lines | Too large | Must split into multiple PRs | + +**Why small PRs are better-ish:** +- Faster reviews +- Lower chance of conflicts +- Easier to understand changes +- Less likely to introduce bugs + +### PR Template +**Does my head in when there is no comments or something generic like 'added stuff", admittedly its easier but its a crappy mindset because the person reviewing has no idea. + +When opening a PR, include: + +```markdown +## What +Brief description of changes (1-2 sentences) + +## Why +Problem being solved or feature being added + +## How +Technical approach taken + +## Testing +How to verify these changes work + +## Screenshots/Videos +If UI changes, show before/after -- not always relevant +``` + +### Review Process + +1. **Open PR** when code is ready for review (not draft) +2. **Respond to feedback** within 24 hours +3. **Keep PR updated** - rebase when main moves forward +4. **Squash "fix review comments" commits** before final merge +5. **Delete branch** immediately after merge + +--- + +## Continuous Integration (CI) + +Every PR automatically runs: + +```mermaid +graph LR + A[Push to PR] --> B[Lint Check] + B --> C[Type Check] + C --> D[Unit Tests] + D --> E[Integration Tests] + E --> F[Build] + F --> G{All Pass?} + G -->|Yes| H[Ready to Merge] + G -->|No| I[Fix and Push Again] +``` + +**CI must pass before merge button activates.** + +If CI fails: +1. Check the logs in the "Checks" tab +2. Fix the issue locally +3. Commit and push +4. CI runs automatically again + +--- + +## Merge Strategy + +We use **Squash and Merge** for all PRs: +**this is what i like, there is more than one way to skin this cat, find what works for your project.** + +**Benefits:** +- Each PR becomes a single commit on `main` +- Clean, readable history +- Easy to revert entire features +- Encourages frequent commits during development + +**Process:** +1. PR gets approved and CI passes +2. Click "Squash and merge" +3. Edit commit message (auto-generated from PR) +4. GitHub automatically deletes the branch + +--- + +## Alternative Workflows (For Reference ONly) + +While we use rebase-focused workflow, other valid approaches exist: + +### Git Flow +- Uses `develop` branch as integration branch +- `main` only for releases +- More complex, better for teams with scheduled releases + +### Trunk-Based Development +- Everyone commits to `main` frequently +- Heavy use of feature flags +- Requires strong CI/CD and testing discipline + +### GitHub Flow +- Similar to our approach +- Allows merge commits instead of rebase +- Simpler but creates non-linear history + +**Why we chose rebase workflow:** +Balances simplicity (better than Git Flow) with code quality (cleaner than merge-heavy approaches). Ideal for learning teams transitioning from solo to collaborative development. + +--- + +## Quick Reference Guide + +**Starting a new feature:** +```bash +git fetch origin +git checkout -b feature/my-feature origin/main +``` + +**Daily sync:** +```bash +git fetch origin +git stash +git rebase origin/main +git stash pop +git push --force-with-lease +``` + +**Opening a PR:** +1. Push branch: `git push -u origin feature/my-feature` +2. Open PR on GitHub +3. Request review +4. Respond to feedback + +**After PR merged:** +```bash +git checkout main +git pull +git branch -D feature/my-feature +``` + +--- + +## Getting Help + +**Stuck during rebase?** +1. Don't panic -- i cant stress this enough.. see below. +2. Run `git status` to see what's happening +3. Ask in team chat with: + - Output of `git status` + - What you were trying to do + - Current branch name +4. DONT Panic. Everyone screws up and makes mistakes. If you fuck up, fix it, have an RCA and move on. I have bunged code and production systems over the years. FUck up, fix it, move on. +**Common commands for troubleshooting:** +```bash +git status # See current state +git log --oneline -10 # Recent commits +git reflog # History of HEAD movements +git diff # Uncommitted changes +``` + +**Remember:** Git is forgiving, and i cannot stress this enough - almost nothing is truly lost. We can always recover from mistakes with `reflog`. + +--- + +## Workflow Checklist + +Before starting work: +- [ ] `git fetch origin` +- [ ] `git checkout -b feature/name origin/main` + +During development: +- [ ] Commit frequently with clear messages +- [ ] Rebase on `main` daily +- [ ] Keep PR less than 400 lines if possible + +Before opening PR: +- [ ] Final rebase: `git rebase -i origin/main` +- [ ] Squash WIP commits if needed +- [ ] Run tests locally +- [ ] Push: `git push -u origin feature/name` + +During review: +- [ ] Respond to feedback within 24h +- [ ] Keep branch updated with `main` +- [ ] Address all review comments + +After merge: +- [ ] Delete branch locally: `git branch -D feature/name` +- [ ] Pull latest main: `git checkout main && git pull` +- [ ] Celebrate! + +--- + +*This workflow is designed to minimize conflicts and maximize collaboration. When in doubt, communicate early with the team and sync your branch often!* + +--- + + + + +## Architecture Overview + +### Application Type +- **Type**: Garmin Watch App (not data field or widget) +- **Target Devices**: Forerunner 165, Forerunner 165 Music +- **SDK Version**: Minimum API Level 5.2.0 +- **Architecture**: MVC (Model-View-Controller/Delegate pattern) + +### High-Level Structure +``` +GarminApp (Application Core) + ├── Views + │ ├── SimpleView (Main activity view + haptic alerts) + │ └── AdvancedView (Chart visualization + haptic alerts) + ├── Delegates (Input handlers) + │ ├── SimpleViewDelegate (Main controls) + │ ├── AdvancedViewDelegate (Chart controls) + │ └── Settings Delegates (Configuration) + ├── Managers + │ ├── SensorManager (Cadence sensor) + │ └── Logger (Memory tracking) + └── Data Processing + ├── Cadence Quality Calculator + ├── Activity Recording Session + └── Haptic Alert Manager +``` + +### Component Interaction Flow + +```mermaid +graph TB + A[User] -->|Input| B[ViewDelegate] + B -->|Commands| C[GarminApp] + C -->|State Changes| D[View] + D -->|Display| A + + E[Cadence Sensor] -->|Data| C + C -->|Process| F[CQ Algorithm] + C -->|Monitor| G[Haptic Manager] + G -->|Vibrations| H[Watch Hardware] + + C -->|Records| I[ActivitySession] + I -->|Saves| J[FIT File] + J -->|Syncs| K[Garmin Connect] + + style G fill:#ff9999 + style H fill:#ff9999 +``` + +--- + +## Core Components + +### 1. GarminApp.mc +**Purpose**: Central application controller and data manager + +**Key Responsibilities**: +- Activity session lifecycle management (start/pause/resume/stop/save/discard) +- Cadence data collection and storage +- Cadence quality score computation +- State machine management +- Timer management +- Integration with Garmin Activity Recording API + +#### 1a. Memory Footprint (Cold Numbers) +* Static allocation: ≈ 2.8 kB + * 280 cadence samples × 4 B = 1.1 kB + * 280 EMA smoothed values = 1.1 kB + * 10 CQ history = 40 B + * Misc buffers / state ≈ 600 B +* Peak stack during draw: ≈ 400 B +* Total at run-time: < 3.5 kB → fits easily into the 32 kB heap of the FR165 + +**Important Constants**: +```monkey-c +MAX_BARS = 280 // Maximum cadence samples to store +BASELINE_AVG_CADENCE = 160 // Minimum acceptable cadence +MAX_CADENCE = 190 // Maximum cadence for calculations +MIN_CQ_SAMPLES = 30 // Minimum samples for CQ calculation +DEBUG_MODE = true // Enable debug logging +``` + +**State Variables**: +- `_sessionState`: Current session state (IDLE/RECORDING/PAUSED/STOPPED) +- `activitySession`: Garmin ActivityRecording session object +- `_cadenceHistory`: Circular buffer storing 280 cadence samples +- `_cadenceBarAvg`: Rolling average buffer for chart display +- `_cqHistory`: Last 10 CQ scores for trend analysis + +### 2. SimpleView.mc & AdvancedView.mc +**Purpose**: Display interfaces with integrated haptic feedback + +**SimpleView Responsibilities**: +- Display current cadence, heart rate, distance, time +- Show cadence zone status (In Zone/Out Zone) +- Trigger haptic alerts when out of zone +- Update UI every second + +**AdvancedView Responsibilities**: +- Render 28-minute cadence histogram +- Display heart rate and distance circles +- Show zone boundaries on chart +- Trigger haptic alerts when out of zone +- Color-code bars based on cadence zones + +**Haptic Alert Variables** (both views): +```monkey-c +private var _lastZoneState = 0; // -1=below, 0=in zone, 1=above +private var _alertStartTime = null; // When alerts began +private var _alertDuration = 180000; // 3 minutes in milliseconds +private var _alertInterval = 30000; // 30 seconds between alerts +private var _lastAlertTime = 0; // Last alert timestamp +private var _pendingSecondVibe = false; // Double-buzz tracking +private var _secondVibeTime = 0; // When second buzz should fire +``` + +--- + +## Data Flow + +### 1. Cadence Data Collection Pipeline + +```mermaid +graph TD + A[Cadence Sensor] -->|Raw Data| B[Activity.getActivityInfo] + B -->|currentCadence| C[updateCadenceBarAvg] + C -->|Every 1s| D[_cadenceBarAvg Buffer] + D -->|Buffer Full| E[Calculate Average] + E --> F[updateCadenceHistory] + F --> G[_cadenceHistory 280 samples] + G --> H[computeCadenceQualityScore] + H --> I[_cqHistory Last 10 scores] + + G -->|Monitor| J[Zone Detection] + J -->|Out of Zone| K[Trigger Haptic Alert] + K --> L[User Feedback] + + style K fill:#ff9999 + style L fill:#ff9999 +``` + +### 2. Haptic Alert Data Flow + +```mermaid +sequenceDiagram + participant User + participant View + participant ZoneChecker + participant HapticManager + participant Hardware + + User->>View: Running (cadence changes) + View->>ZoneChecker: Check current cadence + + alt Cadence drops below minimum + ZoneChecker->>HapticManager: Trigger single buzz pattern + HapticManager->>Hardware: Vibrate 200ms + HapticManager->>HapticManager: Start 30s timer + Note over HapticManager: Repeat every 30s for 3 min + else Cadence exceeds maximum + ZoneChecker->>HapticManager: Trigger double buzz pattern + HapticManager->>Hardware: Vibrate 200ms + HapticManager->>Hardware: Wait 240ms + HapticManager->>Hardware: Vibrate 200ms + HapticManager->>HapticManager: Start 30s timer + Note over HapticManager: Repeat every 30s for 3 min + else Returns to zone + ZoneChecker->>HapticManager: Stop alerts + HapticManager->>HapticManager: Cancel timer + end +``` + +### 3. Timer System + +**Global Timer** (`globalTimer`): +- Frequency: Every 1 second +- Callback: `updateCadenceBarAvg()` +- Runs: Always (from app start to stop) +- Purpose: Collect cadence data when recording + +**View Refresh Timers**: +- SimpleView: Refresh every 1 second (reused for haptic checks) +- AdvancedView: Refresh every 1 second (reused for haptic checks) +- Purpose: Update UI elements and monitor zone status + +**Haptic Alert System** (NO dedicated timers): +- Uses existing view refresh cycle +- Checks zone status on each UI update +- Triggers vibrations when appropriate +- No additional timer overhead + +### 4. Data Averaging System + +The app uses a two-tier averaging system: + +**Tier 1: Bar Averaging** +``` +Chart Duration = 6 seconds (ThirtyminChart default) +↓ +Collect 6 cadence readings (1 per second) +↓ +Calculate average of these 6 readings +↓ +Store as single bar value +``` + +**Tier 2: Historical Storage** +``` +280 bar values stored +↓ +Each bar = average of 6 seconds +↓ +Total history = 280 × 6 = 1680 seconds = 28 minutes +``` + +**Chart Duration Options**: +- FifteenminChart = 3 seconds per bar +- ThirtyminChart = 6 seconds per bar (default) +- OneHourChart = 13 seconds per bar +- TwoHourChart = 26 seconds per bar + +### 4a. Sensor Manager Abstraction +`SensorManager.mc` decouples real vs. simulated cadence: + +```monkey-c +useSimulator = true → returns hard-coded value (for desk testing) +useSimulator = false → reads Activity.getActivityInfo().currentCadence +``` + +--- + +## State Management + +### Session State Machine + +```mermaid +stateDiagram-v2 + [*] --> IDLE: App Start + IDLE --> RECORDING: startRecording() + RECORDING --> PAUSED: pauseRecording() + PAUSED --> RECORDING: resumeRecording() + RECORDING --> STOPPED: stopRecording() + PAUSED --> STOPPED: stopRecording() + STOPPED --> IDLE: saveSession() / discardSession() + + note right of RECORDING + - Timer active + - Data collecting + - Haptic alerts enabled + - UI updating + end note + + note right of PAUSED + - Timer stopped + - Data frozen + - Haptic alerts disabled + - UI static + end note + + note right of STOPPED + - Final CQ calculated + - Haptic alerts disabled + - Awaiting user decision + end note +``` + +### State Transition Rules + +**IDLE → RECORDING**: +- User presses START/STOP button +- Creates new ActivityRecording session +- Starts Garmin timer +- Resets all cadence data arrays +- Initializes timestamps +- **Enables haptic zone monitoring** + +**RECORDING → PAUSED**: +- User selects "Pause" from menu +- Stops Garmin timer (timer pauses) +- Records pause timestamp +- Data collection stops +- **Disables haptic alerts** + +**PAUSED → RECORDING**: +- User selects "Resume" from menu +- Restarts Garmin timer +- Accumulates paused time +- Data collection resumes +- **Re-enables haptic zone monitoring** + +**RECORDING/PAUSED → STOPPED**: +- User selects "Stop" from menu +- Stops Garmin timer +- Computes final CQ score +- Freezes all metrics +- **Disables haptic alerts** +- Awaits save/discard decision + +**STOPPED → IDLE**: +- User selects "Save": Saves to FIT file +- User selects "Discard": Deletes session +- Resets all data structures +- Ready for new session + +--- + +## Activity Recording System + +### Garmin ActivityRecording Integration + +**Session Creation** (`startRecording()`): +```monkey-c +activitySession = ActivityRecording.createSession({ + :name => "Running", + :sport => ActivityRecording.SPORT_RUNNING, + :subSport => ActivityRecording.SUB_SPORT_GENERIC +}); +activitySession.start(); +``` + +**What This Does**: +- Creates official Garmin activity +- Starts timer (visible in UI) +- Records GPS, heart rate, cadence automatically +- Manages distance calculation +- Handles sensor data collection + +**Pause/Resume** (`pauseRecording()` / `resumeRecording()`): +```monkey-c +// Pause +activitySession.stop(); // Pauses timer + +// Resume +activitySession.start(); // Resumes timer +``` + +**Save** (`saveSession()`): +```monkey-c +activitySession.save(); +``` +- Writes FIT file to device +- Syncs to Garmin Connect +- Appears in activity history +- Includes all sensor data + +**Discard** (`discardSession()`): +```monkey-c +activitySession.discard(); +``` +- Deletes session completely +- No FIT file created +- No sync to Garmin Connect + +--- + +## Haptic Feedback System + +### Overview +The haptic feedback system provides real-time tactile alerts when the runner's cadence drifts outside their optimal zone. This helps maintain proper running form without constantly looking at the watch. + +### Design Philosophy +**Timer-Free Architecture**: Instead of creating additional timers (which are limited on Garmin devices), the system piggybacks on the existing 1-second view refresh cycle. This approach: +- Eliminates "Too Many Timers" errors +- Reduces memory overhead +- Maintains precise timing through timestamp tracking +- Seamlessly integrates with existing UI updates + +### Alert Patterns + +```mermaid +graph LR + A[Zone Detection] --> B{Cadence Status?} + B -->|Below Min| C[Single Buzz] + B -->|Above Max| D[Double Buzz] + B -->|In Zone| E[No Alert] + + C --> F[200ms vibration] + D --> G[200ms vibration] + G --> H[240ms pause] + H --> I[200ms vibration] + + F --> J[Repeat every 30s for 3 min] + I --> J + + style C fill:#9999ff + style D fill:#ff9999 + style E fill:#99ff99 +``` + +**Single Buzz** (Below Minimum Cadence): +- Pattern: One 200ms vibration +- Meaning: Speed up your steps +- Repeat: Every 30 seconds +- Duration: 3 minutes max + +**Double Buzz** (Above Maximum Cadence): +- Pattern: Two 200ms vibrations with 240ms gap +- Meaning: Slow down your steps +- Repeat: Every 30 seconds +- Duration: 3 minutes max + +**No Alert** (In Target Zone): +- Pattern: Silence +- Meaning: Perfect cadence, keep going! + +### Implementation Details + +#### Zone State Tracking +```monkey-c +private var _lastZoneState = 0; // -1 = below, 0 = in zone, 1 = above + +// Determine current zone +if (cadence < minZone) { + newZoneState = -1; // Below minimum +} else if (cadence > maxZone) { + newZoneState = 1; // Above maximum +} else { + newZoneState = 0; // In target zone +} +``` + +#### Alert Triggering Logic +```monkey-c +if (newZoneState != _lastZoneState) { + if (newZoneState == -1) { + // Just dropped below minimum + triggerSingleVibration(); + startAlertCycle(); + } else if (newZoneState == 1) { + // Just exceeded maximum + triggerDoubleVibration(); + startAlertCycle(); + } else { + // Returned to zone + stopAlertCycle(); + } + _lastZoneState = newZoneState; +} +``` + +#### Alert Cycle Management +```monkey-c +function startAlertCycle() as Void { + _alertStartTime = System.getTimer(); + _lastAlertTime = System.getTimer(); + // Initial alert already fired +} + +function checkAndTriggerAlerts() as Void { + if (_alertStartTime == null) { return; } + + var currentTime = System.getTimer(); + var elapsed = currentTime - _alertStartTime; + + // Stop after 3 minutes + if (elapsed >= 180000) { + _alertStartTime = null; + return; + } + + // Check if 30 seconds passed since last alert + var timeSinceLastAlert = currentTime - _lastAlertTime; + if (timeSinceLastAlert >= 30000) { + _lastAlertTime = currentTime; + + if (_lastZoneState == -1) { + triggerSingleVibration(); + } else if (_lastZoneState == 1) { + triggerDoubleVibration(); + } + } +} +``` + +#### Double Buzz Implementation +```monkey-c +function triggerDoubleVibration() as Void { + if (Attention has :vibrate) { + // First vibration + var vibeData = [new Attention.VibeProfile(50, 200)]; + Attention.vibrate(vibeData); + + // Schedule second vibration + _pendingSecondVibe = true; + _secondVibeTime = System.getTimer() + 240; + } +} + +function checkPendingVibration() as Void { + if (_pendingSecondVibe) { + var currentTime = System.getTimer(); + if (currentTime >= _secondVibeTime) { + var vibeData = [new Attention.VibeProfile(50, 200)]; + Attention.vibrate(vibeData); + _pendingSecondVibe = false; + } + } +} +``` + +### Integration with Views + +Both `SimpleView` and `AdvancedView` include identical haptic feedback implementations: + +```mermaid +graph TD + A[onUpdate Called] --> B[displayCadence / drawElements] + B --> C[Get current cadence] + C --> D[Determine zone state] + D --> E{State changed?} + E -->|Yes| F[Trigger appropriate alert] + E -->|No| G[Check if alert needed] + F --> H[Start/stop alert cycle] + G --> H + H --> I[checkPendingVibration] + I --> J[Continue UI update] +``` + +**SimpleView Integration**: +```monkey-c +function displayCadence() as Void { + // ... update UI elements ... + + // Determine zone state + var newZoneState = 0; + if (currentCadence < minZone) { + newZoneState = -1; + } else if (currentCadence > maxZone) { + newZoneState = 1; + } + + // Handle zone transitions + if (newZoneState != _lastZoneState) { + // Trigger appropriate alert and start cycle + } else { + // Check if periodic alert needed + checkAndTriggerAlerts(); + } +} +``` + +**AdvancedView Integration**: +```monkey-c +function checkCadenceZone() as Void { + // Get activity info and determine zone + // Same logic as SimpleView + // Integrated with chart rendering +} +``` + +### Timing Accuracy + +The system achieves accurate 30-second intervals through timestamp comparison: + +``` +Initial Alert: T = 0s [BUZZ] +Check at: T = 1s (29s remaining - no alert) +Check at: T = 2s (28s remaining - no alert) +... +Check at: T = 30s (0s remaining - BUZZ!) +Check at: T = 31s (new cycle starts) +``` + +Actual timing variance: ±1 second (due to 1Hz refresh rate) + +### Memory Overhead + +**Additional Memory per View**: +- State tracking: 3 integers (12 bytes) +- Timestamps: 3 longs (24 bytes) +- Boolean flags: 1 boolean (1 byte) +- **Total: ~40 bytes per view** + +**No Additional Timers Required**: +- Reuses existing `_refreshTimer` (SimpleView) +- Reuses existing `_simulationTimer` (AdvancedView) +- Zero timer creation overhead + +### User Experience Flow + +```mermaid +sequenceDiagram + participant Runner + participant Watch + participant App + + Runner->>Watch: Start activity + Watch->>App: BEGIN RECORDING + + Note over App: Monitoring cadence... + + Runner->>Watch: Cadence drops to 115 SPM + App->>Watch: [SINGLE BUZZ] + Note over Watch: Below minimum alert + + Note over App: Wait 30 seconds... + + App->>Watch: [SINGLE BUZZ] + Note over Watch: Still below minimum + + Runner->>Watch: Increases cadence to 145 SPM + Note over App: Back in zone - stop alerts + + Runner->>Watch: Cadence spikes to 165 SPM + App->>Watch: [DOUBLE BUZZ] + Note over Watch: Above maximum alert + + Runner->>Watch: Reduces cadence to 150 SPM + Note over App: Back in zone - stop alerts +``` + +### Haptic Feedback Best Practices + +**For Developers**: +1. Always check `Attention has :vibrate` before calling vibration +2. Reuse existing timers rather than creating new ones +3. Use timestamp-based tracking for precise intervals +4. Clean up state in `onHide()` to prevent orphaned alerts +5. Test thoroughly with rapid zone transitions + +### Future Enhancements + +Potential improvements to the haptic system: + +1. **Configurable Alert Patterns** + - User-selectable vibration duration + - Custom interval timing (15s, 45s, 60s) + - Triple-buzz for extreme deviations + +2. **Progressive Alert Intensity** + - Gentle buzz for minor deviations (±3 SPM) + - Strong buzz for major deviations (±10 SPM) + - Requires multi-pattern vibration support + +3. **Smart Alert Suppression** + - Disable during warm-up (first 5 minutes) + - Pause alerts on steep hills (using GPS grade) + - Adaptive zones based on fatigue detection + +4. **Audio Cues** (device-dependent) + - Combine vibration with tones + - Voice feedback for major transitions + - Requires audio hardware support + +--- + +## Cadence Quality Algorithm + +### Overview +The Cadence Quality (CQ) score is a composite metric that measures running efficiency based on two factors: + +1. **Time in Zone** (70% weight): Percentage of time spent within ideal cadence range +2. **Smoothness** (30% weight): Consistency of cadence over time + +### Algorithm Flow + +```mermaid +graph TD + A[Collect Cadence Sample] --> B{Min 30 samples?} + B -->|No| C[Display: CQ --] + B -->|Yes| D[Calculate Time in Zone] + D --> E[Calculate Smoothness] + E --> F[Compute Weighted Average] + F --> G[CQ Score 0-100%] + G --> H[Store in History] + H --> I[Update Display] +``` + +### Time in Zone Calculation + +**Purpose**: Measures what percentage of your running time is spent at the optimal cadence + +**Formula**: +``` +Time in Zone % = (samples in zone / total samples) × 100 +``` + +**Implementation**: +```monkey-c +function computeTimeInZoneScore() as Number { + if (_cadenceCount < MIN_CQ_SAMPLES) { + return -1; // Not enough data yet + } + + 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(); +} +``` + +**Example**: +- 280 total samples collected +- 210 samples within zone [145-155 SPM] +- Time in Zone = (210/280) × 100 = 75% + +### Smoothness Calculation + +**Purpose**: Measures cadence consistency (low variance = better form) + +**Formula**: +``` +Average Difference = Σ |current - previous| / number of transitions +Smoothness % = 100 - (average difference × 10) +``` + +**Implementation**: +```monkey-c +function computeSmoothnessScore() as Number { + if (_cadenceCount < MIN_CQ_SAMPLES) { + return -1; + } + + 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++; + } + } + + if (diffCount == 0) { + return -1; + } + + var avgDiff = totalDiff / diffCount; + var rawScore = 100 - (avgDiff * 10); + + // Clamp to 0-100 range + if (rawScore < 0) { rawScore = 0; } + if (rawScore > 100) { rawScore = 100; } + + return rawScore; +} +``` + +**Example**: +- Sample transitions: 145→148 (3), 148→147 (1), 147→150 (3) +- Average difference = (3+1+3)/3 = 2.33 +- Smoothness = 100 - (2.33 × 10) = 76.7% + +### Final CQ Score + +**Weighted Combination**: +``` +CQ = (Time in Zone × 0.7) + (Smoothness × 0.3) +``` + +**Implementation**: +```monkey-c +function computeCadenceQualityScore() as Number { + var timeInZone = computeTimeInZoneScore(); + var smoothness = computeSmoothnessScore(); + + if (timeInZone < 0 || smoothness < 0) { + return -1; // Not enough data + } + + var cq = (timeInZone * 0.7) + (smoothness * 0.3); + return cq.toNumber(); +} +``` + +**Example**: +- Time in Zone = 75% +- Smoothness = 76.7% +- CQ = (75 × 0.7) + (76.7 × 0.3) = 52.5 + 23.01 = **75.5%** + +### CQ Score Interpretation + +| Score Range | Rating | Interpretation | +|-------------|--------|----------------| +| 90-100% | Excellent | Elite running form | +| 80-89% | Very Good | Consistent optimal cadence | +| 70-79% | Good | Generally on target | +| 60-69% | Fair | Room for improvement | +| 50-59% | Poor | Frequent zone violations | +| 0-49% | Very Poor | Needs significant work | + +### Confidence Calculation + +**Purpose**: Indicates reliability of CQ score based on missing data + +```monkey-c +function computeCQConfidence() as String { + if (_cadenceCount < MIN_CQ_SAMPLES) { + return "Low"; + } + + var missingRatio = _missingCadenceCount.toFloat() / + (_cadenceCount + _missingCadenceCount).toFloat(); + + if (missingRatio > 0.2) { + return "Low"; // >20% missing data + } else if (missingRatio > 0.1) { + return "Medium"; // 10-20% missing + } else { + return "High"; // <10% missing + } +} +``` + +### Trend Analysis + +**Purpose**: Shows if cadence quality is improving, stable, or declining + +```monkey-c +function computeCQTrend() as String { + if (_cqHistory.size() < 5) { + return "Insufficient data"; + } + + // Compare recent half vs. older half + var midpoint = _cqHistory.size() / 2; + var olderAvg = 0.0; + var recentAvg = 0.0; + + for (var i = 0; i < midpoint; i++) { + olderAvg += _cqHistory[i]; + } + olderAvg /= midpoint; + + for (var i = midpoint; i < _cqHistory.size(); i++) { + recentAvg += _cqHistory[i]; + } + recentAvg /= (_cqHistory.size() - midpoint); + + var diff = recentAvg - olderAvg; + + if (diff > 5) { + return "Improving"; + } else if (diff < -5) { + return "Declining"; + } else { + return "Stable"; + } +} +``` + +### CQ Storage in FIT File + +When the activity is stopped, the final CQ score is frozen and written to the FIT file: + +```monkey-c +function stopRecording() as Void { + // ... stop activity session ... + + 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(); + } + + _sessionState = STOPPED; +} +``` + +This frozen CQ score: +- Appears in the activity summary +- Syncs to Garmin Connect +- Provides historical tracking +- Can be compared across runs + +--- + +## User Interface + +### SimpleView (Main Display) + +**Layout**: +``` + ┌─────────────────────┐ + │ [REC] 00:12:34 │ ← Time + Recording Indicator + ├─────────────────────┤ + │ ❤ │ │ ⚡ │ + │ 152 │ 148 │ 180 │ ← Heart Rate, Cadence, Steps + ├─────────────────────┤ + │ In Zone (145-155) │ ← Zone Status + ├─────────────────────┤ + │ 2.45 km │ ← Distance + ├─────────────────────┤ + │ CQ: 75% │ ← Cadence Quality + └─────────────────────┘ +``` + +**Color Coding**: +- **Green**: Cadence in optimal zone +- **Blue**: Slightly below zone (within threshold) +- **Grey**: Well below zone +- **Orange**: Slightly above zone (within threshold) +- **Red**: Well above zone + +**Haptic Feedback Integration**: +- Single buzz when cadence drops below minimum +- Double buzz when cadence exceeds maximum +- Repeats every 30 seconds if still out of zone +- Automatically stops after 3 minutes or when returning to zone + +### AdvancedView (Chart Display) + +**Layout**: +``` + ┌─────────────────────┐ + │ 1:23:45 │ ← Session Time + ├──────┬─────────┬────┤ + │ ❤ │ │ 🏃 │ + │ 152 │ │2.4 │ ← HR Circle + Distance Circle + ├──────┴─────────┴────┤ + │ 148 spm │ ← Current Cadence + ├─────────────────────┤ + │ ▂▅▇█▆▅▃▂▃▄▅▆▇█▆▄▃ │ ← 28-min Histogram + │ │ + ├─────────────────────┤ + │ Zone: 145-155 spm │ ← Zone Range + └─────────────────────┘ +``` + +**Chart Features**: +- 280 bars representing 28 minutes of data +- Fixed vertical scale (0-200 SPM) +- Color-coded bars matching zone status +- Real-time updates every second +- Smooth scrolling as new data arrives + +**Haptic Feedback Integration**: +- Same alert patterns as SimpleView +- Integrated with chart updates +- Visual + tactile feedback for optimal learning + +### Navigation + +```mermaid +graph TD + A[SimpleView] -->|Swipe Up / Press Down| B[AdvancedView] + B -->|Swipe Down / Press Up| A + A -->|Swipe Left / Press Up| C[Settings] + B -->|Swipe Left| C + C -->|Back| A + A -->|Press Select| D{Activity State?} + D -->|IDLE| E[Start Recording] + D -->|RECORDING| F[Activity Menu] + D -->|PAUSED| G[Paused Menu] + D -->|STOPPED| H[Save/Discard Menu] +``` + +**Button Mapping**: +- **SELECT**: Start/Stop activity or open control menu +- **UP**: Navigate to settings or previous view +- **DOWN**: Navigate to next view +- **BACK**: Exit menus (disabled during active session) +- **MENU**: Open cadence zone settings + +### Activity Control Menus + +**During Recording**: +``` +┌──────────────────────┐ +│ Activity │ +├──────────────────────┤ +│ > Resume │ +│ > Pause │ +│ > Stop │ +└──────────────────────┘ +``` + +**When Paused**: +``` +┌──────────────────────┐ +│ Activity Paused │ +├──────────────────────┤ +│ > Resume │ +│ > Stop │ +└──────────────────────┘ +``` + +**After Stopping**: +``` +┌──────────────────────┐ +│ Save Activity? │ +├──────────────────────┤ +│ > Save │ +│ > Discard │ +└──────────────────────┘ +``` + +### Settings Menu + +``` +┌──────────────────────┐ +│ Settings │ +├──────────────────────┤ +│ > Profile │ +│ > Customization │ +│ > Feedback │ +│ > Cadence Range │ +└──────────────────────┘ +``` + +**Profile Settings**: +- Height (cm) +- Speed (km/h) +- Gender (Male/Female/Other) +- Experience Level (Beginner/Intermediate/Advanced) + +**Customization**: +- Chart Duration (15min/30min/1hr/2hr) + +**Feedback** (Future): +- Haptic intensity +- Alert interval +- Alert duration + +**Cadence Range**: +- Set Min Cadence (manual adjustment) +- Set Max Cadence (manual adjustment) + +--- + +## Settings System + +### User Profile Configuration + +**Purpose**: Calculate personalized ideal cadence based on biomechanics + +**Formula** (from research): +``` +Reference Cadence = (-1.251 × leg_length) + (3.665 × speed_m/s) + 254.858 +Final Cadence = Reference × Experience_Factor +``` + +**Gender-Specific Adjustments**: +```monkey-c +function idealCadenceCalculator() as Void { + var referenceCadence = 0; + var userLegLength = _userHeight * 0.53; // 53% of height + var userSpeedms = _userSpeed / 3.6; // Convert km/h to m/s + + switch (_userGender) { + case Male: + referenceCadence = (-1.268 × userLegLength) + + (3.471 × userSpeedms) + 261.378; + break; + case Female: + referenceCadence = (-1.190 × userLegLength) + + (3.705 × userSpeedms) + 249.688; + break; + default: + referenceCadence = (-1.251 × userLegLength) + + (3.665 × userSpeedms) + 254.858; + break; + } + + referenceCadence *= _experienceLvl; + referenceCadence = Math.round(referenceCadence); + + var finalCadence = max(BASELINE_AVG_CADENCE, + min(referenceCadence, MAX_CADENCE)); + + _idealMaxCadence = finalCadence + 5; + _idealMinCadence = finalCadence - 5; +} +``` + +**Experience Level Multipliers**: +- Beginner: 1.06 (higher cadence for learning) +- Intermediate: 1.04 (moderate adjustment) +- Advanced: 1.02 (minimal adjustment) + +### Persistent Storage + +**Storage Keys**: +```monkey-c +const PROP_USER_HEIGHT = "user_height"; +const PROP_USER_SPEED = "user_speed"; +const PROP_USER_GENDER = "user_gender"; +const PROP_EXPERIENCE_LVL = "experience_level"; +const PROP_CHART_DURATION = "chart_duration"; +const PROP_MIN_CADENCE = "min_cadence"; +const PROP_MAX_CADENCE = "max_cadence"; +``` + +**Save Settings**: +```monkey-c +function saveSettings() as Void { + Storage.setValue(PROP_USER_HEIGHT, _userHeight); + Storage.setValue(PROP_USER_SPEED, _userSpeed); + Storage.setValue(PROP_USER_GENDER, _userGender); + Storage.setValue(PROP_EXPERIENCE_LVL, _experienceLvl); + Storage.setValue(PROP_CHART_DURATION, _chartDuration); + Storage.setValue(PROP_MIN_CADENCE, _idealMinCadence); + Storage.setValue(PROP_MAX_CADENCE, _idealMaxCadence); +} +``` + +**Load Settings**: +```monkey-c +function loadSettings() as Void { + var height = Storage.getValue(PROP_USER_HEIGHT); + if (height != null) { + _userHeight = height as Number; + } + // ... load other settings ... +} +``` + +Settings are automatically: +- Loaded on app start +- Saved when modified +- Persisted between sessions +- Restored after watch reboot + +--- + +## Documentation Reference + +--- + + +This reference covers the formatting used throughout this documentation. Use it when contributing updates or creating new documentation. markdown can be +intimidating at first, but onve you master it, you will use it for everything. + +--- + +## Markdown Basics + +### Headers + +```markdown +# H1 - Main Title +## H2 - Major Section +### H3 - Subsection +#### H4 - Minor Heading +``` + +**Usage in this doc:** +- H1: Document title only +- H2: Major sections (Architecture, Core Components, etc.) +- H3: Subsections within major sections +- H4: Rarely used, for very specific details + +--- + +### Text Formatting + +```markdown +**Bold text** for emphasis +*Italic text* for subtle emphasis +`Inline code` for commands, variables, filenames +~~Strikethrough~~ for deprecated content +``` + +**Examples:** +- **Bold**: Important terms, warnings +- *Italic*: Notes, asides +- `Code`: `git push`, `_cadenceHistory`, `SimpleView.mc` + +--- + +### Links + +```markdown +[Link text](https://example.com) +[Internal link](#section-name) +[Link with title](https://example.com "Hover text") +``` + +**Internal link rules:** +- Section names become anchors automatically +- Convert to lowercase +- Replace spaces with hyphens +- Remove special characters or convert to hyphens + +**Examples:** +```markdown +[Architecture Overview](#architecture-overview) # Correct +[GitHub Workflow & Collaboration](#github-workflow--collaboration) # & becomes -- +[State Management](#state-management) # Simple case +``` + +--- + +### Lists + +**Unordered lists:** +```markdown +- Item 1 +- Item 2 + - Nested item 2a + - Nested item 2b +- Item 3 +``` + +**Ordered lists:** +```markdown +1. First item +2. Second item +3. Third item +``` + +**Checklists:** +```markdown +- [ ] Incomplete task +- [x] Completed task +``` + +--- + +### Code Blocks + +**Inline code:** +```markdown +Use `git status` to check your working directory. +``` + +**Fenced code blocks with syntax highlighting:** + +````markdown +```bash +git fetch origin +git rebase origin/main +``` + +```monkey-c +function initialize() { + View.initialize(); +} +``` + +```javascript +const response = await fetch(url); +``` +```` + +**Supported languages in this doc:** +- `bash` - Shell commands +- `monkey-c` - Monkey C code +- `javascript` - JS examples +- `yaml` - GitHub Actions workflows +- `markdown` - Markdown examples +- No language tag - Plain text + +--- + +### Tables + +**Basic table:** +```markdown +| Column 1 | Column 2 | Column 3 | +|----------|----------|----------| +| Data 1 | Data 2 | Data 3 | +| Data 4 | Data 5 | Data 6 | +``` + +**Table with alignment:** +```markdown +| Left-aligned | Center-aligned | Right-aligned | +|:-------------|:--------------:|--------------:| +| Left | Center | Right | +``` + +**Tips:** +- Use `|:---` for left align (default) +- Use `|:---:|` for center align +- Use `|---:|` for right align +- Don't worry about perfect spacing - Markdown handles it + +--- + +### Blockquotes + +```markdown +> This is a blockquote +> It can span multiple lines +> +> And include multiple paragraphs +``` + +**Usage:** Notes, warnings, important callouts + +--- + +### Horizontal Rules + +```markdown +--- +``` + +**Usage:** Separate major sections (used throughout this doc) + +--- + +## Mermaid Diagrams + +Mermaid creates diagrams from text. All diagrams must be in fenced code blocks with `mermaid` language tag. + +### Flowcharts (Graph) + +**Basic syntax:** +````markdown +```mermaid +graph TD + A[Start] --> B[Process] + B --> C{Decision?} + C -->|Yes| D[Action 1] + C -->|No| E[Action 2] + D --> F[End] + E --> F +``` +```` + +**Node shapes:** +```markdown +A[Rectangle] # Square corners +B(Rounded) # Rounded corners +C([Stadium]) # Pill shape +D[[Subroutine]] # Double border +E[(Database)] # Cylinder +F((Circle)) # Circle +G>Flag] # Flag shape +H{Diamond} # Diamond (decision) +I{{Hexagon}} # Hexagon +``` + +**Arrow types:** +```markdown +A --> B # Solid arrow +A -.-> B # Dotted arrow +A ==> B # Thick arrow +A --- B # Line (no arrow) +A -- Text --> B # Labeled arrow +A -->|Text| B # Labeled arrow (compact) +``` + +**Direction:** +```markdown +graph TD # Top to Down +graph LR # Left to Right +graph BT # Bottom to Top +graph RL # Right to Left +``` + +**Example from this doc (Data Flow):** +````markdown +```mermaid +graph TD + A[Cadence Sensor] -->|Raw Data| B[Activity.getActivityInfo] + B -->|currentCadence| C[updateCadenceBarAvg] + C -->|Every 1s| D[_cadenceBarAvg Buffer] + D -->|Buffer Full| E[Calculate Average] + E --> F[updateCadenceHistory] +``` +```` + +--- + +### Sequence Diagrams + +**Basic syntax:** +````markdown +```mermaid +sequenceDiagram + participant A as Alice + participant B as Bob + + A->>B: Hello Bob! + B->>A: Hi Alice! + + Note over A,B: This is a note + Note right of A: Note on right + Note left of B: Note on left +``` +```` + +**Arrow types:** +```markdown +A->>B: Solid arrow (message) +A-->>B: Dotted arrow (return) +A-xB: Cross (lost message) +``` + +**Special syntax:** +```markdown +alt Alternative 1 + A->>B: Do this +else Alternative 2 + A->>B: Do that +end + +loop Every 30s + A->>B: Repeat this +end +``` + +**Example from this doc (Haptic Alerts):** +````markdown +```mermaid +sequenceDiagram + participant User + participant View + participant ZoneChecker + participant HapticManager + + User->>View: Running (cadence changes) + View->>ZoneChecker: Check current cadence + + alt Cadence drops below minimum + ZoneChecker->>HapticManager: Trigger single buzz + else Cadence exceeds maximum + ZoneChecker->>HapticManager: Trigger double buzz + else Returns to zone + ZoneChecker->>HapticManager: Stop alerts + end +``` +```` + +--- + +### State Diagrams + +**Basic syntax:** +````markdown +```mermaid +stateDiagram-v2 + [*] --> State1 + State1 --> State2: Transition + State2 --> State3: Another transition + State3 --> [*] + + note right of State1 + This is a note + end note +``` +```` + +**Example from this doc (Session States):** +````markdown +```mermaid +stateDiagram-v2 + [*] --> IDLE: App Start + IDLE --> RECORDING: startRecording() + RECORDING --> PAUSED: pauseRecording() + PAUSED --> RECORDING: resumeRecording() + RECORDING --> STOPPED: stopRecording() + STOPPED --> IDLE: saveSession() / discardSession() + + note right of RECORDING + Timer active + Data collecting + Haptic alerts enabled + end note +``` +```` + +--- + +## Documentation Best Practices + +### When to Use Which Diagram + +| Diagram Type | Use When | Example in This Doc | +|--------------|----------|---------------------| +| **Flowchart** | Showing process flow, data pipeline, decision trees | Data collection pipeline, CI workflow | +| **Sequence Diagram** | Showing interactions over time between components | Haptic alert timing, merge conflict scenario | +| **State Diagram** | Showing state transitions and lifecycle | Session state machine | + +### Formatting Guidelines + +**DO:** +- Use consistent header levels (don't skip levels) +- Include code language tags in fenced blocks +- Use tables for structured data comparisons +- Add horizontal rules between major sections +- Keep lines under 120 characters when possible +- Use relative links for internal references + +**DON'T:** +- Use HTML unless absolutely necessary +- Skip header levels (H2 → H4 without H3) +- Use images for text content (accessibility) +- Hard-code line breaks (let Markdown handle wrapping) +- Use bare URLs (always use link syntax) + +### Code Block Guidelines + +**For commands:** +```bash +# Good: Show full command with context +git fetch origin +git rebase origin/main +git push --force-with-lease + +# Bad: No context, unclear +git push -f +``` + +**For code:** +```monkey-c +// Good: Include relevant context and comments +function startRecording() as Void { + // Create Garmin activity session + activitySession = ActivityRecording.createSession({ + :name => "Running", + :sport => ActivityRecording.SPORT_RUNNING + }); +} + +// Bad: No context or explanation +activitySession.start(); +``` + +### Table Guidelines + +**Comparison tables:** +- Left column: Item being compared +- Other columns: Attributes or options +- Use bold for headers + +**Decision tables:** +- Left column: Condition/situation +- Right columns: Action or outcome +- Consider using "When/Action/Command" structure + +**Examples:** +```markdown +| When | Action | Command | +|------|--------|---------| +| Start work | Branch from latest main | `git checkout -b feature/name` | +``` + +--- + +## Mermaid Troubleshooting + +### Common Issues + +**Diagram not rendering?** +1. Check for typos in `mermaid` tag +2. Ensure proper indentation +3. Close all parentheses and brackets +4. Check for special characters in node names + +**Arrows not connecting?** +- Make sure node IDs match exactly (case-sensitive) +- Check arrow syntax (`-->` not `->`) + +**Text not showing?** +- Wrap text with spaces in quotes: `A["My Text"]` +- Use `|Text|` for inline labels on arrows + +### Testing Diagrams + +**Before committing:** +1. View in GitHub's preview tab +2. Or use online editor: https://mermaid.live +3. Check that text is readable +4. Verify all arrows point correctly + +--- + +## Contributing to Documentation + +### Before You Edit + +1. Read through existing docs to understand style +2. Check this reference for syntax +3. Test Mermaid diagrams in preview + +### Making Changes + +1. Create branch: `git checkout -b docs/your-update` +2. Edit markdown files +3. Preview changes locally or on GitHub +4. Commit with clear message: "docs: update haptic feedback section" +5. Open PR with description of changes + +### Review Checklist + +- [ ] Headers follow hierarchy (no skipped levels) +- [ ] Links work (test internal anchors) +- [ ] Code blocks have language tags +- [ ] Tables align properly +- [ ] Mermaid diagrams render correctly +- [ ] No spelling errors in headers or links +- [ ] Follows existing document style + +--- + +## Quick Reference: Common Patterns + +### Section Header Pattern +```markdown +--- + +## Section Name + +Brief introduction paragraph explaining what this section covers. + +### Subsection + +Content here... + +**Key Points:** +- Point 1 +- Point 2 +- Point 3 +``` + +### Code Example Pattern +```markdown +**Implementation:** +```monkey-c +function example() as Void { + // Explanation comment + var result = doSomething(); +} +``` + +**What this does:** +- Explains the code +- Provides context +``` + +### Comparison Table Pattern +```markdown +| Feature | Option A | Option B | +|---------|----------|----------| +| Speed | Fast | Slow | +| Memory | High | Low | +| Complexity | Simple | Complex | +``` + +--- + +--- + + + + + + + + +## Features Reference + +### Current Features (v1.0) + +✅ **Core Functionality** +- Real-time cadence monitoring +- 28-minute rolling histogram +- Cadence Quality (CQ) scoring +- Activity recording to FIT file +- Pause/Resume functionality +- Save/Discard workflow + +✅ **User Interface** +- SimpleView (main display) +- AdvancedView (chart visualization) +- Settings menus +- Activity control menus +- Recording indicator + +✅ **Smart Features** +- Personalized cadence zones +- Gender-specific calculations +- Experience level adjustment +- Color-coded zone feedback +- CQ trend analysis +- **Haptic zone alerts** + +✅ **Data Management** +- Circular buffer storage +- Two-tier averaging system +- Persistent settings +- FIT file integration +- Memory optimization + +### Haptic Feedback Feature (v1.1) + +✅ **Alert Patterns** +- Single buzz for below-zone cadence +- Double buzz for above-zone cadence +- 30-second repeat interval +- 3-minute maximum duration +- Automatic stop on zone re-entry + +✅ **Technical Implementation** +- Timer-free architecture +- Timestamp-based tracking +- Integrated with view refresh cycle +- No additional memory overhead +- Works on both SimpleView and AdvancedView + +✅ **User Benefits** +- No need to constantly watch screen +- Tactile feedback during runs +- Non-intrusive alerts +- Customizable zone ranges +- Improves form awareness + +### Future Enhancements + +#### 🔴 High Priority +1. **Configurable Alert Settings** + - Alert interval (15s/30s/45s/60s) + - Alert duration (1min/3min/5min/continuous) + - Vibration intensity (light/medium/strong) + +2. **Battery Optimization** + - Adaptive refresh rate based on battery level + - Low-power mode during steady-state running + - Smart sensor polling + +3. **Chart Rendering Optimization** + - Reduce draw calls + - Cache static elements + - Optimize bar calculations + +#### 🟡 Medium Priority +4. **Smooth Bars** + - Gradient transitions between zones + - Anti-aliased rendering + - Sub-pixel accuracy + +5. **Zone Boundary Lines** + - Visual indicators on chart + - Min/max cadence markers + - Target zone highlighting + +6. **Statistical Overlays** + - Average line + - Standard deviation bands + - Trend line + +7. **Terrain-Adaptive Zones** + - Adjust zones for hills (using GPS elevation) + - Compensate for terrain difficulty + - Smart zone boundaries + +#### 🟢 Low Priority +8. **Fade Old Bars** + - Opacity gradient for time perspective + - Highlight recent data + - Visual age indication + +9. **Auto-Adjust Chart Duration** + - Extend duration for long runs + - Compress for short workouts + - Dynamic time window + +10. **CSV Export** + - Export cadence history + - Include all metrics + - Bluetooth transfer to phone + +11. **Dynamic Memory Management** + - Adapt buffer sizes to available memory + - Graceful degradation on low memory + - Device-specific optimization + +12. **Night Mode** + - Auto-detect sunrise/sunset + - Red/orange color palette + - Preserve dark adaptation + +13. **Progressive Alert Intensity** + - Gentle buzz for minor deviations + - Strong buzz for major deviations + - Gradient feedback system + +--- + +## Implementation Priority Matrix + +### Phase 1: Core Stability (Completed) +- ✅ State machine +- ✅ Activity recording +- ✅ Pause/Resume +- ✅ Save/Discard +- ✅ Basic haptic alerts + +### Phase 2: User Customization (Current) +- 🔄 Configurable alert settings +- 🔄 Battery optimization +- 🔄 Chart rendering optimization + +### Phase 3: Advanced Features (Future) +- 📋 Smooth bars +- 📋 Zone boundary lines +- 📋 Statistical overlays +- 📋 Terrain-adaptive zones + +### Phase 4: Polish & Enhancement (Future) +- 📋 Fade old bars +- 📋 Auto-adjust chart duration +- 📋 CSV export +- 📋 Dynamic memory management +- 📋 Night mode +- 📋 Progressive alert intensity + +--- + +## Technical Debt & Code Quality + +### Refactoring Needed +- [ ] Extract chart rendering to `ChartRenderer.mc` class +- [ ] Create `CircularBuffer.mc` reusable class +- [ ] Consolidate color constants into `Colors.mc` +- [ ] Create `HapticManager.mc` for centralized vibration control +- [ ] Add input validation layer for all settings +- [ ] Document all public methods with JSDoc-style comments + +### Testing & Quality +- [ ] Add unit tests for CQ algorithm +- [ ] Add integration tests for state machine +- [ ] Add haptic feedback timing tests +- [ ] Profile memory usage during 2+ hour activities +- [ ] Benchmark chart rendering on FR165 vs FR165 Music +- [ ] Test sensor disconnection recovery +- [ ] Test haptic alerts across rapid zone transitions + +### Performance Profiling Targets +- [ ] Chart draw time: <50ms per frame +- [ ] Memory usage: <5% of total device memory +- [ ] Battery drain: <5% per hour (GPS active) +- [ ] Haptic timing accuracy: ±1 second + +--- + +## Debugging Guide + +### Common Issues + +**Issue**: Haptic alerts not firing +**Cause**: Attention module not supported or state not RECORDING +**Solution**: +- Check `Attention has :vibrate` capability +- Verify `_sessionState == RECORDING` +- Confirm cadence is actually out of zone + +**Issue**: Alerts continue after returning to zone +**Cause**: Zone state not properly updated +**Solution**: +- Check `_lastZoneState` variable +- Verify zone detection logic +- Ensure `stopAlertCycle()` is called + +**Issue**: Double buzz only fires once +**Cause**: `_pendingSecondVibe` not being checked +**Solution**: +- Confirm `checkPendingVibration()` called in `onUpdate()` +- Verify `_secondVibeTime` calculation +- Check timer precision + +**Issue**: Timer not pausing +**Cause**: ActivityRecording session not properly controlled +**Solution**: Check `activitySession.stop()` is called on pause + +**Issue**: Cadence data not collecting +**Cause**: State not RECORDING or sensor not connected +**Solution**: Verify `_sessionState == RECORDING` and sensor paired + +**Issue**: CQ always shows "--" +**Cause**: Less than MIN_CQ_SAMPLES (30) collected +**Solution**: Wait 30 seconds after starting, check sensor connection + +**Issue**: Chart not updating +**Cause**: View timer not running or data not flowing +**Solution**: Check `_simulationTimer` started in `onShow()` + +### Debug Checklist + +1. ✓ `DEBUG_MODE = true` in GarminApp.mc +2. ✓ Watch console for `[INFO]`, `[DEBUG]`, `[CADENCE]` messages +3. ✓ Verify state transitions match expected flow +4. ✓ Check `_cadenceCount` increments when recording +5. ✓ Confirm `activitySession != null` when active +6. ✓ Validate sensor pairing in Garmin Connect app +7. ✓ Monitor `_lastZoneState` for zone transitions +8. ✓ Verify haptic timing with stopwatch +9. ✓ Check `_alertStartTime` and `_lastAlertTime` values + +### Haptic Debugging + +**Enable Haptic Debug Logging**: +```monkey-c +// In triggerSingleVibration() +System.println("[HAPTIC] Single buzz triggered at " + System.getTimer()); + +// In triggerDoubleVibration() +System.println("[HAPTIC] Double buzz triggered at " + System.getTimer()); + +// In checkAndTriggerAlerts() +System.println("[HAPTIC] Time since last alert: " + timeSinceLastAlert); +``` + +**Test Haptic Timing**: +1. Start recording +2. Manually set cadence out of zone +3. Note timestamp of first alert +4. Wait 30 seconds +5. Verify second alert timing +6. Repeat for full 3-minute cycle + +--- + +## Version History + +**Current Version**: 1.1 (January 2026) + +**v1.1 Changes**: +- ✅ Added: Haptic feedback system + - Single buzz for below-zone cadence + - Double buzz for above-zone cadence + - 30-second repeat interval + - 3-minute maximum duration + - Timer-free implementation +- ✅ Fixed: Timer creation overhead +- ✅ Added: Zone state tracking +- ✅ Improved: Memory efficiency +- ✅ Updated: Documentation with flow diagrams + +**v1.0 Changes** (from original): +- ✅ Fixed: Uncommented critical recording check (line 270) +- ✅ Added: Full state machine (IDLE/RECORDING/PAUSED/STOPPED) +- ✅ Added: Pause/Resume functionality +- ✅ Added: Save/Discard workflow +- ✅ Added: Garmin ActivityRecording integration +- ✅ Added: Menu system for activity control +- ✅ Fixed: Timer now properly pauses/resumes +- ✅ Added: Visual state indicators +- ✅ Added: Comprehensive documentation + +**Known Limitations**: +- No persistent storage of CQ history +- No lap/split functionality +- No custom alert thresholds +- No data export capability +- Haptic intensity not configurable +- No terrain-adaptive zones + +--- + +## Glossary + +**CQ**: Cadence Quality - composite score measuring running efficiency +**FIT File**: Flexible and Interoperable Transfer - Garmin's activity file format +**SPM**: Steps Per Minute - cadence measurement unit +**Circular Buffer**: Fixed-size buffer that wraps when full +**Activity Session**: Garmin's ActivityRecording instance managing timer/sensors +**State Machine**: System that transitions between defined states based on events +**Delegate Pattern**: Separation of input handling from view logic +**MVC**: Model-View-Controller architecture pattern +**Haptic Feedback**: Tactile vibration alerts +**Zone State**: Current cadence position relative to target range (-1/0/1) +**Alert Cycle**: Period of repeated haptic alerts (3 minutes maximum) +**Timer-Free**: Architecture using timestamps instead of dedicated timers + +--- + +## Other Info + +**Application**: Garmin Cadence Monitoring App for Forerunner 165 +**Platform**: Garmin Connect IQ SDK 8.3.0 +**Language**: Monkey C +**Target API**: 5.2.0+ +**Documentation Version**: 2.0 +**Last Updated**: January 2026 + +## Special Mentions for their amazing work this semester. +**Dom** +**Chum** +**Jack** +**Kyle** +**Jin** + +--- + + diff --git a/source/Delegates/SettingsDelegates/CustomizableDelegates/SelectBarChartDelegate.mc b/source/Delegates/SettingsDelegates/CustomizableDelegates/SelectBarChartDelegate.mc deleted file mode 100644 index d26d760..0000000 --- a/source/Delegates/SettingsDelegates/CustomizableDelegates/SelectBarChartDelegate.mc +++ /dev/null @@ -1,50 +0,0 @@ -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectBarChartDelegate extends WatchUi.Menu2InputDelegate { - - private var _menu as WatchUi.Menu2; - var app = getApp(); - var chartDuration = app.getChartDuration(); - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - _menu = menu; - var newTitle = Lang.format("Chart: $1$", [chartDuration]); - - // This updates the UI when the chart duration is changed - _menu.setTitle(newTitle); - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Try to change cadence range based off menu selection - if (id == :chart_15m){ - app.setChartDuration(GarminApp.FifteenminChart); - } - else if (id == :chart_30m){ - app.setChartDuration(GarminApp.ThirtyminChart); - } - else if (id == :chart_1h){ - app.setChartDuration(GarminApp.OneHourChart); - } - else if (id == :chart_2h){ - app.setChartDuration(GarminApp.TwoHourChart); - } - else {System.println("ERROR");} - - WatchUi.popView(WatchUi.SLIDE_RIGHT); - - } - - function onMenuItem(item as Symbol) as Void {} - - //returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} \ No newline at end of file diff --git a/source/Delegates/SettingsDelegates/CustomizableDelegates/SelectCustomizableDelegate.mc b/source/Delegates/SettingsDelegates/CustomizableDelegates/SelectCustomizableDelegate.mc deleted file mode 100644 index 345a3b5..0000000 --- a/source/Delegates/SettingsDelegates/CustomizableDelegates/SelectCustomizableDelegate.mc +++ /dev/null @@ -1,47 +0,0 @@ -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectCutomizableDelegate extends WatchUi.Menu2InputDelegate { - - //private var _menu as WatchUi.Menu2; - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - //_menu = menu; - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Add if more customizable options are added - if (id == :cust_bar_chart){ - pushBarChartMenu(); - } - else {System.println("ERROR");} - - } - - function pushBarChartMenu() as Void { - var menu = new WatchUi.Menu2({ - :title => "Bar Chart Length:" - }); - - menu.addItem(new WatchUi.MenuItem("15 Minute", null, :chart_15m, null)); - menu.addItem(new WatchUi.MenuItem("30 Minute", null, :chart_30m, null)); - menu.addItem(new WatchUi.MenuItem("1 Hour", null, :chart_1h, null)); - menu.addItem(new WatchUi.MenuItem("2 Hour", null, :chart_2h, null)); - - //pushes the view to the screen with the relevent delegate - WatchUi.pushView(menu, new SelectBarChartDelegate(menu), WatchUi.SLIDE_LEFT); - } - - function onMenuItem(item as Symbol) as Void {} - - //returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} \ No newline at end of file diff --git a/source/Delegates/SettingsDelegates/FeedbackDelegates/SelectAudibleDelegate.mc b/source/Delegates/SettingsDelegates/FeedbackDelegates/SelectAudibleDelegate.mc deleted file mode 100644 index 68d5708..0000000 --- a/source/Delegates/SettingsDelegates/FeedbackDelegates/SelectAudibleDelegate.mc +++ /dev/null @@ -1,50 +0,0 @@ -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectAudibleDelegate extends WatchUi.Menu2InputDelegate { - - private var _menu as WatchUi.Menu2; - var app = Application.getApp() as GarminApp; - //var Audible = app.getAudible(); - var Audible = "low";// make sure to change to above!! - after feature has been added - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - _menu = menu; - - var newTitle = Lang.format("Audible: $1$", [Audible]); - - // This updates the UI when the cadence is changed - _menu.setTitle(newTitle); - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Try to change cadence range based off menu selection - if (id == :audible_low){ - System.println("Audible Feedback: LOW"); - //app.setAudible("low"); - } - else if (id == :audible_med){ - System.println("Audible Feedback: MEDIUM"); - //app.setUserAudible("med"); - } - else if (id == :audible_high){ - System.println("Audible Feedback: HIGH"); - //app.setUserAudible("high"); - } else {System.println("ERROR");} - - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } - - function onMenuItem(item as Symbol) as Void {} - - // Returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} \ No newline at end of file diff --git a/source/Delegates/SettingsDelegates/FeedbackDelegates/SelectFeedbackDelegate.mc b/source/Delegates/SettingsDelegates/FeedbackDelegates/SelectFeedbackDelegate.mc deleted file mode 100644 index 50515b9..0000000 --- a/source/Delegates/SettingsDelegates/FeedbackDelegates/SelectFeedbackDelegate.mc +++ /dev/null @@ -1,66 +0,0 @@ -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectFeedbackDelegate extends WatchUi.Menu2InputDelegate { - - //private var _menu as WatchUi.Menu2; - var app = Application.getApp() as GarminApp; - //var experienceLvl = app.getUserGender(); - var gender = "Other";// make sure to change to above!! - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - //_menu = menu; - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Try to change cadence range based off menu selection - if (id == :haptic_feedback){ - System.println("Haptic menu selected"); - pushHapticSettings(); - } - else if (id == :audible_feedback){ - System.println("Audible menu selected"); - pushAudibleSettings(); - } else {System.println("ERROR");} - - } - - function pushHapticSettings() as Void{ - var menu = new WatchUi.Menu2({ - :title => "Haptic Settings" - }); - //temp items since feedback has not yet been implemented - menu.addItem(new WatchUi.MenuItem("Low", null, :haptic_low, null)); - menu.addItem(new WatchUi.MenuItem("Medium", null, :haptic_med, null)); - menu.addItem(new WatchUi.MenuItem("High", null, :haptic_high, null)); - - //pushes the view to the screen with the relevent delegate - WatchUi.pushView(menu, new SelectHapticDelegate(menu), WatchUi.SLIDE_LEFT); - } - - function pushAudibleSettings() as Void{ - var menu = new WatchUi.Menu2({ - :title => "Audible Settings" - }); - - menu.addItem(new WatchUi.MenuItem("Low", null, :audible_low, null)); - menu.addItem(new WatchUi.MenuItem("Medium", null, :audible_med, null)); - menu.addItem(new WatchUi.MenuItem("High", null, :audible_high, null)); - - //pushes the view to the screen with the relevent delegate - WatchUi.pushView(menu, new SelectAudibleDelegate(menu), WatchUi.SLIDE_LEFT); - } - - function onMenuItem(item as Symbol) as Void {} - - // Returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} \ No newline at end of file diff --git a/source/Delegates/SettingsDelegates/FeedbackDelegates/SelectHapticDelegate.mc b/source/Delegates/SettingsDelegates/FeedbackDelegates/SelectHapticDelegate.mc deleted file mode 100644 index d701071..0000000 --- a/source/Delegates/SettingsDelegates/FeedbackDelegates/SelectHapticDelegate.mc +++ /dev/null @@ -1,50 +0,0 @@ -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectHapticDelegate extends WatchUi.Menu2InputDelegate { - - private var _menu as WatchUi.Menu2; - var app = Application.getApp() as GarminApp; - //var haptic = app.getHaptic(); - var haptic = "low";// make sure to change to above!! - after feature has been added - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - _menu = menu; - - var newTitle = Lang.format("Haptic: $1$", [haptic]); - - // This updates the UI when the cadence is changed - _menu.setTitle(newTitle); - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Try to change cadence range based off menu selection - if (id == :haptic_low){ - System.println("Haptic Feedback: LOW"); - //app.setHaptic("low"); - } - else if (id == :haptic_med){ - System.println("Haptic Feedback: MEDIUM"); - //app.setUserHaptic("med"); - } - else if (id == :haptic_high){ - System.println("Haptic Feedback: HIGH"); - //app.setUserHaptic("high"); - } else {System.println("ERROR");} - - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } - - function onMenuItem(item as Symbol) as Void {} - - // Returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} \ No newline at end of file diff --git a/source/Delegates/SettingsDelegates/ProfileDelegates/ProfilePickerDelegate.mc b/source/Delegates/SettingsDelegates/ProfileDelegates/ProfilePickerDelegate.mc deleted file mode 100644 index 5cf3f9f..0000000 --- a/source/Delegates/SettingsDelegates/ProfileDelegates/ProfilePickerDelegate.mc +++ /dev/null @@ -1,39 +0,0 @@ -import Toybox.WatchUi; -import Toybox.System; -import Toybox.Application; -import Toybox.Lang; - -class ProfilePickerDelegate extends WatchUi.PickerDelegate { - - private var _typeId; - - function initialize(typeId) { - PickerDelegate.initialize(); - _typeId = typeId; - } - - function onAccept(values as Array) as Boolean { - var pickedValue = values[0]; // Gets the "selected" value - - var app = Application.getApp() as GarminApp; - - if (_typeId == :prof_height) { - System.println("Height Saved: " + pickedValue); - app.setUserHeight(pickedValue); - } - else if (_typeId == :prof_speed) { - System.println("Speed Saved: " + pickedValue); - app.setUserSpeed(pickedValue); - } - - app.idealCadenceCalculator(); - - WatchUi.popView(WatchUi.SLIDE_RIGHT); - return true; - } - - function onCancel() as Boolean { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - return true; - } -} \ No newline at end of file diff --git a/source/Delegates/SettingsDelegates/ProfileDelegates/ProfilePickerFactory.mc b/source/Delegates/SettingsDelegates/ProfileDelegates/ProfilePickerFactory.mc deleted file mode 100644 index a6140f5..0000000 --- a/source/Delegates/SettingsDelegates/ProfileDelegates/ProfilePickerFactory.mc +++ /dev/null @@ -1,64 +0,0 @@ -import Toybox.WatchUi; -import Toybox.Graphics; -import Toybox.Lang; - -class ProfilePickerFactory extends WatchUi.PickerFactory { - private var _start as Number; - private var _stop as Number; - private var _increment as Number; - private var _label as String; - - function initialize(start as Number, stop as Number, increment as Number, options as Dictionary?) { - PickerFactory.initialize(); - _start = start; - _stop = stop; - _increment = increment; - _label = ""; - - if (options != null) { - if (options.hasKey(:label)) { - _label = options[:label] as String; - } - } - } - - function getSize() as Number { - return (_stop - _start) / _increment + 1; - } - - function getValue(index as Number) as Object? { - return _start + (index * _increment); - } - - function getDrawable(index as Number, selected as Boolean) as Drawable? { - - // gets the selected value - var val = getValue(index); - - // converts to number if needed - if (val has :toNumber) { - val = val.toNumber(); - } - - // string that is displayed (e.g. "175" + " cm") - var displayString = Lang.format("$1$$2$", [val, _label]); - - return new WatchUi.Text({ - :text => displayString, - :color => Graphics.COLOR_WHITE, - :font => Graphics.FONT_MEDIUM, - :locX => WatchUi.LAYOUT_HALIGN_CENTER, - :locY => WatchUi.LAYOUT_VALIGN_CENTER - }); - } - - function getIndex(value as Number) as Number { - - var safeValue = value; - if (safeValue has :toNumber) { - safeValue = safeValue.toNumber(); - } - - return (safeValue - _start) / _increment; - } -} \ No newline at end of file diff --git a/source/Delegates/SettingsDelegates/ProfileDelegates/SelectExperienceDelegate.mc b/source/Delegates/SettingsDelegates/ProfileDelegates/SelectExperienceDelegate.mc deleted file mode 100644 index c33bb92..0000000 --- a/source/Delegates/SettingsDelegates/ProfileDelegates/SelectExperienceDelegate.mc +++ /dev/null @@ -1,61 +0,0 @@ -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectExperienceDelegate extends WatchUi.Menu2InputDelegate { - - private var _menu as WatchUi.Menu2; - var app = Application.getApp() as GarminApp; - var experienceLvl = app.getExperienceLvl(); - var experienceLvlString = "NULL"; - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - _menu = menu; - - if (experienceLvl == GarminApp.Beginner){ - experienceLvlString = "Beginner"; - } else if (experienceLvl == GarminApp.Intermediate){ - experienceLvlString = "Intermediate"; - } else if (experienceLvl == GarminApp.Advanced){ - experienceLvlString = "Advanced"; - } - var newTitle = Lang.format("Experience: $1$", [experienceLvlString]); - - // This updates the UI when the experience level is changed - _menu.setTitle(newTitle); - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Try to change user experience lvl based off menu selection - if (id == :exp_beginner){ - System.println("User ExperienceLvl: Beginner"); - app.setExperienceLvl(GarminApp.Beginner); - } - else if (id == :exp_intermediate){ - System.println("User ExperienceLvl: Intermediate"); - app.setExperienceLvl(GarminApp.Intermediate); - } - else if (id == :exp_advanced){ - System.println("User ExperienceLvl: Advanced"); - app.setExperienceLvl(GarminApp.Advanced); - } else {System.println("ERROR");} - - app.idealCadenceCalculator(); - - WatchUi.popView(WatchUi.SLIDE_RIGHT); - - } - - - function onMenuItem(item as Symbol) as Void {} - - // Returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} \ No newline at end of file diff --git a/source/Delegates/SettingsDelegates/ProfileDelegates/SelectGenderDelegate.mc b/source/Delegates/SettingsDelegates/ProfileDelegates/SelectGenderDelegate.mc deleted file mode 100644 index 260fcf2..0000000 --- a/source/Delegates/SettingsDelegates/ProfileDelegates/SelectGenderDelegate.mc +++ /dev/null @@ -1,52 +0,0 @@ -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectGenderDelegate extends WatchUi.Menu2InputDelegate { - - private var _menu as WatchUi.Menu2; - var app = Application.getApp() as GarminApp; - var gender = app.getUserGender(); - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - _menu = menu; - - // need if statements to display experiencelvl string instead of float values - var newTitle = Lang.format("Gender: $1$", [gender]); - - // This updates the UI when the cadence is changed - _menu.setTitle(newTitle); - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Try to change user gender based off menu selection - if (id == :user_male){ - app.setUserGender(GarminApp.Male); - System.println("User Gender: Male"); - } - else if (id == :user_female){ - app.setUserGender(GarminApp.Female); - System.println("User Gender: Female"); - } - else if (id == :user_other){ - app.setUserGender(GarminApp.Other); - System.println("User Gender: Other"); - } else {System.println("ERROR");} - - app.idealCadenceCalculator(); - - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } - - function onMenuItem(item as Symbol) as Void {} - - // Returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} \ No newline at end of file diff --git a/source/Delegates/SettingsDelegates/ProfileDelegates/SelectProfileDelegate.mc b/source/Delegates/SettingsDelegates/ProfileDelegates/SelectProfileDelegate.mc deleted file mode 100644 index f9e8551..0000000 --- a/source/Delegates/SettingsDelegates/ProfileDelegates/SelectProfileDelegate.mc +++ /dev/null @@ -1,103 +0,0 @@ -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; -import Toybox.Graphics; - -class SelectProfileDelegate extends WatchUi.Menu2InputDelegate { - - //private var _menu as WatchUi.Menu2; - var app = Application.getApp() as GarminApp; - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - //_menu = menu; - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //displays the menu for the selected item - if (id == :profile_height){ - heightPicker(); - } - else if (id == :profile_speed){ - speedPicker(); - } - else if (id == :profile_experience){ - experienceMenu(); - } - else if (id == :profile_gender){ - genderMenu(); - } - } - - function onMenuItem(item as Symbol) as Void {} - - // Returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } - - function heightPicker() as Void { - - var currentHeight = app.getUserHeight(); - if (currentHeight == null) { currentHeight = 175; } // Default 175 cm - - var factory = new ProfilePickerFactory(100, 250, 1, {:label=>" cm"}); - - var picker = new WatchUi.Picker({ - :title => new WatchUi.Text({:text=>"Set Height", :locX=>WatchUi.LAYOUT_HALIGN_CENTER, :locY=>WatchUi.LAYOUT_VALIGN_BOTTOM, :color=>Graphics.COLOR_WHITE}), - :pattern => [factory], - :defaults => [factory.getIndex(currentHeight)] - }); - - WatchUi.pushView(picker, new ProfilePickerDelegate(:prof_height), WatchUi.SLIDE_LEFT); - - } - - function speedPicker() as Void { - //uses number not float - var currentSpeed = app.getUserSpeed().toNumber(); - if (currentSpeed == null) { currentSpeed = 3; } // Default 3 km/h - - var factory = new ProfilePickerFactory(3, 30, 1, {:label=>" km/h"}); - - var picker = new WatchUi.Picker({ - :title => new WatchUi.Text({:text=>"Set Speed", :locX=>WatchUi.LAYOUT_HALIGN_CENTER, :locY=>WatchUi.LAYOUT_VALIGN_BOTTOM, :color=>Graphics.COLOR_WHITE}), - :pattern => [factory], - :defaults => [factory.getIndex(currentSpeed)] - }); - - WatchUi.pushView(picker, new ProfilePickerDelegate(:prof_speed), WatchUi.SLIDE_LEFT); - - } - - function experienceMenu() as Void { - var menu = new WatchUi.Menu2({ - :title => "Set Experience" - }); - - menu.addItem(new WatchUi.MenuItem("Beginner", null, :exp_beginner, null)); - menu.addItem(new WatchUi.MenuItem("Intermediate", null, :exp_intermediate, null)); - menu.addItem(new WatchUi.MenuItem("Advanced", null, :exp_advanced, null)); - - //pushes the view to the screen with the relevent delegate - WatchUi.pushView(menu, new SelectExperienceDelegate(menu), WatchUi.SLIDE_LEFT); - } - - function genderMenu() as Void { - var menu = new WatchUi.Menu2({ - :title => "Set Gender" - }); - - menu.addItem(new WatchUi.MenuItem("Male", null, :user_male, null)); - menu.addItem(new WatchUi.MenuItem("Female", null, :user_female, null)); - menu.addItem(new WatchUi.MenuItem("Other", null, :user_other, null)); - - //pushes the view to the screen with the relevent delegate - WatchUi.pushView(menu, new SelectGenderDelegate(menu), WatchUi.SLIDE_LEFT); - } - -} \ No newline at end of file diff --git a/source/Delegates/SettingsDelegates/SettingsDelegate.mc b/source/Delegates/SettingsDelegates/SettingsDelegate.mc deleted file mode 100644 index 972f1e0..0000000 --- a/source/Delegates/SettingsDelegates/SettingsDelegate.mc +++ /dev/null @@ -1,101 +0,0 @@ -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -//this Delegate handels the menu items and creates the menus for each item -class SettingsMenuDelegate extends WatchUi.Menu2InputDelegate { - - function initialize() { - Menu2InputDelegate.initialize(); - } - - //triggers when user selects a menu option - function onSelect(item as WatchUi.MenuItem) as Void { - var id = item.getId(); - - //pushes next menu view based on selection - if (id == :set_profile) { - System.println("Selected: Set Profile"); - //function to push next view - pushProfileMenu(); - } - else if (id == :cust_options) { - System.println("Selected: Customizable Options"); - pushCustMenu(); - } - else if (id == :feedback_options) { - System.println("Selected: Feedback Options"); - pushFeedbackMenu(); - } - else if (id == :cadence_range) { - pushCadenceMenu(); - } - } - - //allows user to go back from the menu view - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } - - function pushProfileMenu() as Void{ - - //creates the secondary menu and sets title - var menu = new WatchUi.Menu2({ - :title => "Profile Options" - }); - - //creates the new menu items - menu.addItem(new WatchUi.MenuItem("Height", null, :profile_height, null)); - menu.addItem(new WatchUi.MenuItem("Speed", null, :profile_speed, null)); - menu.addItem(new WatchUi.MenuItem("Experience level", null, :profile_experience, null)); - menu.addItem(new WatchUi.MenuItem("Gender", null, :profile_gender, null)); - - //pushes the view to the screen with the relevent delegate - WatchUi.pushView(menu, new SelectProfileDelegate(menu), WatchUi.SLIDE_LEFT); - - } - - function pushCustMenu() as Void{ - - var menu = new WatchUi.Menu2({ - :title => "Customization Options" - }); - - menu.addItem(new WatchUi.MenuItem("Bar Chart", null, :cust_bar_chart, null)); - - WatchUi.pushView(menu, new SelectCutomizableDelegate(menu), WatchUi.SLIDE_LEFT); - - } - - function pushFeedbackMenu() as Void{ - - var menu = new WatchUi.Menu2({ - :title => "Feedback Options" - }); - - menu.addItem(new WatchUi.MenuItem("Haptic Feedback", null, :haptic_feedback, null)); - menu.addItem(new WatchUi.MenuItem("Audible Feedback", null, :audible_feedback, null)); - - WatchUi.pushView(menu, new SelectFeedbackDelegate(menu), WatchUi.SLIDE_LEFT); - } - - function pushCadenceMenu() as Void { - - //sets the cadence variables to the global app variable to be used within the title - var app = Application.getApp() as GarminApp; - var minCadence = app.getMinCadence(); - var maxCadence = app.getMaxCadence(); - - var menu = new WatchUi.Menu2({ - :title => Lang.format("Cadence: $1$ - $2$", [minCadence, maxCadence]) - }); - - menu.addItem(new WatchUi.MenuItem(WatchUi.loadResource(Rez.Strings.menu_inc_min), null, :item_inc_min, null)); - menu.addItem(new WatchUi.MenuItem(WatchUi.loadResource(Rez.Strings.menu_dec_min), null, :item_dec_min, null)); - menu.addItem(new WatchUi.MenuItem(WatchUi.loadResource(Rez.Strings.menu_inc_max), null, :item_inc_max, null)); - menu.addItem(new WatchUi.MenuItem(WatchUi.loadResource(Rez.Strings.menu_dec_max), null, :item_dec_max, null)); - - WatchUi.pushView(menu, new SelectCadenceDelegate(menu), WatchUi.SLIDE_LEFT); - } -} \ No newline at end of file diff --git a/source/Logger.mc b/source/Logger.mc deleted file mode 100644 index 1c97e52..0000000 --- a/source/Logger.mc +++ /dev/null @@ -1,24 +0,0 @@ -import Toybox.Lang; -import Toybox.System; - -/** - * Simple logger for memory monitoring only - */ -module Logger { - - /** - * Log memory statistics - */ - function logMemoryStats(tag as String) as Void { - try { - var stats = System.getSystemStats(); - var usedMemory = stats.totalMemory - stats.freeMemory; - var memoryPercent = (usedMemory.toFloat() / stats.totalMemory.toFloat() * 100).toNumber(); - - System.println("[MEMORY] " + tag + ": " + usedMemory + "/" + stats.totalMemory + - " bytes (" + memoryPercent + "% used)"); - } catch (e) { - System.println("[ERROR] Failed to log memory stats: " + e.getErrorMessage()); - } - } -} \ No newline at end of file diff --git a/source/SensorManager.mc b/source/SensorManager.mc deleted file mode 100644 index 1fd6131..0000000 --- a/source/SensorManager.mc +++ /dev/null @@ -1,58 +0,0 @@ -using Toybox.Activity; -using Toybox.System; -using Toybox.Lang; - -class SensorManager { - - // ----------------------- - // Static configuration - // ----------------------- - - // Use simulator mode by default - static var useSimulator = true; - - // Simulated cadence value for testing - static var simulatedCadence = 0; - - // ----------------------- - // Public Methods - // ----------------------- - - // Set simulated cadence (for testing) - public static function setSimCadence(value) { - if (value == null || !(value instanceof Lang.Number)) { - System.println("[SensorManager] ERROR: simulated cadence must be a number"); - return; - } - - SensorManager.simulatedCadence = value; - System.println("[SensorManager] Simulated cadence set to: " + SensorManager.simulatedCadence.toString()); - } - - // Switch mode between simulator and real sensor - public static function setMode(simulator) { - // No strict type check needed - SensorManager.useSimulator = simulator ? true : false; - System.println("[SensorManager] Mode set to: " + (SensorManager.useSimulator ? "SIM" : "REAL")); - } - - // Get current cadence - public static function getCadence() { - var cadence = 0; - - if (SensorManager.useSimulator) { - cadence = SensorManager.simulatedCadence; - System.println("[SensorManager] Returning SIM cadence: " + cadence); - } else { - var info = Activity.getActivityInfo(); - if (info != null && info.currentCadence != null) { - cadence = info.currentCadence; - System.println("[SensorManager] Returning REAL cadence: " + cadence); - } else { - System.println("[SensorManager] REAL cadence unavailable, returning 0"); - } - } - - return cadence; - } -} \ No newline at end of file diff --git a/source/Views/SettingsView.mc b/source/Views/SettingsView.mc deleted file mode 100644 index 54234bc..0000000 --- a/source/Views/SettingsView.mc +++ /dev/null @@ -1,64 +0,0 @@ -import Toybox.Graphics; -import Toybox.WatchUi; -import Toybox.System; -import Toybox.Application; -import Toybox.Lang; -import Toybox.Math; - -class SettingsView extends WatchUi.View { - - // to store the coords and width/heigh of the button (for cadence for now) - private var _buttonCoords as Array?; - - - function initialize() { - View.initialize(); - _buttonCoords = [0, 0, 0, 0] as Array; - } - - - function onLayout(dc as Dc) as Void { - - // Define button dimensions based on screen size (rough values for now) - var screenWidth = dc.getWidth(); - var screenHeight = dc.getHeight(); - - var x1 = screenWidth * 0.2; - var y1 = screenHeight / 2; - var width = screenWidth - (screenWidth * 0.4); - var height = screenHeight / 3; - - // Sets button coords - _buttonCoords = [x1, y1, width, height] as Array; - System.println(x1.toString() + " and " + y1.toString() + " and " + width.toString() + " and " + height.toString()); - - } - - function onShow() as Void {} - - function onUpdate(dc as Dc) as Void { - - View.onUpdate(dc); - drawCadenceButton(dc); - - } - - // Draws the temp button - function drawCadenceButton(dc as Dc) as Void { - - dc.setColor(Graphics.COLOR_BLUE, Graphics.COLOR_BLUE); - dc.drawRoundedRectangle(_buttonCoords[0], _buttonCoords[1], _buttonCoords[2], _buttonCoords[3], 10); - - } - - // Public getter method for the button coordinates - function getButtonCoords() as Array { - return _buttonCoords; - } - - function refreshScreen() as Void{ - WatchUi.requestUpdate(); - } - - function onHide() as Void {} -} \ No newline at end of file From b98b95ab845d4173679d16d04e382249e529d367 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 27 Jan 2026 14:13:43 +1100 Subject: [PATCH 2/5] removed profanity --- TECHNICAL_DOCUMENTATION_v2.md | 552 ++++++++++++++++++++++------------ 1 file changed, 361 insertions(+), 191 deletions(-) diff --git a/TECHNICAL_DOCUMENTATION_v2.md b/TECHNICAL_DOCUMENTATION_v2.md index ddecca4..0a042c0 100644 --- a/TECHNICAL_DOCUMENTATION_v2.md +++ b/TECHNICAL_DOCUMENTATION_v2.md @@ -1,13 +1,14 @@ # Redback Operation Garmin App - Technical Documentation ## Concept & Vision + A Garmin watch-app that turns the built-in cadence sensor into a **real-time running-efficiency coach that can be used for rehabilitation or hard core trainers alike**. While you run it shows: -* Live cadence vs. your **personal ideal zone** (calculated from height, speed, gender, experience) -* A **28-minute rolling histogram** that colour-codes every stride -* A **0-100 % "Cadence Quality" (CQ)** score that is frozen when you stop and is written into the FIT file so it follows the activity into Garmin Connect -* **Smart haptic alerts** that vibrate when you drift out of your optimal cadence zone +- Live cadence vs. your **personal ideal zone** (calculated from height, speed, gender, experience) +- A **28-minute rolling histogram** that colour-codes every stride +- A **0-100 % "Cadence Quality" (CQ)** score that is frozen when you stop and is written into the FIT file so it follows the activity into Garmin Connect +- **Smart haptic alerts** that vibrate when you drift out of your optimal cadence zone ## Table of Contents @@ -29,11 +30,13 @@ While you run it shows: --- ## Prerequisites + - Garmin Connect IQ SDK 8.3.0+ - Visual Studio Code with Connect IQ extension - Forerunner 165/165 Music device or simulator ## Build Process + 1. Clone repository 2. Configure project settings in `monkey.jungle` 3. Build for target device: @@ -54,17 +57,18 @@ This project follows a **rebase-focused, fast-merge workflow** designed to keep ### Core Principles -| Principle | Meaning | Why It Matters | -|-----------|---------|----------------| -| Main is always green | If it's on `main`, it works and deploys | Broken code never reaches production | -| Rebase, don't merge | Linear history = readable git log | Easy to track down bugs, understand changes | -| Small, fast PRs | Less than 400 lines, less than 3 days open | Easier reviews, less conflict potential | -| Automated enforcement | Machines check; humans review logic | Catches issues before review | -| Sync early, sync often | Rebase on `main` daily | Prevents large, scary conflicts | +| Principle | Meaning | Why It Matters | +| ---------------------- | ------------------------------------------ | ------------------------------------------- | +| Main is always green | If it's on `main`, it works and deploys | Broken code never reaches production | +| Rebase, don't merge | Linear history = readable git log | Easy to track down bugs, understand changes | +| Small, fast PRs | Less than 400 lines, less than 3 days open | Easier reviews, less conflict potential | +| Automated enforcement | Machines check; humans review logic | Catches issues before review | +| Sync early, sync often | Rebase on `main` daily | Prevents large, scary conflicts | ### Why Rebase? The Visual Comparison **MERGE (creates complex history):** + ``` A---B---C-------F---G main \ / @@ -73,12 +77,14 @@ A---B---C-------F---G main ``` **REBASE (clean linear history):** + ``` A---B---C---F---G---D'---E' main (feature commits replayed on top) ``` **Benefits of Rebasing:** + - History reads like a book: chronological, no tangles - `git bisect` works reliably to find bugs - Each commit can be individually tested @@ -96,20 +102,20 @@ sequenceDiagram participant YourBranch as feature/vibration participant Main participant Teammate - + Note over You,Main: Monday 9am You->>YourBranch: Create branch from main@100 - + Note over You,Main: Tuesday 3pm Teammate->>Main: Merge feature/graph-fix (main@105) - + Note over You,Main: Wednesday 10am You->>YourBranch: Open Pull Request YourBranch-->>Main: "Warning: Branch is out-of-date" - + Note over You,Main: Thursday 2pm Teammate->>Main: Merge feature/settings (main@110) - + Note over You,Main: Friday 11am You->>Main: Attempt merge Note over Main: Conflicts! Tests fail! Team sad! @@ -155,13 +161,13 @@ git push --force-with-lease ### When to Sync Your Branch -| Timing | Action | Command | -|--------|--------|---------| -| **Start work** | Branch from latest main | `git fetch && git checkout -b feature/name origin/main` | -| **Daily** (minimum) | Rebase on main | `git fetch && git rebase origin/main && git push --force-with-lease` | -| **Before opening PR** | Final rebase + cleanup | `git rebase -i origin/main` (squash "WIP" commits if needed) | -| **After PR approved** | Last sync before merge | `git fetch && git rebase origin/main` | -| **After PR merged** | Delete branch | `git push origin --delete feature/name && git branch -D feature/name` | +| Timing | Action | Command | +| --------------------- | ----------------------- | --------------------------------------------------------------------- | +| **Start work** | Branch from latest main | `git fetch && git checkout -b feature/name origin/main` | +| **Daily** (minimum) | Rebase on main | `git fetch && git rebase origin/main && git push --force-with-lease` | +| **Before opening PR** | Final rebase + cleanup | `git rebase -i origin/main` (squash "WIP" commits if needed) | +| **After PR approved** | Last sync before merge | `git fetch && git rebase origin/main` | +| **After PR merged** | Delete branch | `git push origin --delete feature/name && git branch -D feature/name` | --- @@ -172,11 +178,13 @@ git push --force-with-lease **Don't click "Update branch" button** - it creates a merge commit! **Instead, use terminal:** + ```bash git fetch origin git rebase origin/main git push --force-with-lease ``` + Then refresh the PR page - warning disappears. ### Situation 2: Someone Else Pushed to Your Branch @@ -197,7 +205,8 @@ git rebase origin/main git push --force-with-lease ``` -### Situation 3: Rebase Goes Wrong +### Situation 3: Rebase Goes Wrong + **I have done this more times than i care to remember** If you make a mistake during rebase: @@ -216,19 +225,20 @@ git reset --hard HEAD@{5} # Numbers from reflog --- ## Branch Protection Rules + **some of these are active on the bramch** Our repository enforces these rules on the `main` branch (Settings → Branches): -| Setting | Value | Why | -|---------|-------|-----| -| **Require pull request** | Enabled | Prevents accidental `git push origin main` | -| **Required approvals** | 1 reviewer | Ensures code review before merge | -| **Dismiss stale approvals** | Enabled | New commits after approval require re-review | -| **Require status checks** | CI must pass | Code must build and pass tests | +| Setting | Value | Why | +| ------------------------------- | ------------ | -------------------------------------------------- | +| **Require pull request** | Enabled | Prevents accidental `git push origin main` | +| **Required approvals** | 1 reviewer | Ensures code review before merge | +| **Dismiss stale approvals** | Enabled | New commits after approval require re-review | +| **Require status checks** | CI must pass | Code must build and pass tests | | **Require branches up to date** | **Critical** | PR must include latest `main` commits before merge | -| **Allow force pushes** | Disabled | Protects main's history | -| **Allow deletions** | Disabled | Prevents accidental branch deletion | +| **Allow force pushes** | Disabled | Protects main's history | +| **Allow deletions** | Disabled | Prevents accidental branch deletion | **Why "Require branches up to date" is critical:** This forces you to rebase before merging, ensuring the final merge result was actually tested in CI. Without this, two "green" PRs can be merged sequentially and break `main`. @@ -236,21 +246,23 @@ This forces you to rebase before merging, ensuring the final merge result was ac --- ## Branch Naming Conventions -**this is my preffered naming convention - i try an incorparte naming conventions into everything, even university submissions, so instead of SIT782_5_4HDV3r5edit8 i have a meaning name. + +\*\*this is my preffered naming convention - i try an incorparte naming conventions into everything, even university submissions, so instead of SIT782_5_4HDV3r5edit8 i have a meaning name. Use these prefixes to keep branches organized: -| Prefix | Purpose | Example | -|--------|---------|---------| -| `feature/` | New functionality | `feature/vibration-alerts` | -| `fix/` | Bug fixes | `fix/timer-crash` | +| Prefix | Purpose | Example | +| ----------- | ------------------------------------- | --------------------------------- | +| `feature/` | New functionality | `feature/vibration-alerts` | +| `fix/` | Bug fixes | `fix/timer-crash` | | `refactor/` | Code improvement (no behavior change) | `refactor/extract-chart-renderer` | -| `docs/` | Documentation only | `docs/api-examples` | -| `hotfix/` | Urgent production fix | `hotfix/memory-leak` | +| `docs/` | Documentation only | `docs/api-examples` | +| `hotfix/` | Urgent production fix | `hotfix/memory-leak` | **Format:** `prefix/descriptive-name-in-kebab-case` **Examples:** + - Good: `feature/haptic-feedback` - Good: `fix/null-pointer-crash` - Bad: `my-branch` (no prefix) @@ -262,37 +274,44 @@ Use these prefixes to keep branches organized: ### PR Size Guidelines -| Lines Changed | Status | Recommendation | -|---------------|--------|----------------| -| Less than 200 lines | Excellent | Ideal for fast review | -| 200-400 lines | Large | Consider splitting | -| 400+ lines | Too large | Must split into multiple PRs | +| Lines Changed | Status | Recommendation | +| ------------------- | --------- | ---------------------------- | +| Less than 200 lines | Excellent | Ideal for fast review | +| 200-400 lines | Large | Consider splitting | +| 400+ lines | Too large | Must split into multiple PRs | **Why small PRs are better-ish:** -- Faster reviews + +- Faster reviews - Lower chance of conflicts - Easier to understand changes - Less likely to introduce bugs ### PR Template -**Does my head in when there is no comments or something generic like 'added stuff", admittedly its easier but its a crappy mindset because the person reviewing has no idea. + +\*\*Does my head in when there is no comments or something generic like 'added stuff", admittedly its easier but its a crappy mindset because the person reviewing has no idea. When opening a PR, include: ```markdown ## What + Brief description of changes (1-2 sentences) ## Why + Problem being solved or feature being added ## How + Technical approach taken ## Testing + How to verify these changes work ## Screenshots/Videos + If UI changes, show before/after -- not always relevant ``` @@ -325,6 +344,7 @@ graph LR **CI must pass before merge button activates.** If CI fails: + 1. Check the logs in the "Checks" tab 2. Fix the issue locally 3. Commit and push @@ -338,12 +358,14 @@ We use **Squash and Merge** for all PRs: **this is what i like, there is more than one way to skin this cat, find what works for your project.** **Benefits:** + - Each PR becomes a single commit on `main` - Clean, readable history - Easy to revert entire features - Encourages frequent commits during development **Process:** + 1. PR gets approved and CI passes 2. Click "Squash and merge" 3. Edit commit message (auto-generated from PR) @@ -356,16 +378,19 @@ We use **Squash and Merge** for all PRs: While we use rebase-focused workflow, other valid approaches exist: ### Git Flow + - Uses `develop` branch as integration branch - `main` only for releases - More complex, better for teams with scheduled releases ### Trunk-Based Development + - Everyone commits to `main` frequently - Heavy use of feature flags - Requires strong CI/CD and testing discipline ### GitHub Flow + - Similar to our approach - Allows merge commits instead of rebase - Simpler but creates non-linear history @@ -378,12 +403,14 @@ Balances simplicity (better than Git Flow) with code quality (cleaner than merge ## Quick Reference Guide **Starting a new feature:** + ```bash git fetch origin git checkout -b feature/my-feature origin/main ``` **Daily sync:** + ```bash git fetch origin git stash @@ -393,12 +420,14 @@ git push --force-with-lease ``` **Opening a PR:** + 1. Push branch: `git push -u origin feature/my-feature` 2. Open PR on GitHub 3. Request review 4. Respond to feedback **After PR merged:** + ```bash git checkout main git pull @@ -410,14 +439,16 @@ git branch -D feature/my-feature ## Getting Help **Stuck during rebase?** + 1. Don't panic -- i cant stress this enough.. see below. 2. Run `git status` to see what's happening 3. Ask in team chat with: - Output of `git status` - What you were trying to do - Current branch name -4. DONT Panic. Everyone screws up and makes mistakes. If you fuck up, fix it, have an RCA and move on. I have bunged code and production systems over the years. FUck up, fix it, move on. -**Common commands for troubleshooting:** +4. DONT Panic. Everyone screws up and makes mistakes. If you f*ck up, fix it, have an RCA and move on. I have bunged code and production systems over the years. F*ck up, fix it, move on. + **Common commands for troubleshooting:** + ```bash git status # See current state git log --oneline -10 # Recent commits @@ -432,48 +463,52 @@ git diff # Uncommitted changes ## Workflow Checklist Before starting work: + - [ ] `git fetch origin` - [ ] `git checkout -b feature/name origin/main` During development: + - [ ] Commit frequently with clear messages - [ ] Rebase on `main` daily - [ ] Keep PR less than 400 lines if possible Before opening PR: + - [ ] Final rebase: `git rebase -i origin/main` - [ ] Squash WIP commits if needed - [ ] Run tests locally - [ ] Push: `git push -u origin feature/name` During review: + - [ ] Respond to feedback within 24h - [ ] Keep branch updated with `main` - [ ] Address all review comments After merge: + - [ ] Delete branch locally: `git branch -D feature/name` - [ ] Pull latest main: `git checkout main && git pull` - [ ] Celebrate! --- -*This workflow is designed to minimize conflicts and maximize collaboration. When in doubt, communicate early with the team and sync your branch often!* +_This workflow is designed to minimize conflicts and maximize collaboration. When in doubt, communicate early with the team and sync your branch often!_ --- - - - ## Architecture Overview ### Application Type + - **Type**: Garmin Watch App (not data field or widget) - **Target Devices**: Forerunner 165, Forerunner 165 Music - **SDK Version**: Minimum API Level 5.2.0 - **Architecture**: MVC (Model-View-Controller/Delegate pattern) ### High-Level Structure + ``` GarminApp (Application Core) ├── Views @@ -500,16 +535,16 @@ graph TB B -->|Commands| C[GarminApp] C -->|State Changes| D[View] D -->|Display| A - + E[Cadence Sensor] -->|Data| C C -->|Process| F[CQ Algorithm] C -->|Monitor| G[Haptic Manager] G -->|Vibrations| H[Watch Hardware] - + C -->|Records| I[ActivitySession] I -->|Saves| J[FIT File] J -->|Syncs| K[Garmin Connect] - + style G fill:#ff9999 style H fill:#ff9999 ``` @@ -519,9 +554,11 @@ graph TB ## Core Components ### 1. GarminApp.mc + **Purpose**: Central application controller and data manager **Key Responsibilities**: + - Activity session lifecycle management (start/pause/resume/stop/save/discard) - Cadence data collection and storage - Cadence quality score computation @@ -530,15 +567,17 @@ graph TB - Integration with Garmin Activity Recording API #### 1a. Memory Footprint (Cold Numbers) -* Static allocation: ≈ 2.8 kB - * 280 cadence samples × 4 B = 1.1 kB - * 280 EMA smoothed values = 1.1 kB - * 10 CQ history = 40 B - * Misc buffers / state ≈ 600 B -* Peak stack during draw: ≈ 400 B -* Total at run-time: < 3.5 kB → fits easily into the 32 kB heap of the FR165 + +- Static allocation: ≈ 2.8 kB + - 280 cadence samples × 4 B = 1.1 kB + - 280 EMA smoothed values = 1.1 kB + - 10 CQ history = 40 B + - Misc buffers / state ≈ 600 B +- Peak stack during draw: ≈ 400 B +- Total at run-time: < 3.5 kB → fits easily into the 32 kB heap of the FR165 **Important Constants**: + ```monkey-c MAX_BARS = 280 // Maximum cadence samples to store BASELINE_AVG_CADENCE = 160 // Minimum acceptable cadence @@ -548,6 +587,7 @@ DEBUG_MODE = true // Enable debug logging ``` **State Variables**: + - `_sessionState`: Current session state (IDLE/RECORDING/PAUSED/STOPPED) - `activitySession`: Garmin ActivityRecording session object - `_cadenceHistory`: Circular buffer storing 280 cadence samples @@ -555,15 +595,18 @@ DEBUG_MODE = true // Enable debug logging - `_cqHistory`: Last 10 CQ scores for trend analysis ### 2. SimpleView.mc & AdvancedView.mc + **Purpose**: Display interfaces with integrated haptic feedback **SimpleView Responsibilities**: + - Display current cadence, heart rate, distance, time - Show cadence zone status (In Zone/Out Zone) - Trigger haptic alerts when out of zone - Update UI every second **AdvancedView Responsibilities**: + - Render 28-minute cadence histogram - Display heart rate and distance circles - Show zone boundaries on chart @@ -571,6 +614,7 @@ DEBUG_MODE = true // Enable debug logging - Color-code bars based on cadence zones **Haptic Alert Variables** (both views): + ```monkey-c private var _lastZoneState = 0; // -1=below, 0=in zone, 1=above private var _alertStartTime = null; // When alerts began @@ -597,11 +641,11 @@ graph TD F --> G[_cadenceHistory 280 samples] G --> H[computeCadenceQualityScore] H --> I[_cqHistory Last 10 scores] - + G -->|Monitor| J[Zone Detection] J -->|Out of Zone| K[Trigger Haptic Alert] K --> L[User Feedback] - + style K fill:#ff9999 style L fill:#ff9999 ``` @@ -615,10 +659,10 @@ sequenceDiagram participant ZoneChecker participant HapticManager participant Hardware - + User->>View: Running (cadence changes) View->>ZoneChecker: Check current cadence - + alt Cadence drops below minimum ZoneChecker->>HapticManager: Trigger single buzz pattern HapticManager->>Hardware: Vibrate 200ms @@ -640,17 +684,20 @@ sequenceDiagram ### 3. Timer System **Global Timer** (`globalTimer`): + - Frequency: Every 1 second - Callback: `updateCadenceBarAvg()` - Runs: Always (from app start to stop) - Purpose: Collect cadence data when recording **View Refresh Timers**: + - SimpleView: Refresh every 1 second (reused for haptic checks) - AdvancedView: Refresh every 1 second (reused for haptic checks) - Purpose: Update UI elements and monitor zone status **Haptic Alert System** (NO dedicated timers): + - Uses existing view refresh cycle - Checks zone status on each UI update - Triggers vibrations when appropriate @@ -661,6 +708,7 @@ sequenceDiagram The app uses a two-tier averaging system: **Tier 1: Bar Averaging** + ``` Chart Duration = 6 seconds (ThirtyminChart default) ↓ @@ -672,6 +720,7 @@ Store as single bar value ``` **Tier 2: Historical Storage** + ``` 280 bar values stored ↓ @@ -681,16 +730,18 @@ Total history = 280 × 6 = 1680 seconds = 28 minutes ``` **Chart Duration Options**: + - FifteenminChart = 3 seconds per bar - ThirtyminChart = 6 seconds per bar (default) - OneHourChart = 13 seconds per bar - TwoHourChart = 26 seconds per bar ### 4a. Sensor Manager Abstraction + `SensorManager.mc` decouples real vs. simulated cadence: ```monkey-c -useSimulator = true → returns hard-coded value (for desk testing) +useSimulator = true → returns hard-coded value (for desk testing) useSimulator = false → reads Activity.getActivityInfo().currentCadence ``` @@ -709,21 +760,21 @@ stateDiagram-v2 RECORDING --> STOPPED: stopRecording() PAUSED --> STOPPED: stopRecording() STOPPED --> IDLE: saveSession() / discardSession() - + note right of RECORDING - Timer active - Data collecting - Haptic alerts enabled - UI updating end note - + note right of PAUSED - Timer stopped - Data frozen - Haptic alerts disabled - UI static end note - + note right of STOPPED - Final CQ calculated - Haptic alerts disabled @@ -734,6 +785,7 @@ stateDiagram-v2 ### State Transition Rules **IDLE → RECORDING**: + - User presses START/STOP button - Creates new ActivityRecording session - Starts Garmin timer @@ -742,6 +794,7 @@ stateDiagram-v2 - **Enables haptic zone monitoring** **RECORDING → PAUSED**: + - User selects "Pause" from menu - Stops Garmin timer (timer pauses) - Records pause timestamp @@ -749,6 +802,7 @@ stateDiagram-v2 - **Disables haptic alerts** **PAUSED → RECORDING**: + - User selects "Resume" from menu - Restarts Garmin timer - Accumulates paused time @@ -756,6 +810,7 @@ stateDiagram-v2 - **Re-enables haptic zone monitoring** **RECORDING/PAUSED → STOPPED**: + - User selects "Stop" from menu - Stops Garmin timer - Computes final CQ score @@ -764,6 +819,7 @@ stateDiagram-v2 - Awaits save/discard decision **STOPPED → IDLE**: + - User selects "Save": Saves to FIT file - User selects "Discard": Deletes session - Resets all data structures @@ -776,6 +832,7 @@ stateDiagram-v2 ### Garmin ActivityRecording Integration **Session Creation** (`startRecording()`): + ```monkey-c activitySession = ActivityRecording.createSession({ :name => "Running", @@ -786,6 +843,7 @@ activitySession.start(); ``` **What This Does**: + - Creates official Garmin activity - Starts timer (visible in UI) - Records GPS, heart rate, cadence automatically @@ -793,6 +851,7 @@ activitySession.start(); - Handles sensor data collection **Pause/Resume** (`pauseRecording()` / `resumeRecording()`): + ```monkey-c // Pause activitySession.stop(); // Pauses timer @@ -802,18 +861,22 @@ activitySession.start(); // Resumes timer ``` **Save** (`saveSession()`): + ```monkey-c activitySession.save(); ``` + - Writes FIT file to device - Syncs to Garmin Connect - Appears in activity history - Includes all sensor data **Discard** (`discardSession()`): + ```monkey-c activitySession.discard(); ``` + - Deletes session completely - No FIT file created - No sync to Garmin Connect @@ -823,10 +886,13 @@ activitySession.discard(); ## Haptic Feedback System ### Overview + The haptic feedback system provides real-time tactile alerts when the runner's cadence drifts outside their optimal zone. This helps maintain proper running form without constantly looking at the watch. ### Design Philosophy + **Timer-Free Architecture**: Instead of creating additional timers (which are limited on Garmin devices), the system piggybacks on the existing 1-second view refresh cycle. This approach: + - Eliminates "Too Many Timers" errors - Reduces memory overhead - Maintains precise timing through timestamp tracking @@ -840,39 +906,43 @@ graph LR B -->|Below Min| C[Single Buzz] B -->|Above Max| D[Double Buzz] B -->|In Zone| E[No Alert] - + C --> F[200ms vibration] D --> G[200ms vibration] G --> H[240ms pause] H --> I[200ms vibration] - + F --> J[Repeat every 30s for 3 min] I --> J - + style C fill:#9999ff style D fill:#ff9999 style E fill:#99ff99 ``` **Single Buzz** (Below Minimum Cadence): + - Pattern: One 200ms vibration - Meaning: Speed up your steps - Repeat: Every 30 seconds - Duration: 3 minutes max **Double Buzz** (Above Maximum Cadence): + - Pattern: Two 200ms vibrations with 240ms gap - Meaning: Slow down your steps - Repeat: Every 30 seconds - Duration: 3 minutes max **No Alert** (In Target Zone): + - Pattern: Silence - Meaning: Perfect cadence, keep going! ### Implementation Details #### Zone State Tracking + ```monkey-c private var _lastZoneState = 0; // -1 = below, 0 = in zone, 1 = above @@ -887,6 +957,7 @@ if (cadence < minZone) { ``` #### Alert Triggering Logic + ```monkey-c if (newZoneState != _lastZoneState) { if (newZoneState == -1) { @@ -906,6 +977,7 @@ if (newZoneState != _lastZoneState) { ``` #### Alert Cycle Management + ```monkey-c function startAlertCycle() as Void { _alertStartTime = System.getTimer(); @@ -915,21 +987,21 @@ function startAlertCycle() as Void { function checkAndTriggerAlerts() as Void { if (_alertStartTime == null) { return; } - + var currentTime = System.getTimer(); var elapsed = currentTime - _alertStartTime; - + // Stop after 3 minutes if (elapsed >= 180000) { _alertStartTime = null; return; } - + // Check if 30 seconds passed since last alert var timeSinceLastAlert = currentTime - _lastAlertTime; if (timeSinceLastAlert >= 30000) { _lastAlertTime = currentTime; - + if (_lastZoneState == -1) { triggerSingleVibration(); } else if (_lastZoneState == 1) { @@ -940,13 +1012,14 @@ function checkAndTriggerAlerts() as Void { ``` #### Double Buzz Implementation + ```monkey-c function triggerDoubleVibration() as Void { if (Attention has :vibrate) { // First vibration var vibeData = [new Attention.VibeProfile(50, 200)]; Attention.vibrate(vibeData); - + // Schedule second vibration _pendingSecondVibe = true; _secondVibeTime = System.getTimer() + 240; @@ -984,10 +1057,11 @@ graph TD ``` **SimpleView Integration**: + ```monkey-c function displayCadence() as Void { // ... update UI elements ... - + // Determine zone state var newZoneState = 0; if (currentCadence < minZone) { @@ -995,7 +1069,7 @@ function displayCadence() as Void { } else if (currentCadence > maxZone) { newZoneState = 1; } - + // Handle zone transitions if (newZoneState != _lastZoneState) { // Trigger appropriate alert and start cycle @@ -1007,6 +1081,7 @@ function displayCadence() as Void { ``` **AdvancedView Integration**: + ```monkey-c function checkCadenceZone() as Void { // Get activity info and determine zone @@ -1033,12 +1108,14 @@ Actual timing variance: ±1 second (due to 1Hz refresh rate) ### Memory Overhead **Additional Memory per View**: + - State tracking: 3 integers (12 bytes) - Timestamps: 3 longs (24 bytes) - Boolean flags: 1 boolean (1 byte) - **Total: ~40 bytes per view** **No Additional Timers Required**: + - Reuses existing `_refreshTimer` (SimpleView) - Reuses existing `_simulationTimer` (AdvancedView) - Zero timer creation overhead @@ -1050,28 +1127,28 @@ sequenceDiagram participant Runner participant Watch participant App - + Runner->>Watch: Start activity Watch->>App: BEGIN RECORDING - + Note over App: Monitoring cadence... - + Runner->>Watch: Cadence drops to 115 SPM App->>Watch: [SINGLE BUZZ] Note over Watch: Below minimum alert - + Note over App: Wait 30 seconds... - + App->>Watch: [SINGLE BUZZ] Note over Watch: Still below minimum - + Runner->>Watch: Increases cadence to 145 SPM Note over App: Back in zone - stop alerts - + Runner->>Watch: Cadence spikes to 165 SPM App->>Watch: [DOUBLE BUZZ] Note over Watch: Above maximum alert - + Runner->>Watch: Reduces cadence to 150 SPM Note over App: Back in zone - stop alerts ``` @@ -1079,6 +1156,7 @@ sequenceDiagram ### Haptic Feedback Best Practices **For Developers**: + 1. Always check `Attention has :vibrate` before calling vibration 2. Reuse existing timers rather than creating new ones 3. Use timestamp-based tracking for precise intervals @@ -1114,6 +1192,7 @@ Potential improvements to the haptic system: ## Cadence Quality Algorithm ### Overview + The Cadence Quality (CQ) score is a composite metric that measures running efficiency based on two factors: 1. **Time in Zone** (70% weight): Percentage of time spent within ideal cadence range @@ -1138,44 +1217,47 @@ graph TD **Purpose**: Measures what percentage of your running time is spent at the optimal cadence **Formula**: + ``` Time in Zone % = (samples in zone / total samples) × 100 ``` **Implementation**: + ```monkey-c function computeTimeInZoneScore() as Number { if (_cadenceCount < MIN_CQ_SAMPLES) { return -1; // Not enough data yet } - + 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(); } ``` **Example**: + - 280 total samples collected - 210 samples within zone [145-155 SPM] - Time in Zone = (210/280) × 100 = 75% @@ -1185,47 +1267,50 @@ function computeTimeInZoneScore() as Number { **Purpose**: Measures cadence consistency (low variance = better form) **Formula**: + ``` Average Difference = Σ |current - previous| / number of transitions Smoothness % = 100 - (average difference × 10) ``` **Implementation**: + ```monkey-c function computeSmoothnessScore() as Number { if (_cadenceCount < MIN_CQ_SAMPLES) { return -1; } - + 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++; } } - + if (diffCount == 0) { return -1; } - + var avgDiff = totalDiff / diffCount; var rawScore = 100 - (avgDiff * 10); - + // Clamp to 0-100 range if (rawScore < 0) { rawScore = 0; } if (rawScore > 100) { rawScore = 100; } - + return rawScore; } ``` **Example**: + - Sample transitions: 145→148 (3), 148→147 (1), 147→150 (3) - Average difference = (3+1+3)/3 = 2.33 - Smoothness = 100 - (2.33 × 10) = 76.7% @@ -1233,40 +1318,43 @@ function computeSmoothnessScore() as Number { ### Final CQ Score **Weighted Combination**: + ``` CQ = (Time in Zone × 0.7) + (Smoothness × 0.3) ``` **Implementation**: + ```monkey-c function computeCadenceQualityScore() as Number { var timeInZone = computeTimeInZoneScore(); var smoothness = computeSmoothnessScore(); - + if (timeInZone < 0 || smoothness < 0) { return -1; // Not enough data } - + var cq = (timeInZone * 0.7) + (smoothness * 0.3); return cq.toNumber(); } ``` **Example**: + - Time in Zone = 75% - Smoothness = 76.7% - CQ = (75 × 0.7) + (76.7 × 0.3) = 52.5 + 23.01 = **75.5%** ### CQ Score Interpretation -| Score Range | Rating | Interpretation | -|-------------|--------|----------------| -| 90-100% | Excellent | Elite running form | -| 80-89% | Very Good | Consistent optimal cadence | -| 70-79% | Good | Generally on target | -| 60-69% | Fair | Room for improvement | -| 50-59% | Poor | Frequent zone violations | -| 0-49% | Very Poor | Needs significant work | +| Score Range | Rating | Interpretation | +| ----------- | --------- | -------------------------- | +| 90-100% | Excellent | Elite running form | +| 80-89% | Very Good | Consistent optimal cadence | +| 70-79% | Good | Generally on target | +| 60-69% | Fair | Room for improvement | +| 50-59% | Poor | Frequent zone violations | +| 0-49% | Very Poor | Needs significant work | ### Confidence Calculation @@ -1277,10 +1365,10 @@ function computeCQConfidence() as String { if (_cadenceCount < MIN_CQ_SAMPLES) { return "Low"; } - + var missingRatio = _missingCadenceCount.toFloat() / (_cadenceCount + _missingCadenceCount).toFloat(); - + if (missingRatio > 0.2) { return "Low"; // >20% missing data } else if (missingRatio > 0.1) { @@ -1300,24 +1388,24 @@ function computeCQTrend() as String { if (_cqHistory.size() < 5) { return "Insufficient data"; } - + // Compare recent half vs. older half var midpoint = _cqHistory.size() / 2; var olderAvg = 0.0; var recentAvg = 0.0; - + for (var i = 0; i < midpoint; i++) { olderAvg += _cqHistory[i]; } olderAvg /= midpoint; - + for (var i = midpoint; i < _cqHistory.size(); i++) { recentAvg += _cqHistory[i]; } recentAvg /= (_cqHistory.size() - midpoint); - + var diff = recentAvg - olderAvg; - + if (diff > 5) { return "Improving"; } else if (diff < -5) { @@ -1335,29 +1423,30 @@ When the activity is stopped, the final CQ score is frozen and written to the FI ```monkey-c function stopRecording() as Void { // ... stop activity session ... - + 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(); } - + _sessionState = STOPPED; } ``` This frozen CQ score: + - Appears in the activity summary - Syncs to Garmin Connect - Provides historical tracking @@ -1370,6 +1459,7 @@ This frozen CQ score: ### SimpleView (Main Display) **Layout**: + ``` ┌─────────────────────┐ │ [REC] 00:12:34 │ ← Time + Recording Indicator @@ -1386,6 +1476,7 @@ This frozen CQ score: ``` **Color Coding**: + - **Green**: Cadence in optimal zone - **Blue**: Slightly below zone (within threshold) - **Grey**: Well below zone @@ -1393,6 +1484,7 @@ This frozen CQ score: - **Red**: Well above zone **Haptic Feedback Integration**: + - Single buzz when cadence drops below minimum - Double buzz when cadence exceeds maximum - Repeats every 30 seconds if still out of zone @@ -1401,6 +1493,7 @@ This frozen CQ score: ### AdvancedView (Chart Display) **Layout**: + ``` ┌─────────────────────┐ │ 1:23:45 │ ← Session Time @@ -1418,6 +1511,7 @@ This frozen CQ score: ``` **Chart Features**: + - 280 bars representing 28 minutes of data - Fixed vertical scale (0-200 SPM) - Color-coded bars matching zone status @@ -1425,6 +1519,7 @@ This frozen CQ score: - Smooth scrolling as new data arrives **Haptic Feedback Integration**: + - Same alert patterns as SimpleView - Integrated with chart updates - Visual + tactile feedback for optimal learning @@ -1446,6 +1541,7 @@ graph TD ``` **Button Mapping**: + - **SELECT**: Start/Stop activity or open control menu - **UP**: Navigate to settings or previous view - **DOWN**: Navigate to next view @@ -1455,6 +1551,7 @@ graph TD ### Activity Control Menus **During Recording**: + ``` ┌──────────────────────┐ │ Activity │ @@ -1466,6 +1563,7 @@ graph TD ``` **When Paused**: + ``` ┌──────────────────────┐ │ Activity Paused │ @@ -1476,6 +1574,7 @@ graph TD ``` **After Stopping**: + ``` ┌──────────────────────┐ │ Save Activity? │ @@ -1499,20 +1598,24 @@ graph TD ``` **Profile Settings**: + - Height (cm) - Speed (km/h) - Gender (Male/Female/Other) - Experience Level (Beginner/Intermediate/Advanced) **Customization**: + - Chart Duration (15min/30min/1hr/2hr) **Feedback** (Future): + - Haptic intensity - Alert interval - Alert duration **Cadence Range**: + - Set Min Cadence (manual adjustment) - Set Max Cadence (manual adjustment) @@ -1525,45 +1628,48 @@ graph TD **Purpose**: Calculate personalized ideal cadence based on biomechanics **Formula** (from research): + ``` Reference Cadence = (-1.251 × leg_length) + (3.665 × speed_m/s) + 254.858 Final Cadence = Reference × Experience_Factor ``` **Gender-Specific Adjustments**: + ```monkey-c function idealCadenceCalculator() as Void { var referenceCadence = 0; var userLegLength = _userHeight * 0.53; // 53% of height var userSpeedms = _userSpeed / 3.6; // Convert km/h to m/s - + switch (_userGender) { case Male: - referenceCadence = (-1.268 × userLegLength) + + referenceCadence = (-1.268 × userLegLength) + (3.471 × userSpeedms) + 261.378; break; case Female: - referenceCadence = (-1.190 × userLegLength) + + referenceCadence = (-1.190 × userLegLength) + (3.705 × userSpeedms) + 249.688; break; default: - referenceCadence = (-1.251 × userLegLength) + + referenceCadence = (-1.251 × userLegLength) + (3.665 × userSpeedms) + 254.858; break; } - + referenceCadence *= _experienceLvl; referenceCadence = Math.round(referenceCadence); - - var finalCadence = max(BASELINE_AVG_CADENCE, + + var finalCadence = max(BASELINE_AVG_CADENCE, min(referenceCadence, MAX_CADENCE)); - + _idealMaxCadence = finalCadence + 5; _idealMinCadence = finalCadence - 5; } ``` **Experience Level Multipliers**: + - Beginner: 1.06 (higher cadence for learning) - Intermediate: 1.04 (moderate adjustment) - Advanced: 1.02 (minimal adjustment) @@ -1571,6 +1677,7 @@ function idealCadenceCalculator() as Void { ### Persistent Storage **Storage Keys**: + ```monkey-c const PROP_USER_HEIGHT = "user_height"; const PROP_USER_SPEED = "user_speed"; @@ -1582,6 +1689,7 @@ const PROP_MAX_CADENCE = "max_cadence"; ``` **Save Settings**: + ```monkey-c function saveSettings() as Void { Storage.setValue(PROP_USER_HEIGHT, _userHeight); @@ -1595,6 +1703,7 @@ function saveSettings() as Void { ``` **Load Settings**: + ```monkey-c function loadSettings() as Void { var height = Storage.getValue(PROP_USER_HEIGHT); @@ -1606,6 +1715,7 @@ function loadSettings() as Void { ``` Settings are automatically: + - Loaded on app start - Saved when modified - Persisted between sessions @@ -1617,9 +1727,8 @@ Settings are automatically: --- - -This reference covers the formatting used throughout this documentation. Use it when contributing updates or creating new documentation. markdown can be -intimidating at first, but onve you master it, you will use it for everything. +This reference covers the formatting used throughout this documentation. Use it when contributing updates or creating new documentation. markdown can be +intimidating at first, but onve you master it, you will use it for everything. --- @@ -1629,12 +1738,16 @@ intimidating at first, but onve you master it, you will use it for everything. ```markdown # H1 - Main Title + ## H2 - Major Section + ### H3 - Subsection + #### H4 - Minor Heading ``` **Usage in this doc:** + - H1: Document title only - H2: Major sections (Architecture, Core Components, etc.) - H3: Subsections within major sections @@ -1646,14 +1759,15 @@ intimidating at first, but onve you master it, you will use it for everything. ```markdown **Bold text** for emphasis -*Italic text* for subtle emphasis +_Italic text_ for subtle emphasis `Inline code` for commands, variables, filenames ~~Strikethrough~~ for deprecated content ``` **Examples:** + - **Bold**: Important terms, warnings -- *Italic*: Notes, asides +- _Italic_: Notes, asides - `Code`: `git push`, `_cadenceHistory`, `SimpleView.mc` --- @@ -1667,16 +1781,18 @@ intimidating at first, but onve you master it, you will use it for everything. ``` **Internal link rules:** + - Section names become anchors automatically - Convert to lowercase - Replace spaces with hyphens - Remove special characters or convert to hyphens **Examples:** + ```markdown -[Architecture Overview](#architecture-overview) # Correct -[GitHub Workflow & Collaboration](#github-workflow--collaboration) # & becomes -- -[State Management](#state-management) # Simple case +[Architecture Overview](#architecture-overview) # Correct +[GitHub Workflow & Collaboration](#github-workflow--collaboration) # & becomes -- +[State Management](#state-management) # Simple case ``` --- @@ -1684,6 +1800,7 @@ intimidating at first, but onve you master it, you will use it for everything. ### Lists **Unordered lists:** + ```markdown - Item 1 - Item 2 @@ -1693,6 +1810,7 @@ intimidating at first, but onve you master it, you will use it for everything. ``` **Ordered lists:** + ```markdown 1. First item 2. Second item @@ -1700,6 +1818,7 @@ intimidating at first, but onve you master it, you will use it for everything. ``` **Checklists:** + ```markdown - [ ] Incomplete task - [x] Completed task @@ -1710,6 +1829,7 @@ intimidating at first, but onve you master it, you will use it for everything. ### Code Blocks **Inline code:** + ```markdown Use `git status` to check your working directory. ``` @@ -1734,6 +1854,7 @@ const response = await fetch(url); ```` **Supported languages in this doc:** + - `bash` - Shell commands - `monkey-c` - Monkey C code - `javascript` - JS examples @@ -1746,21 +1867,24 @@ const response = await fetch(url); ### Tables **Basic table:** + ```markdown | Column 1 | Column 2 | Column 3 | -|----------|----------|----------| +| -------- | -------- | -------- | | Data 1 | Data 2 | Data 3 | | Data 4 | Data 5 | Data 6 | ``` **Table with alignment:** + ```markdown | Left-aligned | Center-aligned | Right-aligned | -|:-------------|:--------------:|--------------:| -| Left | Center | Right | +| :----------- | :------------: | ------------: | +| Left | Center | Right | ``` **Tips:** + - Use `|:---` for left align (default) - Use `|:---:|` for center align - Use `|---:|` for right align @@ -1798,6 +1922,7 @@ Mermaid creates diagrams from text. All diagrams must be in fenced code blocks w ### Flowcharts (Graph) **Basic syntax:** + ````markdown ```mermaid graph TD @@ -1811,37 +1936,41 @@ graph TD ```` **Node shapes:** + ```markdown -A[Rectangle] # Square corners -B(Rounded) # Rounded corners -C([Stadium]) # Pill shape -D[[Subroutine]] # Double border -E[(Database)] # Cylinder -F((Circle)) # Circle -G>Flag] # Flag shape -H{Diamond} # Diamond (decision) -I{{Hexagon}} # Hexagon +A[Rectangle] # Square corners +B(Rounded) # Rounded corners +C([Stadium]) # Pill shape +D[[Subroutine]] # Double border +E[(Database)] # Cylinder +F((Circle)) # Circle +G>Flag] # Flag shape +H{Diamond} # Diamond (decision) +I{{Hexagon}} # Hexagon ``` **Arrow types:** + ```markdown -A --> B # Solid arrow -A -.-> B # Dotted arrow -A ==> B # Thick arrow -A --- B # Line (no arrow) -A -- Text --> B # Labeled arrow -A -->|Text| B # Labeled arrow (compact) +A --> B # Solid arrow +A -.-> B # Dotted arrow +A ==> B # Thick arrow +A --- B # Line (no arrow) +A -- Text --> B # Labeled arrow +A -->|Text| B # Labeled arrow (compact) ``` **Direction:** + ```markdown -graph TD # Top to Down -graph LR # Left to Right -graph BT # Bottom to Top -graph RL # Right to Left +graph TD # Top to Down +graph LR # Left to Right +graph BT # Bottom to Top +graph RL # Right to Left ``` **Example from this doc (Data Flow):** + ````markdown ```mermaid graph TD @@ -1858,15 +1987,16 @@ graph TD ### Sequence Diagrams **Basic syntax:** + ````markdown ```mermaid sequenceDiagram participant A as Alice participant B as Bob - + A->>B: Hello Bob! B->>A: Hi Alice! - + Note over A,B: This is a note Note right of A: Note on right Note left of B: Note on left @@ -1874,6 +2004,7 @@ sequenceDiagram ```` **Arrow types:** + ```markdown A->>B: Solid arrow (message) A-->>B: Dotted arrow (return) @@ -1881,19 +2012,21 @@ A-xB: Cross (lost message) ``` **Special syntax:** + ```markdown alt Alternative 1 - A->>B: Do this +A->>B: Do this else Alternative 2 - A->>B: Do that +A->>B: Do that end loop Every 30s - A->>B: Repeat this +A->>B: Repeat this end ``` **Example from this doc (Haptic Alerts):** + ````markdown ```mermaid sequenceDiagram @@ -1901,10 +2034,10 @@ sequenceDiagram participant View participant ZoneChecker participant HapticManager - + User->>View: Running (cadence changes) View->>ZoneChecker: Check current cadence - + alt Cadence drops below minimum ZoneChecker->>HapticManager: Trigger single buzz else Cadence exceeds maximum @@ -1920,6 +2053,7 @@ sequenceDiagram ### State Diagrams **Basic syntax:** + ````markdown ```mermaid stateDiagram-v2 @@ -1927,7 +2061,7 @@ stateDiagram-v2 State1 --> State2: Transition State2 --> State3: Another transition State3 --> [*] - + note right of State1 This is a note end note @@ -1935,6 +2069,7 @@ stateDiagram-v2 ```` **Example from this doc (Session States):** + ````markdown ```mermaid stateDiagram-v2 @@ -1944,7 +2079,7 @@ stateDiagram-v2 PAUSED --> RECORDING: resumeRecording() RECORDING --> STOPPED: stopRecording() STOPPED --> IDLE: saveSession() / discardSession() - + note right of RECORDING Timer active Data collecting @@ -1959,15 +2094,16 @@ stateDiagram-v2 ### When to Use Which Diagram -| Diagram Type | Use When | Example in This Doc | -|--------------|----------|---------------------| -| **Flowchart** | Showing process flow, data pipeline, decision trees | Data collection pipeline, CI workflow | -| **Sequence Diagram** | Showing interactions over time between components | Haptic alert timing, merge conflict scenario | -| **State Diagram** | Showing state transitions and lifecycle | Session state machine | +| Diagram Type | Use When | Example in This Doc | +| -------------------- | --------------------------------------------------- | -------------------------------------------- | +| **Flowchart** | Showing process flow, data pipeline, decision trees | Data collection pipeline, CI workflow | +| **Sequence Diagram** | Showing interactions over time between components | Haptic alert timing, merge conflict scenario | +| **State Diagram** | Showing state transitions and lifecycle | Session state machine | ### Formatting Guidelines **DO:** + - Use consistent header levels (don't skip levels) - Include code language tags in fenced blocks - Use tables for structured data comparisons @@ -1976,6 +2112,7 @@ stateDiagram-v2 - Use relative links for internal references **DON'T:** + - Use HTML unless absolutely necessary - Skip header levels (H2 → H4 without H3) - Use images for text content (accessibility) @@ -1985,6 +2122,7 @@ stateDiagram-v2 ### Code Block Guidelines **For commands:** + ```bash # Good: Show full command with context git fetch origin @@ -1996,6 +2134,7 @@ git push -f ``` **For code:** + ```monkey-c // Good: Include relevant context and comments function startRecording() as Void { @@ -2013,19 +2152,22 @@ activitySession.start(); ### Table Guidelines **Comparison tables:** + - Left column: Item being compared - Other columns: Attributes or options - Use bold for headers **Decision tables:** + - Left column: Condition/situation - Right columns: Action or outcome - Consider using "When/Action/Command" structure **Examples:** + ```markdown -| When | Action | Command | -|------|--------|---------| +| When | Action | Command | +| ---------- | ----------------------- | ------------------------------ | | Start work | Branch from latest main | `git checkout -b feature/name` | ``` @@ -2036,22 +2178,26 @@ activitySession.start(); ### Common Issues **Diagram not rendering?** + 1. Check for typos in `mermaid` tag 2. Ensure proper indentation 3. Close all parentheses and brackets 4. Check for special characters in node names **Arrows not connecting?** + - Make sure node IDs match exactly (case-sensitive) - Check arrow syntax (`-->` not `->`) **Text not showing?** + - Wrap text with spaces in quotes: `A["My Text"]` - Use `|Text|` for inline labels on arrows ### Testing Diagrams **Before committing:** + 1. View in GitHub's preview tab 2. Or use online editor: https://mermaid.live 3. Check that text is readable @@ -2090,6 +2236,7 @@ activitySession.start(); ## Quick Reference: Common Patterns ### Section Header Pattern + ```markdown --- @@ -2102,25 +2249,31 @@ Brief introduction paragraph explaining what this section covers. Content here... **Key Points:** + - Point 1 - Point 2 - Point 3 ``` ### Code Example Pattern -```markdown + +````markdown **Implementation:** + ```monkey-c function example() as Void { // Explanation comment var result = doSomething(); } ``` +```` **What this does:** + - Explains the code - Provides context -``` + +```` ### Comparison Table Pattern ```markdown @@ -2129,24 +2282,18 @@ function example() as Void { | Speed | Fast | Slow | | Memory | High | Low | | Complexity | Simple | Complex | -``` +```` --- --- - - - - - - - ## Features Reference ### Current Features (v1.0) ✅ **Core Functionality** + - Real-time cadence monitoring - 28-minute rolling histogram - Cadence Quality (CQ) scoring @@ -2155,6 +2302,7 @@ function example() as Void { - Save/Discard workflow ✅ **User Interface** + - SimpleView (main display) - AdvancedView (chart visualization) - Settings menus @@ -2162,6 +2310,7 @@ function example() as Void { - Recording indicator ✅ **Smart Features** + - Personalized cadence zones - Gender-specific calculations - Experience level adjustment @@ -2170,6 +2319,7 @@ function example() as Void { - **Haptic zone alerts** ✅ **Data Management** + - Circular buffer storage - Two-tier averaging system - Persistent settings @@ -2179,6 +2329,7 @@ function example() as Void { ### Haptic Feedback Feature (v1.1) ✅ **Alert Patterns** + - Single buzz for below-zone cadence - Double buzz for above-zone cadence - 30-second repeat interval @@ -2186,6 +2337,7 @@ function example() as Void { - Automatic stop on zone re-entry ✅ **Technical Implementation** + - Timer-free architecture - Timestamp-based tracking - Integrated with view refresh cycle @@ -2193,6 +2345,7 @@ function example() as Void { - Works on both SimpleView and AdvancedView ✅ **User Benefits** + - No need to constantly watch screen - Tactile feedback during runs - Non-intrusive alerts @@ -2202,6 +2355,7 @@ function example() as Void { ### Future Enhancements #### 🔴 High Priority + 1. **Configurable Alert Settings** - Alert interval (15s/30s/45s/60s) - Alert duration (1min/3min/5min/continuous) @@ -2218,6 +2372,7 @@ function example() as Void { - Optimize bar calculations #### 🟡 Medium Priority + 4. **Smooth Bars** - Gradient transitions between zones - Anti-aliased rendering @@ -2239,6 +2394,7 @@ function example() as Void { - Smart zone boundaries #### 🟢 Low Priority + 8. **Fade Old Bars** - Opacity gradient for time perspective - Highlight recent data @@ -2274,6 +2430,7 @@ function example() as Void { ## Implementation Priority Matrix ### Phase 1: Core Stability (Completed) + - ✅ State machine - ✅ Activity recording - ✅ Pause/Resume @@ -2281,17 +2438,20 @@ function example() as Void { - ✅ Basic haptic alerts ### Phase 2: User Customization (Current) + - 🔄 Configurable alert settings - 🔄 Battery optimization - 🔄 Chart rendering optimization ### Phase 3: Advanced Features (Future) + - 📋 Smooth bars - 📋 Zone boundary lines - 📋 Statistical overlays - 📋 Terrain-adaptive zones ### Phase 4: Polish & Enhancement (Future) + - 📋 Fade old bars - 📋 Auto-adjust chart duration - 📋 CSV export @@ -2304,6 +2464,7 @@ function example() as Void { ## Technical Debt & Code Quality ### Refactoring Needed + - [ ] Extract chart rendering to `ChartRenderer.mc` class - [ ] Create `CircularBuffer.mc` reusable class - [ ] Consolidate color constants into `Colors.mc` @@ -2312,6 +2473,7 @@ function example() as Void { - [ ] Document all public methods with JSDoc-style comments ### Testing & Quality + - [ ] Add unit tests for CQ algorithm - [ ] Add integration tests for state machine - [ ] Add haptic feedback timing tests @@ -2321,6 +2483,7 @@ function example() as Void { - [ ] Test haptic alerts across rapid zone transitions ### Performance Profiling Targets + - [ ] Chart draw time: <50ms per frame - [ ] Memory usage: <5% of total device memory - [ ] Battery drain: <5% per hour (GPS active) @@ -2334,14 +2497,16 @@ function example() as Void { **Issue**: Haptic alerts not firing **Cause**: Attention module not supported or state not RECORDING -**Solution**: +**Solution**: + - Check `Attention has :vibrate` capability - Verify `_sessionState == RECORDING` - Confirm cadence is actually out of zone **Issue**: Alerts continue after returning to zone **Cause**: Zone state not properly updated -**Solution**: +**Solution**: + - Check `_lastZoneState` variable - Verify zone detection logic - Ensure `stopAlertCycle()` is called @@ -2349,6 +2514,7 @@ function example() as Void { **Issue**: Double buzz only fires once **Cause**: `_pendingSecondVibe` not being checked **Solution**: + - Confirm `checkPendingVibration()` called in `onUpdate()` - Verify `_secondVibeTime` calculation - Check timer precision @@ -2384,6 +2550,7 @@ function example() as Void { ### Haptic Debugging **Enable Haptic Debug Logging**: + ```monkey-c // In triggerSingleVibration() System.println("[HAPTIC] Single buzz triggered at " + System.getTimer()); @@ -2396,6 +2563,7 @@ System.println("[HAPTIC] Time since last alert: " + timeSinceLastAlert); ``` **Test Haptic Timing**: + 1. Start recording 2. Manually set cadence out of zone 3. Note timestamp of first alert @@ -2410,6 +2578,7 @@ System.println("[HAPTIC] Time since last alert: " + timeSinceLastAlert); **Current Version**: 1.1 (January 2026) **v1.1 Changes**: + - ✅ Added: Haptic feedback system - Single buzz for below-zone cadence - Double buzz for above-zone cadence @@ -2422,6 +2591,7 @@ System.println("[HAPTIC] Time since last alert: " + timeSinceLastAlert); - ✅ Updated: Documentation with flow diagrams **v1.0 Changes** (from original): + - ✅ Fixed: Uncommented critical recording check (line 270) - ✅ Added: Full state machine (IDLE/RECORDING/PAUSED/STOPPED) - ✅ Added: Pause/Resume functionality @@ -2433,6 +2603,7 @@ System.println("[HAPTIC] Time since last alert: " + timeSinceLastAlert); - ✅ Added: Comprehensive documentation **Known Limitations**: + - No persistent storage of CQ history - No lap/split functionality - No custom alert thresholds @@ -2469,6 +2640,7 @@ System.println("[HAPTIC] Time since last alert: " + timeSinceLastAlert); **Last Updated**: January 2026 ## Special Mentions for their amazing work this semester. + **Dom** **Chum** **Jack** @@ -2476,5 +2648,3 @@ System.println("[HAPTIC] Time since last alert: " + timeSinceLastAlert); **Jin** --- - - From 7277c9a3dcbfa51d776706329764af390ec5b07d Mon Sep 17 00:00:00 2001 From: Gargi2023 Date: Sun, 1 Feb 2026 22:08:55 +1100 Subject: [PATCH 3/5] docs: document memory leak and timing stress testing (Member A) --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5dbf962..6203acf 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,50 @@ Significant development time was spent on debugging, validation, and traceabilit - No activity recording - No FIT file generation + ### Memory & Timing Validation + +Validating the **runtime stability, timer accuracy, and application lifecycle behaviour** of the cadence monitoring system. +--- + +### Timer Accuracy (1-Second Tick Validation) + +Cadence sampling is driven by a repeating timer configured to execute at a strict **1-second interval**, ensuring predictable data collection and UI updates. + +//monkeyc +globalTimer = new Timer.Timer(); +globalTimer.start(method(:updateCadenceBarAvg), 1000, true); + +Evidence: +Simulator terminal logs showing repeated [TIMER] Tick messages at 1-second intervals. + +### Memory Stress Testing (200+ Timer Cycles) + +The application was stress tested across 200+ timer cycles while cadence monitoring remained active. Memory diagnostics were periodically logged during runtime to detect leaks or heap growth. + +Observed results: + +-Stable memory usage (~5–6% of available heap) +-No monotonic increase in memory consumption +-No crashes or garbage collection pressure + +This confirms that timer callbacks, cadence buffers, and Cadence Quality computations do not introduce memory leaks. + +Evidence: +Simulator logs displaying consistent [MEMORY] Runtime values after extended execution. + +###Application Lifecycle Validation (Startup, Pause, Shutdown) + +Correct lifecycle handling was validated to ensure safe resource management and prevent unintended background execution. + +-Timer initialised during application startup +-Activity safely paused via user interaction +-Timer explicitly stopped and released during application shutdown + +Simulator logs confirmed clean startup, correct pause behaviour, and graceful shutdown with no residual timer activity. + +Evidence: +Logs showing pause events followed by clean application termination. + ## 🎯 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. @@ -163,8 +207,8 @@ 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 -``` + +```monkeyc -o TestingCadence.prg -f monkey.jungle -y developer_key.der -w Run in the simulator: From dbe4fd53478c0cd6d4f3e5667dde4dd72c8fde48 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 6 Feb 2026 16:42:23 +1100 Subject: [PATCH 4/5] Updated technical Documetnation to add section on sideloading app --- TECHNICAL_DOCUMENTATION_v3.md | 2883 ++++++++++++++++ combined-source.txt | 5866 +++++++++++++++++---------------- 2 files changed, 5950 insertions(+), 2799 deletions(-) create mode 100644 TECHNICAL_DOCUMENTATION_v3.md diff --git a/TECHNICAL_DOCUMENTATION_v3.md b/TECHNICAL_DOCUMENTATION_v3.md new file mode 100644 index 0000000..02f72ea --- /dev/null +++ b/TECHNICAL_DOCUMENTATION_v3.md @@ -0,0 +1,2883 @@ +# Redback Operations Garmin App - Technical Documentation + +## Concept & Vision + +The brain child of Drs Jason Bonacci and Joel Fuller - A Garmin watch-app that turns the built-in cadence sensor into a **real-time running-efficiency coach that can be used for rehabilitation or hard core trainers alike**. +While you run it shows: + +- Live cadence vs. your **personal ideal zone** (calculated from height, speed, gender, experience) +- A **28-minute rolling histogram** that colour-codes every stride +- A **0-100 % "Cadence Quality" (CQ)** score that is frozen when you stop and is written into the FIT file so it follows the activity into Garmin Connect +- **Smart haptic alerts** that vibrate when you drift out of your optimal cadence zone + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Build Process](#build-process) +3. [Sideloading the App onto a Garmin Watch](#sideloading-the-app-onto-a-garmin-watch) +4. [GitHub Workflow & Collaboration](#github-workflow--collaboration) +5. [Architecture Overview](#architecture-overview) +6. [Core Components](#core-components) +7. [Data Flow](#data-flow) +8. [State Management](#state-management) +9. [Activity Recording System](#activity-recording-system) +10. [Haptic Feedback System](#haptic-feedback-system) +11. [Cadence Quality Algorithm](#cadence-quality-algorithm) +12. [User Interface](#user-interface) +13. [Settings System](#settings-system) +14. [Documentation Reference](#documentation-reference) +15. [Features Reference](#features-reference) + +--- + +## Prerequisites + +- Garmin Connect IQ SDK 8.3.0+ +- Visual Studio Code with Connect IQ extension +- Forerunner 165/165 Music device or simulator + +## Build Process + +1. Clone repository +2. Configure project settings in `monkey.jungle` +3. Build for target device: + ```bash + monkeyc -o bin/app.prg -f monkey.jungle -y developer_key.der + **You need to have generated a devopler key for this** + ``` + +--- + +## Sideloading the App onto a Garmin Watch + +After building the `.prg` file, you need to transfer it to your Garmin watch for testing. This process is called "sideloading" and allows you to run development builds without publishing to the Connect IQ Store. + +### Prerequisites + +- Built `.prg` file (from Build Process above) +- Garmin watch connected via USB cable +- Watch must be in Developer Mode (see below) + +### Step 1: Enable Developer Mode on Your Watch + +Before you can sideload apps, your Garmin device must be in Developer Mode: + +1. **On the watch**: Go to Settings → System → About +2. **Note your Unit ID** (you'll need this for the developer key) +3. **Enable Developer Mode**: + - **Forerunner 165/265/965**: Settings → System → USB Mode → Set to "Garmin" (not MTP) + - **Other devices**: May have a dedicated Developer Mode toggle in System settings + +**Note**: Some devices automatically enable Developer Mode when connected to a computer running the Connect IQ SDK tools. + +### Step 2: Connect Watch to Computer + +1. Connect your Garmin watch to your computer using the USB charging cable +2. The watch should mount as a USB drive (may appear as "GARMIN" or similar) +3. Wait for the device to be fully recognized by your operating system + +**Troubleshooting connection issues**: +- Try a different USB port (prefer USB 2.0 ports for better compatibility) +- Ensure the cable is a data cable, not charge-only +- Restart the watch if it doesn't mount +- On macOS: Check if it appears in Finder under Locations +- On Windows: Check if it appears in File Explorer +- On Linux: Check `/media/` or run `lsblk` to see mounted devices + +### Step 3: Locate the APPS Folder + +Once mounted, navigate to the device: + +**Windows**: +``` +E:\GARMIN\APPS\ +(Drive letter may vary - typically D:, E:, or F:) +``` + +**macOS**: +``` +/Volumes/GARMIN/GARMIN/APPS/ +``` + +**Linux**: +``` +/media/[username]/GARMIN/GARMIN/APPS/ +(or wherever your device automounts) +``` + +If the `APPS` folder doesn't exist, create it: +- Navigate to `GARMIN/` directory +- Create a new folder named `APPS` + +### Step 4: Copy the .prg File + +1. Locate your built `.prg` file: + ``` + your-project-directory/bin/app.prg + ``` + +2. Copy the `.prg` file into the `APPS` folder on your watch + +3. **Rename the file** (optional but recommended for organization): + ``` + CadenceMonitor.prg + ``` + + Naming convention: Use descriptive names without spaces. Good examples: + - `CadenceMonitor.prg` + - `CadenceMonitor_v1.1.prg` + - `CM_debug.prg` + +### Step 5: Safely Eject and Disconnect + +**Important**: Always safely eject to prevent data corruption! + +**Windows**: +1. Right-click the GARMIN drive in File Explorer +2. Select "Eject" +3. Wait for "Safe to Remove Hardware" notification +4. Disconnect USB cable + +**macOS**: +1. Click the eject icon next to GARMIN in Finder +2. Or drag the GARMIN icon to the Trash +3. Wait until it disappears from Finder +4. Disconnect USB cable + +**Linux**: +1. Right-click the device in file manager and select "Unmount" or "Eject" +2. Or use terminal: `umount /media/[username]/GARMIN` +3. Disconnect USB cable + +### Step 6: Access the App on Your Watch + +1. Disconnect the watch from the computer +2. On the watch, navigate to your app: + - Press the "Up" button to open the app menu + - Scroll to find your sideloaded app (appears with a wrench icon 🔧) + - The app name will be what you specified in `monkey.jungle` + +3. Select the app to launch it + +### Sideloading via Command Line (Advanced) + +For faster iteration during development, you can use the Connect IQ SDK tools: + +**Using monkeydo (with simulator)**: +```bash +monkeydo bin/app.prg [device_name] +``` + +**Using connectiq (push to physical device)**: +```bash +# Install the Connect IQ app on your phone first +# Then push via WiFi: +connectiq push bin/app.prg [device_name] +``` + +**Note**: This requires: +- Connect IQ Mobile app installed on your phone +- Watch paired to phone via Bluetooth +- Phone and computer on same WiFi network + +### Multiple Sideloaded Apps + +You can have multiple `.prg` files in the `APPS` folder simultaneously: +- Each appears as a separate app on the watch +- Useful for testing different versions or configurations +- Device memory limitations apply (typically 8-15 sideloaded apps) + +### Removing Sideloaded Apps + +**Method 1: Via Watch Interface**: +1. Navigate to the app in the app menu +2. Hold the "Menu" button (typically bottom left button) +3. Select "Remove" or "Delete" + +**Method 2: Via USB Connection**: +1. Connect watch via USB +2. Navigate to `GARMIN/APPS/` +3. Delete the `.prg` file +4. Safely eject + +### Common Sideloading Issues + +**Issue**: App doesn't appear after copying +**Cause**: File not properly ejected or corrupted during transfer +**Solution**: +- Reconnect watch and verify the `.prg` file is present and has the correct size +- Delete and re-copy the file +- Ensure proper safe eject procedure was followed + +**Issue**: App crashes immediately on launch +**Cause**: Code error, memory issue, or incompatible device +**Solution**: +- Check the device target in `monkey.jungle` matches your watch +- Review console logs during build for warnings +- Test in simulator first: `monkeydo bin/app.prg fr165` + +**Issue**: "Invalid PRG file" error +**Cause**: File corrupted or not signed with developer key +**Solution**: +- Rebuild with proper developer key: `monkeyc ... -y developer_key.der` +- Verify the developer key matches your device's Unit ID +- Generate a new developer key if needed (see Connect IQ SDK documentation) + +**Issue**: Watch only shows empty folder +**Cause**: Wrong APPS folder location +**Solution**: +- Ensure path is `GARMIN/APPS/` not just `APPS/` at root level +- Some devices use `PRIMARY/GARMIN/APPS/` + +**Issue**: "App limit reached" message +**Cause**: Too many sideloaded apps on device +**Solution**: +- Remove unused sideloaded apps +- Typical limit: 8-15 development apps depending on device memory + +### Development Iteration Workflow + +For efficient development cycles: + +1. **Build** the app: + ```bash + monkeyc -o bin/app.prg -f monkey.jungle -y developer_key.der + ``` + +2. **Connect and copy** (or use a script to automate): + ```bash + # Example script (adjust paths) + cp bin/app.prg /Volumes/GARMIN/GARMIN/APPS/CadenceMonitor.prg + sync # Flush filesystem buffer + diskutil unmount /Volumes/GARMIN # macOS + ``` + +3. **Test on device** + +4. **Iterate**: Disconnect, make changes, rebuild, reconnect + +### Alternative: Using the Simulator + +For rapid testing without device connection: + +```bash +# Launch in simulator +monkeydo bin/app.prg fr165 + +# Or use Visual Studio Code Connect IQ extension +# (Build → Run → Select device) +``` + +Benefits: +- Faster iteration (no USB connection) +- Access to debug console +- Multiple device testing + +Limitations: +- Sensor behavior may differ +- Performance characteristics differ from physical hardware +- Haptic feedback not available in simulator + +--- + +## GitHub Workflow & Collaboration + +**Note:** This workflow represents one approach to Git collaboration. Other valid philosophies exist (Git Flow, Trunk-Based Development, etc.). This document is written based on professional development experience and some parts may not directly apply to this specific project, but should help build understanding of collaborative Git workflows. + +### My Philosophy: "Main is Sacred, Feature Branches are Disposable" + +This project follows a **rebase-focused, fast-merge workflow** designed to keep the repository history clean and minimize merge conflicts. This approach is particularly beneficial for teams new to Git and GitHub, as it creates a linear, easy-to-understand history while preventing the dreaded "merge hell." + +### Core Principles + +| Principle | Meaning | Why It Matters | +| ---------------------- | ------------------------------------------ | ------------------------------------------- | +| Main is always green | If it's on `main`, it works and deploys | Broken code never reaches production | +| Rebase, don't merge | Linear history = readable git log | Easy to track down bugs, understand changes | +| Small, fast PRs | Less than 400 lines, less than 3 days open | Easier reviews, less conflict potential | +| Automated enforcement | Machines check; humans review logic | Catches issues before review | +| Sync early, sync often | Rebase on `main` daily | Prevents large, scary conflicts | + +### Why Rebase? The Visual Comparison + +**MERGE (creates complex history):** + +``` +A---B---C-------F---G main + \ / + D---E---/ feature + (creates merge commit bubble) +``` + +**REBASE (clean linear history):** + +``` +A---B---C---F---G---D'---E' main + (feature commits replayed on top) +``` + +**Benefits of Rebasing:** + +- History reads like a book: chronological, no tangles +- `git bisect` works reliably to find bugs +- Each commit can be individually tested +- Code reviews focus on logic, not merge artifacts + +--- + +## Daily Branch Synchronization (Preventing "Out-of-Date" Branches) + +### The Problem Scenario + +```mermaid +sequenceDiagram + participant You + participant YourBranch as feature/vibration + participant Main + participant Teammate + + Note over You,Main: Monday 9am + You->>YourBranch: Create branch from main@100 + + Note over You,Main: Tuesday 3pm + Teammate->>Main: Merge feature/graph-fix (main@105) + + Note over You,Main: Wednesday 10am + You->>YourBranch: Open Pull Request + YourBranch-->>Main: "Warning: Branch is out-of-date" + + Note over You,Main: Thursday 2pm + Teammate->>Main: Merge feature/settings (main@110) + + Note over You,Main: Friday 11am + You->>Main: Attempt merge + Note over Main: Conflicts! Tests fail! Team sad! +``` + +### The Solution: (My) Daily Rebase Ritual + +**Run this every morning** (or after getting coffee): + +```bash / powershell +# Step 1: Fetch latest changes from GitHub +# (Downloads new commits without modifying your files) +git fetch origin + +# Step 2: Optional - See what changed on main +# (Shows commits that have been added since you branched) +git log --oneline --graph origin/main..HEAD + +# Step 3: Save any uncommitted work +# (Rebase requires a clean working directory) +git stash push -m "WIP: morning sync $(date +%Y-%m-%d)" + +# Step 4: Replay your commits on top of latest main +# (This is the key step - makes your branch current) +git rebase origin/main + +# Step 5: Handle conflicts (if any appear) +# Git will pause and show conflicting files +# Edit the files, then: +git add +git rebase --continue + +# If you get stuck or panic: +# git rebase --abort # Returns to pre-rebase state + +# Step 6: Restore your uncommitted work +git stash pop + +# Step 7: Update your remote branch +# (--force-with-lease is safer than --force) +git push --force-with-lease +``` + +### When to Sync Your Branch + +| Timing | Action | Command | +| --------------------- | ----------------------- | --------------------------------------------------------------------- | +| **Start work** | Branch from latest main | `git fetch && git checkout -b feature/name origin/main` | +| **Daily** (minimum) | Rebase on main | `git fetch && git rebase origin/main && git push --force-with-lease` | +| **Before opening PR** | Final rebase + cleanup | `git rebase -i origin/main` (squash "WIP" commits if needed) | +| **After PR approved** | Last sync before merge | `git fetch && git rebase origin/main` | +| **After PR merged** | Delete branch | `git push origin --delete feature/name && git branch -D feature/name` | + +--- + +## Common Situations & Solutions + +### Situation 1: GitHub Shows "This branch is out-of-date" + +**Don't click "Update branch" button** - it creates a merge commit! + +**Instead, use terminal:** + +```bash +git fetch origin +git rebase origin/main +git push --force-with-lease +``` + +Then refresh the PR page - warning disappears. + +### Situation 2: Someone Else Pushed to Your Branch + +If `--force-with-lease` fails with "rejected", someone else modified your branch: + +```bash +# Fetch their changes +git fetch origin + +# Rebase on your remote branch first (get their work) +git rebase origin/feature/your-branch + +# Then rebase on main +git rebase origin/main + +# Communicate with teammate before force-pushing! +git push --force-with-lease +``` + +### Situation 3: Rebase Goes Wrong + +**I have done this more times than i care to remember** + +If you make a mistake during rebase: + +```bash +# Abort and return to pre-rebase state +git rebase --abort + +# Check the reflog to see history +git reflog + +# Jump back to a specific commit if needed +git reset --hard HEAD@{5} # Numbers from reflog +``` + +--- + +## Branch Protection Rules + +**some of these are active on the bramch** + +Our repository enforces these rules on the `main` branch (Settings → Branches): + +| Setting | Value | Why | +| ------------------------------- | ------------ | -------------------------------------------------- | +| **Require pull request** | Enabled | Prevents accidental `git push origin main` | +| **Required approvals** | 1 reviewer | Ensures code review before merge | +| **Dismiss stale approvals** | Enabled | New commits after approval require re-review | +| **Require status checks** | CI must pass | Code must build and pass tests | +| **Require branches up to date** | **Critical** | PR must include latest `main` commits before merge | +| **Allow force pushes** | Disabled | Protects main's history | +| **Allow deletions** | Disabled | Prevents accidental branch deletion | + +**Why "Require branches up to date" is critical:** +This forces you to rebase before merging, ensuring the final merge result was actually tested in CI. Without this, two "green" PRs can be merged sequentially and break `main`. + +--- + +## Branch Naming Conventions + +\*\*this is my preffered naming convention - i try an incorparte naming conventions into everything, even university submissions, so instead of SIT782_5_4HDV3r5edit8 i have a meaning name. + +Use these prefixes to keep branches organized: + +| Prefix | Purpose | Example | +| ----------- | ------------------------------------- | --------------------------------- | +| `feature/` | New functionality | `feature/vibration-alerts` | +| `fix/` | Bug fixes | `fix/timer-crash` | +| `refactor/` | Code improvement (no behavior change) | `refactor/extract-chart-renderer` | +| `docs/` | Documentation only | `docs/api-examples` | +| `hotfix/` | Urgent production fix | `hotfix/memory-leak` | + +**Format:** `prefix/descriptive-name-in-kebab-case` + +**Examples:** + +- Good: `feature/haptic-feedback` +- Good: `fix/null-pointer-crash` +- Bad: `my-branch` (no prefix) +- Bad: `Feature/VibrationStuff` (wrong case) + +--- + +## Pull Request Best Practices + +### PR Size Guidelines + +| Lines Changed | Status | Recommendation | +| ------------------- | --------- | ---------------------------- | +| Less than 200 lines | Excellent | Ideal for fast review | +| 200-400 lines | Large | Consider splitting | +| 400+ lines | Too large | Must split into multiple PRs | + +**Why small PRs are better-ish:** + +- Faster reviews +- Lower chance of conflicts +- Easier to understand changes +- Less likely to introduce bugs + +### PR Template + +\*\*Does my head in when there is no comments or something generic like 'added stuff", admittedly its easier but its a crappy mindset because the person reviewing has no idea. + +When opening a PR, include: + +```markdown +## What + +Brief description of changes (1-2 sentences) + +## Why + +Problem being solved or feature being added + +## How + +Technical approach taken + +## Testing + +How to verify these changes work + +## Screenshots/Videos + +If UI changes, show before/after -- not always relevant +``` + +### Review Process + +1. **Open PR** when code is ready for review (not draft) +2. **Respond to feedback** within 24 hours +3. **Keep PR updated** - rebase when main moves forward +4. **Squash "fix review comments" commits** before final merge +5. **Delete branch** immediately after merge + +--- + +## Continuous Integration (CI) + +Every PR automatically runs: + +```mermaid +graph LR + A[Push to PR] --> B[Lint Check] + B --> C[Type Check] + C --> D[Unit Tests] + D --> E[Integration Tests] + E --> F[Build] + F --> G{All Pass?} + G -->|Yes| H[Ready to Merge] + G -->|No| I[Fix and Push Again] +``` + +**CI must pass before merge button activates.** + +If CI fails: + +1. Check the logs in the "Checks" tab +2. Fix the issue locally +3. Commit and push +4. CI runs automatically again + +--- + +## Merge Strategy + +We use **Squash and Merge** for all PRs: +**this is what i like, there is more than one way to skin this cat, find what works for your project.** + +**Benefits:** + +- Each PR becomes a single commit on `main` +- Clean, readable history +- Easy to revert entire features +- Encourages frequent commits during development + +**Process:** + +1. PR gets approved and CI passes +2. Click "Squash and merge" +3. Edit commit message (auto-generated from PR) +4. GitHub automatically deletes the branch + +--- + +## Alternative Workflows (For Reference ONly) + +While we use rebase-focused workflow, other valid approaches exist: + +### Git Flow + +- Uses `develop` branch as integration branch +- `main` only for releases +- More complex, better for teams with scheduled releases + +### Trunk-Based Development + +- Everyone commits to `main` frequently +- Heavy use of feature flags +- Requires strong CI/CD and testing discipline + +### GitHub Flow + +- Similar to our approach +- Allows merge commits instead of rebase +- Simpler but creates non-linear history + +**Why we chose rebase workflow:** +Balances simplicity (better than Git Flow) with code quality (cleaner than merge-heavy approaches). Ideal for learning teams transitioning from solo to collaborative development. + +--- + +## Quick Reference Guide + +**Starting a new feature:** + +```bash +git fetch origin +git checkout -b feature/my-feature origin/main +``` + +**Daily sync:** + +```bash +git fetch origin +git stash +git rebase origin/main +git stash pop +git push --force-with-lease +``` + +**Opening a PR:** + +1. Push branch: `git push -u origin feature/my-feature` +2. Open PR on GitHub +3. Request review +4. Respond to feedback + +**After PR merged:** + +```bash +git checkout main +git pull +git branch -D feature/my-feature +``` + +--- + +## Getting Help + +**Stuck during rebase?** + +1. Don't panic -- i cant stress this enough.. see below. +2. Run `git status` to see what's happening +3. Ask in team chat with: + - Output of `git status` + - What you were trying to do + - Current branch name +4. DONT Panic. Everyone screws up and makes mistakes. If you f*ck up, fix it, have an RCA and move on. I have bunged code and production systems over the years. F*ck up, fix it, move on. + **Common commands for troubleshooting:** + +```bash +git status # See current state +git log --oneline -10 # Recent commits +git reflog # History of HEAD movements +git diff # Uncommitted changes +``` + +**Remember:** Git is forgiving, and i cannot stress this enough - almost nothing is truly lost. We can always recover from mistakes with `reflog`. + +--- + +## Workflow Checklist + +Before starting work: + +- [ ] `git fetch origin` +- [ ] `git checkout -b feature/name origin/main` + +During development: + +- [ ] Commit frequently with clear messages +- [ ] Rebase on `main` daily +- [ ] Keep PR less than 400 lines if possible + +Before opening PR: + +- [ ] Final rebase: `git rebase -i origin/main` +- [ ] Squash WIP commits if needed +- [ ] Run tests locally +- [ ] Push: `git push -u origin feature/name` + +During review: + +- [ ] Respond to feedback within 24h +- [ ] Keep branch updated with `main` +- [ ] Address all review comments + +After merge: + +- [ ] Delete branch locally: `git branch -D feature/name` +- [ ] Pull latest main: `git checkout main && git pull` +- [ ] Celebrate! + +--- + +_This workflow is designed to minimize conflicts and maximize collaboration. When in doubt, communicate early with the team and sync your branch often!_ + +--- + +## Architecture Overview + +### Application Type + +- **Type**: Garmin Watch App (not data field or widget) +- **Target Devices**: Forerunner 165, Forerunner 165 Music +- **SDK Version**: Minimum API Level 5.2.0 +- **Architecture**: MVC (Model-View-Controller/Delegate pattern) + +### High-Level Structure + +``` +GarminApp (Application Core) + ├── Views + │ ├── SimpleView (Main activity view + haptic alerts) + │ └── AdvancedView (Chart visualization + haptic alerts) + ├── Delegates (Input handlers) + │ ├── SimpleViewDelegate (Main controls) + │ ├── AdvancedViewDelegate (Chart controls) + │ └── Settings Delegates (Configuration) + ├── Managers + │ ├── SensorManager (Cadence sensor) + │ └── Logger (Memory tracking) + └── Data Processing + ├── Cadence Quality Calculator + ├── Activity Recording Session + └── Haptic Alert Manager +``` + +### Component Interaction Flow + +```mermaid +graph TB + A[User] -->|Input| B[ViewDelegate] + B -->|Commands| C[GarminApp] + C -->|State Changes| D[View] + D -->|Display| A + + E[Cadence Sensor] -->|Data| C + C -->|Process| F[CQ Algorithm] + C -->|Monitor| G[Haptic Manager] + G -->|Vibrations| H[Watch Hardware] + + C -->|Records| I[ActivitySession] + I -->|Saves| J[FIT File] + J -->|Syncs| K[Garmin Connect] + + style G fill:#ff9999 + style H fill:#ff9999 +``` + +--- + +## Core Components + +### 1. GarminApp.mc + +**Purpose**: Central application controller and data manager + +**Key Responsibilities**: + +- Activity session lifecycle management (start/pause/resume/stop/save/discard) +- Cadence data collection and storage +- Cadence quality score computation +- State machine management +- Timer management +- Integration with Garmin Activity Recording API + +#### 1a. Memory Footprint (Cold Numbers) + +- Static allocation: ≈ 2.8 kB + - 280 cadence samples × 4 B = 1.1 kB + - 280 EMA smoothed values = 1.1 kB + - 10 CQ history = 40 B + - Misc buffers / state ≈ 600 B +- Peak stack during draw: ≈ 400 B +- Total at run-time: < 3.5 kB → fits easily into the 32 kB heap of the FR165 + +**Important Constants**: + +```monkey-c +MAX_BARS = 280 // Maximum cadence samples to store +BASELINE_AVG_CADENCE = 160 // Minimum acceptable cadence +MAX_CADENCE = 190 // Maximum cadence for calculations +MIN_CQ_SAMPLES = 30 // Minimum samples for CQ calculation +DEBUG_MODE = true // Enable debug logging +``` + +**State Variables**: + +- `_sessionState`: Current session state (IDLE/RECORDING/PAUSED/STOPPED) +- `activitySession`: Garmin ActivityRecording session object +- `_cadenceHistory`: Circular buffer storing 280 cadence samples +- `_cadenceBarAvg`: Rolling average buffer for chart display +- `_cqHistory`: Last 10 CQ scores for trend analysis + +### 2. SimpleView.mc & AdvancedView.mc + +**Purpose**: Display interfaces with integrated haptic feedback + +**SimpleView Responsibilities**: + +- Display current cadence, heart rate, distance, time +- Show cadence zone status (In Zone/Out Zone) +- Trigger haptic alerts when out of zone +- Update UI every second + +**AdvancedView Responsibilities**: + +- Render 28-minute cadence histogram +- Display heart rate and distance circles +- Show zone boundaries on chart +- Trigger haptic alerts when out of zone +- Color-code bars based on cadence zones + +**Haptic Alert Variables** (both views): + +```monkey-c +private var _lastZoneState = 0; // -1=below, 0=in zone, 1=above +private var _alertStartTime = null; // When alerts began +private var _alertDuration = 180000; // 3 minutes in milliseconds +private var _alertInterval = 30000; // 30 seconds between alerts +private var _lastAlertTime = 0; // Last alert timestamp +private var _pendingSecondVibe = false; // Double-buzz tracking +private var _secondVibeTime = 0; // When second buzz should fire +``` + +--- + +## Data Flow + +### 1. Cadence Data Collection Pipeline + +```mermaid +graph TD + A[Cadence Sensor] -->|Raw Data| B[Activity.getActivityInfo] + B -->|currentCadence| C[updateCadenceBarAvg] + C -->|Every 1s| D[_cadenceBarAvg Buffer] + D -->|Buffer Full| E[Calculate Average] + E --> F[updateCadenceHistory] + F --> G[_cadenceHistory 280 samples] + G --> H[computeCadenceQualityScore] + H --> I[_cqHistory Last 10 scores] + + G -->|Monitor| J[Zone Detection] + J -->|Out of Zone| K[Trigger Haptic Alert] + K --> L[User Feedback] + + style K fill:#ff9999 + style L fill:#ff9999 +``` + +### 2. Haptic Alert Data Flow + +```mermaid +sequenceDiagram + participant User + participant View + participant ZoneChecker + participant HapticManager + participant Hardware + + User->>View: Running (cadence changes) + View->>ZoneChecker: Check current cadence + + alt Cadence drops below minimum + ZoneChecker->>HapticManager: Trigger single buzz pattern + HapticManager->>Hardware: Vibrate 200ms + HapticManager->>HapticManager: Start 30s timer + Note over HapticManager: Repeat every 30s for 3 min + else Cadence exceeds maximum + ZoneChecker->>HapticManager: Trigger double buzz pattern + HapticManager->>Hardware: Vibrate 200ms + HapticManager->>Hardware: Wait 240ms + HapticManager->>Hardware: Vibrate 200ms + HapticManager->>HapticManager: Start 30s timer + Note over HapticManager: Repeat every 30s for 3 min + else Returns to zone + ZoneChecker->>HapticManager: Stop alerts + HapticManager->>HapticManager: Cancel timer + end +``` + +### 3. Timer System + +**Global Timer** (`globalTimer`): + +- Frequency: Every 1 second +- Callback: `updateCadenceBarAvg()` +- Runs: Always (from app start to stop) +- Purpose: Collect cadence data when recording + +**View Refresh Timers**: + +- SimpleView: Refresh every 1 second (reused for haptic checks) +- AdvancedView: Refresh every 1 second (reused for haptic checks) +- Purpose: Update UI elements and monitor zone status + +**Haptic Alert System** (NO dedicated timers): + +- Uses existing view refresh cycle +- Checks zone status on each UI update +- Triggers vibrations when appropriate +- No additional timer overhead + +### 4. Data Averaging System + +The app uses a two-tier averaging system: + +**Tier 1: Bar Averaging** + +``` +Chart Duration = 6 seconds (ThirtyminChart default) +↓ +Collect 6 cadence readings (1 per second) +↓ +Calculate average of these 6 readings +↓ +Store as single bar value +``` + +**Tier 2: Historical Storage** + +``` +280 bar values stored +↓ +Each bar = average of 6 seconds +↓ +Total history = 280 × 6 = 1680 seconds = 28 minutes +``` + +**Chart Duration Options**: + +- FifteenminChart = 3 seconds per bar +- ThirtyminChart = 6 seconds per bar (default) +- OneHourChart = 13 seconds per bar +- TwoHourChart = 26 seconds per bar + +### 4a. Sensor Manager Abstraction + +`SensorManager.mc` decouples real vs. simulated cadence: + +```monkey-c +useSimulator = true → returns hard-coded value (for desk testing) +useSimulator = false → reads Activity.getActivityInfo().currentCadence +``` + +--- + +## State Management + +### Session State Machine + +```mermaid +stateDiagram-v2 + [*] --> IDLE: App Start + IDLE --> RECORDING: startRecording() + RECORDING --> PAUSED: pauseRecording() + PAUSED --> RECORDING: resumeRecording() + RECORDING --> STOPPED: stopRecording() + PAUSED --> STOPPED: stopRecording() + STOPPED --> IDLE: saveSession() / discardSession() + + note right of RECORDING + - Timer active + - Data collecting + - Haptic alerts enabled + - UI updating + end note + + note right of PAUSED + - Timer stopped + - Data frozen + - Haptic alerts disabled + - UI static + end note + + note right of STOPPED + - Final CQ calculated + - Haptic alerts disabled + - Awaiting user decision + end note +``` + +### State Transition Rules + +**IDLE → RECORDING**: + +- User presses START/STOP button +- Creates new ActivityRecording session +- Starts Garmin timer +- Resets all cadence data arrays +- Initializes timestamps +- **Enables haptic zone monitoring** + +**RECORDING → PAUSED**: + +- User selects "Pause" from menu +- Stops Garmin timer (timer pauses) +- Records pause timestamp +- Data collection stops +- **Disables haptic alerts** + +**PAUSED → RECORDING**: + +- User selects "Resume" from menu +- Restarts Garmin timer +- Accumulates paused time +- Data collection resumes +- **Re-enables haptic zone monitoring** + +**RECORDING/PAUSED → STOPPED**: + +- User selects "Stop" from menu +- Stops Garmin timer +- Computes final CQ score +- Freezes all metrics +- **Disables haptic alerts** +- Awaits save/discard decision + +**STOPPED → IDLE**: + +- User selects "Save": Saves to FIT file +- User selects "Discard": Deletes session +- Resets all data structures +- Ready for new session + +--- + +## Activity Recording System + +### Garmin ActivityRecording Integration + +**Session Creation** (`startRecording()`): + +```monkey-c +activitySession = ActivityRecording.createSession({ + :name => "Running", + :sport => ActivityRecording.SPORT_RUNNING, + :subSport => ActivityRecording.SUB_SPORT_GENERIC +}); +activitySession.start(); +``` + +**What This Does**: + +- Creates official Garmin activity +- Starts timer (visible in UI) +- Records GPS, heart rate, cadence automatically +- Manages distance calculation +- Handles sensor data collection + +**Pause/Resume** (`pauseRecording()` / `resumeRecording()`): + +```monkey-c +// Pause +activitySession.stop(); // Pauses timer + +// Resume +activitySession.start(); // Resumes timer +``` + +**Save** (`saveSession()`): + +```monkey-c +activitySession.save(); +``` + +- Writes FIT file to device +- Syncs to Garmin Connect +- Appears in activity history +- Includes all sensor data + +**Discard** (`discardSession()`): + +```monkey-c +activitySession.discard(); +``` + +- Deletes session completely +- No FIT file created +- No sync to Garmin Connect + +--- + +## Haptic Feedback System + +### Overview + +The haptic feedback system provides real-time tactile alerts when the runner's cadence drifts outside their optimal zone. This helps maintain proper running form without constantly looking at the watch. + +### Design Philosophy + +**Timer-Free Architecture**: Instead of creating additional timers (which are limited on Garmin devices), the system piggybacks on the existing 1-second view refresh cycle. This approach: + +- Eliminates "Too Many Timers" errors +- Reduces memory overhead +- Maintains precise timing through timestamp tracking +- Seamlessly integrates with existing UI updates + +### Alert Patterns + +```mermaid +graph LR + A[Zone Detection] --> B{Cadence Status?} + B -->|Below Min| C[Single Buzz] + B -->|Above Max| D[Double Buzz] + B -->|In Zone| E[No Alert] + + C --> F[200ms vibration] + D --> G[200ms vibration] + G --> H[240ms pause] + H --> I[200ms vibration] + + F --> J[Repeat every 30s for 3 min] + I --> J + + style C fill:#9999ff + style D fill:#ff9999 + style E fill:#99ff99 +``` + +**Single Buzz** (Below Minimum Cadence): + +- Pattern: One 200ms vibration +- Meaning: Speed up your steps +- Repeat: Every 30 seconds +- Duration: 3 minutes max + +**Double Buzz** (Above Maximum Cadence): + +- Pattern: Two 200ms vibrations with 240ms gap +- Meaning: Slow down your steps +- Repeat: Every 30 seconds +- Duration: 3 minutes max + +**No Alert** (In Target Zone): + +- Pattern: Silence +- Meaning: Perfect cadence, keep going! + +### Implementation Details + +#### Zone State Tracking + +```monkey-c +private var _lastZoneState = 0; // -1 = below, 0 = in zone, 1 = above + +// Determine current zone +if (cadence < minZone) { + newZoneState = -1; // Below minimum +} else if (cadence > maxZone) { + newZoneState = 1; // Above maximum +} else { + newZoneState = 0; // In target zone +} +``` + +#### Alert Triggering Logic + +```monkey-c +if (newZoneState != _lastZoneState) { + if (newZoneState == -1) { + // Just dropped below minimum + triggerSingleVibration(); + startAlertCycle(); + } else if (newZoneState == 1) { + // Just exceeded maximum + triggerDoubleVibration(); + startAlertCycle(); + } else { + // Returned to zone + stopAlertCycle(); + } + _lastZoneState = newZoneState; +} +``` + +#### Alert Cycle Management + +```monkey-c +function startAlertCycle() as Void { + _alertStartTime = System.getTimer(); + _lastAlertTime = System.getTimer(); + // Initial alert already fired +} + +function checkAndTriggerAlerts() as Void { + if (_alertStartTime == null) { return; } + + var currentTime = System.getTimer(); + var elapsed = currentTime - _alertStartTime; + + // Stop after 3 minutes + if (elapsed >= 180000) { + _alertStartTime = null; + return; + } + + // Check if 30 seconds passed since last alert + var timeSinceLastAlert = currentTime - _lastAlertTime; + if (timeSinceLastAlert >= 30000) { + _lastAlertTime = currentTime; + + if (_lastZoneState == -1) { + triggerSingleVibration(); + } else if (_lastZoneState == 1) { + triggerDoubleVibration(); + } + } +} +``` + +#### Double Buzz Implementation + +```monkey-c +function triggerDoubleVibration() as Void { + if (Attention has :vibrate) { + // First vibration + var vibeData = [new Attention.VibeProfile(50, 200)]; + Attention.vibrate(vibeData); + + // Schedule second vibration + _pendingSecondVibe = true; + _secondVibeTime = System.getTimer() + 240; + } +} + +function checkPendingVibration() as Void { + if (_pendingSecondVibe) { + var currentTime = System.getTimer(); + if (currentTime >= _secondVibeTime) { + var vibeData = [new Attention.VibeProfile(50, 200)]; + Attention.vibrate(vibeData); + _pendingSecondVibe = false; + } + } +} +``` + +### Integration with Views + +Both `SimpleView` and `AdvancedView` include identical haptic feedback implementations: + +```mermaid +graph TD + A[onUpdate Called] --> B[displayCadence / drawElements] + B --> C[Get current cadence] + C --> D[Determine zone state] + D --> E{State changed?} + E -->|Yes| F[Trigger appropriate alert] + E -->|No| G[Check if alert needed] + F --> H[Start/stop alert cycle] + G --> H + H --> I[checkPendingVibration] + I --> J[Continue UI update] +``` + +**SimpleView Integration**: + +```monkey-c +function displayCadence() as Void { + // ... update UI elements ... + + // Determine zone state + var newZoneState = 0; + if (currentCadence < minZone) { + newZoneState = -1; + } else if (currentCadence > maxZone) { + newZoneState = 1; + } + + // Handle zone transitions + if (newZoneState != _lastZoneState) { + // Trigger appropriate alert and start cycle + } else { + // Check if periodic alert needed + checkAndTriggerAlerts(); + } +} +``` + +**AdvancedView Integration**: + +```monkey-c +function checkCadenceZone() as Void { + // Get activity info and determine zone + // Same logic as SimpleView + // Integrated with chart rendering +} +``` + +### Timing Accuracy + +The system achieves accurate 30-second intervals through timestamp comparison: + +``` +Initial Alert: T = 0s [BUZZ] +Check at: T = 1s (29s remaining - no alert) +Check at: T = 2s (28s remaining - no alert) +... +Check at: T = 30s (0s remaining - BUZZ!) +Check at: T = 31s (new cycle starts) +``` + +Actual timing variance: ±1 second (due to 1Hz refresh rate) + +### Memory Overhead + +**Additional Memory per View**: + +- State tracking: 3 integers (12 bytes) +- Timestamps: 3 longs (24 bytes) +- Boolean flags: 1 boolean (1 byte) +- **Total: ~40 bytes per view** + +**No Additional Timers Required**: + +- Reuses existing `_refreshTimer` (SimpleView) +- Reuses existing `_simulationTimer` (AdvancedView) +- Zero timer creation overhead + +### User Experience Flow + +```mermaid +sequenceDiagram + participant Runner + participant Watch + participant App + + Runner->>Watch: Start activity + Watch->>App: BEGIN RECORDING + + Note over App: Monitoring cadence... + + Runner->>Watch: Cadence drops to 115 SPM + App->>Watch: [SINGLE BUZZ] + Note over Watch: Below minimum alert + + Note over App: Wait 30 seconds... + + App->>Watch: [SINGLE BUZZ] + Note over Watch: Still below minimum + + Runner->>Watch: Increases cadence to 145 SPM + Note over App: Back in zone - stop alerts + + Runner->>Watch: Cadence spikes to 165 SPM + App->>Watch: [DOUBLE BUZZ] + Note over Watch: Above maximum alert + + Runner->>Watch: Reduces cadence to 150 SPM + Note over App: Back in zone - stop alerts +``` + +### Haptic Feedback Best Practices + +**For Developers**: + +1. Always check `Attention has :vibrate` before calling vibration +2. Reuse existing timers rather than creating new ones +3. Use timestamp-based tracking for precise intervals +4. Clean up state in `onHide()` to prevent orphaned alerts +5. Test thoroughly with rapid zone transitions + +### Future Enhancements + +Potential improvements to the haptic system: + +1. **Configurable Alert Patterns** + - User-selectable vibration duration + - Custom interval timing (15s, 45s, 60s) + - Triple-buzz for extreme deviations + +2. **Progressive Alert Intensity** + - Gentle buzz for minor deviations (±3 SPM) + - Strong buzz for major deviations (±10 SPM) + - Requires multi-pattern vibration support + +3. **Smart Alert Suppression** + - Disable during warm-up (first 5 minutes) + - Pause alerts on steep hills (using GPS grade) + - Adaptive zones based on fatigue detection + +4. **Audio Cues** (device-dependent) + - Combine vibration with tones + - Voice feedback for major transitions + - Requires audio hardware support + +--- + +## Cadence Quality Algorithm + +### Overview + +The Cadence Quality (CQ) score is a composite metric that measures running efficiency based on two factors: + +1. **Time in Zone** (70% weight): Percentage of time spent within ideal cadence range +2. **Smoothness** (30% weight): Consistency of cadence over time + +### Algorithm Flow + +```mermaid +graph TD + A[Collect Cadence Sample] --> B{Min 30 samples?} + B -->|No| C[Display: CQ --] + B -->|Yes| D[Calculate Time in Zone] + D --> E[Calculate Smoothness] + E --> F[Compute Weighted Average] + F --> G[CQ Score 0-100%] + G --> H[Store in History] + H --> I[Update Display] +``` + +### Time in Zone Calculation + +**Purpose**: Measures what percentage of your running time is spent at the optimal cadence + +**Formula**: + +``` +Time in Zone % = (samples in zone / total samples) × 100 +``` + +**Implementation**: + +```monkey-c +function computeTimeInZoneScore() as Number { + if (_cadenceCount < MIN_CQ_SAMPLES) { + return -1; // Not enough data yet + } + + 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(); +} +``` + +**Example**: + +- 280 total samples collected +- 210 samples within zone [145-155 SPM] +- Time in Zone = (210/280) × 100 = 75% + +### Smoothness Calculation + +**Purpose**: Measures cadence consistency (low variance = better form) + +**Formula**: + +``` +Average Difference = Σ |current - previous| / number of transitions +Smoothness % = 100 - (average difference × 10) +``` + +**Implementation**: + +```monkey-c +function computeSmoothnessScore() as Number { + if (_cadenceCount < MIN_CQ_SAMPLES) { + return -1; + } + + 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++; + } + } + + if (diffCount == 0) { + return -1; + } + + var avgDiff = totalDiff / diffCount; + var rawScore = 100 - (avgDiff * 10); + + // Clamp to 0-100 range + if (rawScore < 0) { rawScore = 0; } + if (rawScore > 100) { rawScore = 100; } + + return rawScore; +} +``` + +**Example**: + +- Sample transitions: 145→148 (3), 148→147 (1), 147→150 (3) +- Average difference = (3+1+3)/3 = 2.33 +- Smoothness = 100 - (2.33 × 10) = 76.7% + +### Final CQ Score + +**Weighted Combination**: + +``` +CQ = (Time in Zone × 0.7) + (Smoothness × 0.3) +``` + +**Implementation**: + +```monkey-c +function computeCadenceQualityScore() as Number { + var timeInZone = computeTimeInZoneScore(); + var smoothness = computeSmoothnessScore(); + + if (timeInZone < 0 || smoothness < 0) { + return -1; // Not enough data + } + + var cq = (timeInZone * 0.7) + (smoothness * 0.3); + return cq.toNumber(); +} +``` + +**Example**: + +- Time in Zone = 75% +- Smoothness = 76.7% +- CQ = (75 × 0.7) + (76.7 × 0.3) = 52.5 + 23.01 = **75.5%** + +### CQ Score Interpretation + +| Score Range | Rating | Interpretation | +| ----------- | --------- | -------------------------- | +| 90-100% | Excellent | Elite running form | +| 80-89% | Very Good | Consistent optimal cadence | +| 70-79% | Good | Generally on target | +| 60-69% | Fair | Room for improvement | +| 50-59% | Poor | Frequent zone violations | +| 0-49% | Very Poor | Needs significant work | + +### Confidence Calculation + +**Purpose**: Indicates reliability of CQ score based on missing data + +```monkey-c +function computeCQConfidence() as String { + if (_cadenceCount < MIN_CQ_SAMPLES) { + return "Low"; + } + + var missingRatio = _missingCadenceCount.toFloat() / + (_cadenceCount + _missingCadenceCount).toFloat(); + + if (missingRatio > 0.2) { + return "Low"; // >20% missing data + } else if (missingRatio > 0.1) { + return "Medium"; // 10-20% missing + } else { + return "High"; // <10% missing + } +} +``` + +### Trend Analysis + +**Purpose**: Shows if cadence quality is improving, stable, or declining + +```monkey-c +function computeCQTrend() as String { + if (_cqHistory.size() < 5) { + return "Insufficient data"; + } + + // Compare recent half vs. older half + var midpoint = _cqHistory.size() / 2; + var olderAvg = 0.0; + var recentAvg = 0.0; + + for (var i = 0; i < midpoint; i++) { + olderAvg += _cqHistory[i]; + } + olderAvg /= midpoint; + + for (var i = midpoint; i < _cqHistory.size(); i++) { + recentAvg += _cqHistory[i]; + } + recentAvg /= (_cqHistory.size() - midpoint); + + var diff = recentAvg - olderAvg; + + if (diff > 5) { + return "Improving"; + } else if (diff < -5) { + return "Declining"; + } else { + return "Stable"; + } +} +``` + +### CQ Storage in FIT File + +When the activity is stopped, the final CQ score is frozen and written to the FIT file: + +```monkey-c +function stopRecording() as Void { + // ... stop activity session ... + + 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(); + } + + _sessionState = STOPPED; +} +``` + +This frozen CQ score: + +- Appears in the activity summary +- Syncs to Garmin Connect +- Provides historical tracking +- Can be compared across runs + +--- + +## User Interface + +### SimpleView (Main Display) + +**Layout**: + +``` + ┌─────────────────────┐ + │ [REC] 00:12:34 │ ← Time + Recording Indicator + ├─────────────────────┤ + │ ❤ │ │ ⚡ │ + │ 152 │ 148 │ 180 │ ← Heart Rate, Cadence, Steps + ├─────────────────────┤ + │ In Zone (145-155) │ ← Zone Status + ├─────────────────────┤ + │ 2.45 km │ ← Distance + ├─────────────────────┤ + │ CQ: 75% │ ← Cadence Quality + └─────────────────────┘ +``` + +**Color Coding**: + +- **Green**: Cadence in optimal zone +- **Blue**: Slightly below zone (within threshold) +- **Grey**: Well below zone +- **Orange**: Slightly above zone (within threshold) +- **Red**: Well above zone + +**Haptic Feedback Integration**: + +- Single buzz when cadence drops below minimum +- Double buzz when cadence exceeds maximum +- Repeats every 30 seconds if still out of zone +- Automatically stops after 3 minutes or when returning to zone + +### AdvancedView (Chart Display) + +**Layout**: + +``` + ┌─────────────────────┐ + │ 1:23:45 │ ← Session Time + ├──────┬─────────┬────┤ + │ ❤ │ │ 🏃 │ + │ 152 │ │2.4 │ ← HR Circle + Distance Circle + ├──────┴─────────┴────┤ + │ 148 spm │ ← Current Cadence + ├─────────────────────┤ + │ ▂▅▇█▆▅▃▂▃▄▅▆▇█▆▄▃ │ ← 28-min Histogram + │ │ + ├─────────────────────┤ + │ Zone: 145-155 spm │ ← Zone Range + └─────────────────────┘ +``` + +**Chart Features**: + +- 280 bars representing 28 minutes of data +- Fixed vertical scale (0-200 SPM) +- Color-coded bars matching zone status +- Real-time updates every second +- Smooth scrolling as new data arrives + +**Haptic Feedback Integration**: + +- Same alert patterns as SimpleView +- Integrated with chart updates +- Visual + tactile feedback for optimal learning + +### Navigation + +```mermaid +graph TD + A[SimpleView] -->|Swipe Up / Press Down| B[AdvancedView] + B -->|Swipe Down / Press Up| A + A -->|Swipe Left / Press Up| C[Settings] + B -->|Swipe Left| C + C -->|Back| A + A -->|Press Select| D{Activity State?} + D -->|IDLE| E[Start Recording] + D -->|RECORDING| F[Activity Menu] + D -->|PAUSED| G[Paused Menu] + D -->|STOPPED| H[Save/Discard Menu] +``` + +**Button Mapping**: + +- **SELECT**: Start/Stop activity or open control menu +- **UP**: Navigate to settings or previous view +- **DOWN**: Navigate to next view +- **BACK**: Exit menus (disabled during active session) +- **MENU**: Open cadence zone settings + +### Activity Control Menus + +**During Recording**: + +``` +┌──────────────────────┐ +│ Activity │ +├──────────────────────┤ +│ > Resume │ +│ > Pause │ +│ > Stop │ +└──────────────────────┘ +``` + +**When Paused**: + +``` +┌──────────────────────┐ +│ Activity Paused │ +├──────────────────────┤ +│ > Resume │ +│ > Stop │ +└──────────────────────┘ +``` + +**After Stopping**: + +``` +┌──────────────────────┐ +│ Save Activity? │ +├──────────────────────┤ +│ > Save │ +│ > Discard │ +└──────────────────────┘ +``` + +### Settings Menu + +``` +┌──────────────────────┐ +│ Settings │ +├──────────────────────┤ +│ > Profile │ +│ > Customization │ +│ > Feedback │ +│ > Cadence Range │ +└──────────────────────┘ +``` + +**Profile Settings**: + +- Height (cm) +- Speed (km/h) +- Gender (Male/Female/Other) +- Experience Level (Beginner/Intermediate/Advanced) + +**Customization**: + +- Chart Duration (15min/30min/1hr/2hr) + +**Feedback** (Future): + +- Haptic intensity +- Alert interval +- Alert duration + +**Cadence Range**: + +- Set Min Cadence (manual adjustment) +- Set Max Cadence (manual adjustment) + +--- + +## Settings System + +### User Profile Configuration + +**Purpose**: Calculate personalized ideal cadence based on biomechanics + +**Formula** (from research): + +``` +Reference Cadence = (-1.251 × leg_length) + (3.665 × speed_m/s) + 254.858 +Final Cadence = Reference × Experience_Factor +``` + +**Gender-Specific Adjustments**: + +```monkey-c +function idealCadenceCalculator() as Void { + var referenceCadence = 0; + var userLegLength = _userHeight * 0.53; // 53% of height + var userSpeedms = _userSpeed / 3.6; // Convert km/h to m/s + + switch (_userGender) { + case Male: + referenceCadence = (-1.268 × userLegLength) + + (3.471 × userSpeedms) + 261.378; + break; + case Female: + referenceCadence = (-1.190 × userLegLength) + + (3.705 × userSpeedms) + 249.688; + break; + default: + referenceCadence = (-1.251 × userLegLength) + + (3.665 × userSpeedms) + 254.858; + break; + } + + referenceCadence *= _experienceLvl; + referenceCadence = Math.round(referenceCadence); + + var finalCadence = max(BASELINE_AVG_CADENCE, + min(referenceCadence, MAX_CADENCE)); + + _idealMaxCadence = finalCadence + 5; + _idealMinCadence = finalCadence - 5; +} +``` + +**Experience Level Multipliers**: + +- Beginner: 1.06 (higher cadence for learning) +- Intermediate: 1.04 (moderate adjustment) +- Advanced: 1.02 (minimal adjustment) + +### Persistent Storage + +**Storage Keys**: + +```monkey-c +const PROP_USER_HEIGHT = "user_height"; +const PROP_USER_SPEED = "user_speed"; +const PROP_USER_GENDER = "user_gender"; +const PROP_EXPERIENCE_LVL = "experience_level"; +const PROP_CHART_DURATION = "chart_duration"; +const PROP_MIN_CADENCE = "min_cadence"; +const PROP_MAX_CADENCE = "max_cadence"; +``` + +**Save Settings**: + +```monkey-c +function saveSettings() as Void { + Storage.setValue(PROP_USER_HEIGHT, _userHeight); + Storage.setValue(PROP_USER_SPEED, _userSpeed); + Storage.setValue(PROP_USER_GENDER, _userGender); + Storage.setValue(PROP_EXPERIENCE_LVL, _experienceLvl); + Storage.setValue(PROP_CHART_DURATION, _chartDuration); + Storage.setValue(PROP_MIN_CADENCE, _idealMinCadence); + Storage.setValue(PROP_MAX_CADENCE, _idealMaxCadence); +} +``` + +**Load Settings**: + +```monkey-c +function loadSettings() as Void { + var height = Storage.getValue(PROP_USER_HEIGHT); + if (height != null) { + _userHeight = height as Number; + } + // ... load other settings ... +} +``` + +Settings are automatically: + +- Loaded on app start +- Saved when modified +- Persisted between sessions +- Restored after watch reboot + +--- + +## Documentation Reference + +--- + +This reference covers the formatting used throughout this documentation. Use it when contributing updates or creating new documentation. markdown can be +intimidating at first, but onve you master it, you will use it for everything. + +--- + +## Markdown Basics + +### Headers + +```markdown +# H1 - Main Title + +## H2 - Major Section + +### H3 - Subsection + +#### H4 - Minor Heading +``` + +**Usage in this doc:** + +- H1: Document title only +- H2: Major sections (Architecture, Core Components, etc.) +- H3: Subsections within major sections +- H4: Rarely used, for very specific details + +--- + +### Text Formatting + +```markdown +**Bold text** for emphasis +_Italic text_ for subtle emphasis +`Inline code` for commands, variables, filenames +~~Strikethrough~~ for deprecated content +``` + +**Examples:** + +- **Bold**: Important terms, warnings +- _Italic_: Notes, asides +- `Code`: `git push`, `_cadenceHistory`, `SimpleView.mc` + +--- + +### Links + +```markdown +[Link text](https://example.com) +[Internal link](#section-name) +[Link with title](https://example.com "Hover text") +``` + +**Internal link rules:** + +- Section names become anchors automatically +- Convert to lowercase +- Replace spaces with hyphens +- Remove special characters or convert to hyphens + +**Examples:** + +```markdown +[Architecture Overview](#architecture-overview) # Correct +[GitHub Workflow & Collaboration](#github-workflow--collaboration) # & becomes -- +[State Management](#state-management) # Simple case +``` + +--- + +### Lists + +**Unordered lists:** + +```markdown +- Item 1 +- Item 2 + - Nested item 2a + - Nested item 2b +- Item 3 +``` + +**Ordered lists:** + +```markdown +1. First item +2. Second item +3. Third item +``` + +**Checklists:** + +```markdown +- [ ] Incomplete task +- [x] Completed task +``` + +--- + +### Code Blocks + +**Inline code:** + +```markdown +Use `git status` to check your working directory. +``` + +**Fenced code blocks with syntax highlighting:** + +````markdown +```bash +git fetch origin +git rebase origin/main +``` + +```monkey-c +function initialize() { + View.initialize(); +} +``` + +```javascript +const response = await fetch(url); +``` +```` + +**Supported languages in this doc:** + +- `bash` - Shell commands +- `monkey-c` - Monkey C code +- `javascript` - JS examples +- `yaml` - GitHub Actions workflows +- `markdown` - Markdown examples +- No language tag - Plain text + +--- + +### Tables + +**Basic table:** + +```markdown +| Column 1 | Column 2 | Column 3 | +| -------- | -------- | -------- | +| Data 1 | Data 2 | Data 3 | +| Data 4 | Data 5 | Data 6 | +``` + +**Table with alignment:** + +```markdown +| Left-aligned | Center-aligned | Right-aligned | +| :----------- | :------------: | ------------: | +| Left | Center | Right | +``` + +**Tips:** + +- Use `|:---` for left align (default) +- Use `|:---:|` for center align +- Use `|---:|` for right align +- Don't worry about perfect spacing - Markdown handles it + +--- + +### Blockquotes + +```markdown +> This is a blockquote +> It can span multiple lines +> +> And include multiple paragraphs +``` + +**Usage:** Notes, warnings, important callouts + +--- + +### Horizontal Rules + +```markdown +--- +``` + +**Usage:** Separate major sections (used throughout this doc) + +--- + +## Mermaid Diagrams + +Mermaid creates diagrams from text. All diagrams must be in fenced code blocks with `mermaid` language tag. + +### Flowcharts (Graph) + +**Basic syntax:** + +````markdown +```mermaid +graph TD + A[Start] --> B[Process] + B --> C{Decision?} + C -->|Yes| D[Action 1] + C -->|No| E[Action 2] + D --> F[End] + E --> F +``` +```` + +**Node shapes:** + +```markdown +A[Rectangle] # Square corners +B(Rounded) # Rounded corners +C([Stadium]) # Pill shape +D[[Subroutine]] # Double border +E[(Database)] # Cylinder +F((Circle)) # Circle +G>Flag] # Flag shape +H{Diamond} # Diamond (decision) +I{{Hexagon}} # Hexagon +``` + +**Arrow types:** + +```markdown +A --> B # Solid arrow +A -.-> B # Dotted arrow +A ==> B # Thick arrow +A --- B # Line (no arrow) +A -- Text --> B # Labeled arrow +A -->|Text| B # Labeled arrow (compact) +``` + +**Direction:** + +```markdown +graph TD # Top to Down +graph LR # Left to Right +graph BT # Bottom to Top +graph RL # Right to Left +``` + +**Example from this doc (Data Flow):** + +````markdown +```mermaid +graph TD + A[Cadence Sensor] -->|Raw Data| B[Activity.getActivityInfo] + B -->|currentCadence| C[updateCadenceBarAvg] + C -->|Every 1s| D[_cadenceBarAvg Buffer] + D -->|Buffer Full| E[Calculate Average] + E --> F[updateCadenceHistory] +``` +```` + +--- + +### Sequence Diagrams + +**Basic syntax:** + +````markdown +```mermaid +sequenceDiagram + participant A as Alice + participant B as Bob + + A->>B: Hello Bob! + B->>A: Hi Alice! + + Note over A,B: This is a note + Note right of A: Note on right + Note left of B: Note on left +``` +```` + +**Arrow types:** + +```markdown +A->>B: Solid arrow (message) +A-->>B: Dotted arrow (return) +A-xB: Cross (lost message) +``` + +**Special syntax:** + +```markdown +alt Alternative 1 +A->>B: Do this +else Alternative 2 +A->>B: Do that +end + +loop Every 30s +A->>B: Repeat this +end +``` + +**Example from this doc (Haptic Alerts):** + +````markdown +```mermaid +sequenceDiagram + participant User + participant View + participant ZoneChecker + participant HapticManager + + User->>View: Running (cadence changes) + View->>ZoneChecker: Check current cadence + + alt Cadence drops below minimum + ZoneChecker->>HapticManager: Trigger single buzz + else Cadence exceeds maximum + ZoneChecker->>HapticManager: Trigger double buzz + else Returns to zone + ZoneChecker->>HapticManager: Stop alerts + end +``` +```` + +--- + +### State Diagrams + +**Basic syntax:** + +````markdown +```mermaid +stateDiagram-v2 + [*] --> State1 + State1 --> State2: Transition + State2 --> State3: Another transition + State3 --> [*] + + note right of State1 + This is a note + end note +``` +```` + +**Example from this doc (Session States):** + +````markdown +```mermaid +stateDiagram-v2 + [*] --> IDLE: App Start + IDLE --> RECORDING: startRecording() + RECORDING --> PAUSED: pauseRecording() + PAUSED --> RECORDING: resumeRecording() + RECORDING --> STOPPED: stopRecording() + STOPPED --> IDLE: saveSession() / discardSession() + + note right of RECORDING + Timer active + Data collecting + Haptic alerts enabled + end note +``` +```` + +--- + +## Documentation Best Practices + +### When to Use Which Diagram + +| Diagram Type | Use When | Example in This Doc | +| -------------------- | --------------------------------------------------- | -------------------------------------------- | +| **Flowchart** | Showing process flow, data pipeline, decision trees | Data collection pipeline, CI workflow | +| **Sequence Diagram** | Showing interactions over time between components | Haptic alert timing, merge conflict scenario | +| **State Diagram** | Showing state transitions and lifecycle | Session state machine | + +### Formatting Guidelines + +**DO:** + +- Use consistent header levels (don't skip levels) +- Include code language tags in fenced blocks +- Use tables for structured data comparisons +- Add horizontal rules between major sections +- Keep lines under 120 characters when possible +- Use relative links for internal references + +**DON'T:** + +- Use HTML unless absolutely necessary +- Skip header levels (H2 → H4 without H3) +- Use images for text content (accessibility) +- Hard-code line breaks (let Markdown handle wrapping) +- Use bare URLs (always use link syntax) + +### Code Block Guidelines + +**For commands:** + +```bash +# Good: Show full command with context +git fetch origin +git rebase origin/main +git push --force-with-lease + +# Bad: No context, unclear +git push -f +``` + +**For code:** + +```monkey-c +// Good: Include relevant context and comments +function startRecording() as Void { + // Create Garmin activity session + activitySession = ActivityRecording.createSession({ + :name => "Running", + :sport => ActivityRecording.SPORT_RUNNING + }); +} + +// Bad: No context or explanation +activitySession.start(); +``` + +### Table Guidelines + +**Comparison tables:** + +- Left column: Item being compared +- Other columns: Attributes or options +- Use bold for headers + +**Decision tables:** + +- Left column: Condition/situation +- Right columns: Action or outcome +- Consider using "When/Action/Command" structure + +**Examples:** + +```markdown +| When | Action | Command | +| ---------- | ----------------------- | ------------------------------ | +| Start work | Branch from latest main | `git checkout -b feature/name` | +``` + +--- + +## Mermaid Troubleshooting + +### Common Issues + +**Diagram not rendering?** + +1. Check for typos in `mermaid` tag +2. Ensure proper indentation +3. Close all parentheses and brackets +4. Check for special characters in node names + +**Arrows not connecting?** + +- Make sure node IDs match exactly (case-sensitive) +- Check arrow syntax (`-->` not `->`) + +**Text not showing?** + +- Wrap text with spaces in quotes: `A["My Text"]` +- Use `|Text|` for inline labels on arrows + +### Testing Diagrams + +**Before committing:** + +1. View in GitHub's preview tab +2. Or use online editor: https://mermaid.live +3. Check that text is readable +4. Verify all arrows point correctly + +--- + +## Contributing to Documentation + +### Before You Edit + +1. Read through existing docs to understand style +2. Check this reference for syntax +3. Test Mermaid diagrams in preview + +### Making Changes + +1. Create branch: `git checkout -b docs/your-update` +2. Edit markdown files +3. Preview changes locally or on GitHub +4. Commit with clear message: "docs: update haptic feedback section" +5. Open PR with description of changes + +### Review Checklist + +- [ ] Headers follow hierarchy (no skipped levels) +- [ ] Links work (test internal anchors) +- [ ] Code blocks have language tags +- [ ] Tables align properly +- [ ] Mermaid diagrams render correctly +- [ ] No spelling errors in headers or links +- [ ] Follows existing document style + +--- + +## Quick Reference: Common Patterns + +### Section Header Pattern + +```markdown +--- + +## Section Name + +Brief introduction paragraph explaining what this section covers. + +### Subsection + +Content here... + +**Key Points:** + +- Point 1 +- Point 2 +- Point 3 +``` + +### Code Example Pattern + +````markdown +**Implementation:** + +```monkey-c +function example() as Void { + // Explanation comment + var result = doSomething(); +} +``` +```` + +**What this does:** + +- Explains the code +- Provides context + +```` + +### Comparison Table Pattern +```markdown +| Feature | Option A | Option B | +|---------|----------|----------| +| Speed | Fast | Slow | +| Memory | High | Low | +| Complexity | Simple | Complex | +```` + +--- + +--- + +## Features Reference + +### Current Features (v1.0) + +✅ **Core Functionality** + +- Real-time cadence monitoring +- 28-minute rolling histogram +- Cadence Quality (CQ) scoring +- Activity recording to FIT file +- Pause/Resume functionality +- Save/Discard workflow + +✅ **User Interface** + +- SimpleView (main display) +- AdvancedView (chart visualization) +- Settings menus +- Activity control menus +- Recording indicator + +✅ **Smart Features** + +- Personalized cadence zones +- Gender-specific calculations +- Experience level adjustment +- Color-coded zone feedback +- CQ trend analysis +- **Haptic zone alerts** + +✅ **Data Management** + +- Circular buffer storage +- Two-tier averaging system +- Persistent settings +- FIT file integration +- Memory optimization + +### Haptic Feedback Feature (v1.1) + +✅ **Alert Patterns** + +- Single buzz for below-zone cadence +- Double buzz for above-zone cadence +- 30-second repeat interval +- 3-minute maximum duration +- Automatic stop on zone re-entry + +✅ **Technical Implementation** + +- Timer-free architecture +- Timestamp-based tracking +- Integrated with view refresh cycle +- No additional memory overhead +- Works on both SimpleView and AdvancedView + +✅ **User Benefits** + +- No need to constantly watch screen +- Tactile feedback during runs +- Non-intrusive alerts +- Customizable zone ranges +- Improves form awareness + +### Future Enhancements + +#### 🔴 High Priority + +1. **Configurable Alert Settings** + - Alert interval (15s/30s/45s/60s) + - Alert duration (1min/3min/5min/continuous) + - Vibration intensity (light/medium/strong) + +2. **Battery Optimization** + - Adaptive refresh rate based on battery level + - Low-power mode during steady-state running + - Smart sensor polling + +3. **Chart Rendering Optimization** + - Reduce draw calls + - Cache static elements + - Optimize bar calculations + +#### 🟡 Medium Priority + +4. **Smooth Bars** + - Gradient transitions between zones + - Anti-aliased rendering + - Sub-pixel accuracy + +5. **Zone Boundary Lines** + - Visual indicators on chart + - Min/max cadence markers + - Target zone highlighting + +6. **Statistical Overlays** + - Average line + - Standard deviation bands + - Trend line + +7. **Terrain-Adaptive Zones** + - Adjust zones for hills (using GPS elevation) + - Compensate for terrain difficulty + - Smart zone boundaries + +#### 🟢 Low Priority + +8. **Fade Old Bars** + - Opacity gradient for time perspective + - Highlight recent data + - Visual age indication + +9. **Auto-Adjust Chart Duration** + - Extend duration for long runs + - Compress for short workouts + - Dynamic time window + +10. **CSV Export** + - Export cadence history + - Include all metrics + - Bluetooth transfer to phone + +11. **Dynamic Memory Management** + - Adapt buffer sizes to available memory + - Graceful degradation on low memory + - Device-specific optimization + +12. **Night Mode** + - Auto-detect sunrise/sunset + - Red/orange color palette + - Preserve dark adaptation + +13. **Progressive Alert Intensity** + - Gentle buzz for minor deviations + - Strong buzz for major deviations + - Gradient feedback system + +--- + +## Implementation Priority Matrix + +### Phase 1: Core Stability (Completed) + +- ✅ State machine +- ✅ Activity recording +- ✅ Pause/Resume +- ✅ Save/Discard +- ✅ Basic haptic alerts + +### Phase 2: User Customization (Current) + +- 🔄 Configurable alert settings +- 🔄 Battery optimization +- 🔄 Chart rendering optimization + +### Phase 3: Advanced Features (Future) + +- 📋 Smooth bars +- 📋 Zone boundary lines +- 📋 Statistical overlays +- 📋 Terrain-adaptive zones + +### Phase 4: Polish & Enhancement (Future) + +- 📋 Fade old bars +- 📋 Auto-adjust chart duration +- 📋 CSV export +- 📋 Dynamic memory management +- 📋 Night mode +- 📋 Progressive alert intensity + +--- + +## Technical Debt & Code Quality + +### Refactoring Needed + +- [ ] Extract chart rendering to `ChartRenderer.mc` class +- [ ] Create `CircularBuffer.mc` reusable class +- [ ] Consolidate color constants into `Colors.mc` +- [ ] Create `HapticManager.mc` for centralized vibration control +- [ ] Add input validation layer for all settings +- [ ] Document all public methods with JSDoc-style comments + +### Testing & Quality + +- [ ] Add unit tests for CQ algorithm +- [ ] Add integration tests for state machine +- [ ] Add haptic feedback timing tests +- [ ] Profile memory usage during 2+ hour activities +- [ ] Benchmark chart rendering on FR165 vs FR165 Music +- [ ] Test sensor disconnection recovery +- [ ] Test haptic alerts across rapid zone transitions + +### Performance Profiling Targets + +- [ ] Chart draw time: <50ms per frame +- [ ] Memory usage: <5% of total device memory +- [ ] Battery drain: <5% per hour (GPS active) +- [ ] Haptic timing accuracy: ±1 second + +--- + +## Debugging Guide + +### Common Issues + +**Issue**: Haptic alerts not firing +**Cause**: Attention module not supported or state not RECORDING +**Solution**: + +- Check `Attention has :vibrate` capability +- Verify `_sessionState == RECORDING` +- Confirm cadence is actually out of zone + +**Issue**: Alerts continue after returning to zone +**Cause**: Zone state not properly updated +**Solution**: + +- Check `_lastZoneState` variable +- Verify zone detection logic +- Ensure `stopAlertCycle()` is called + +**Issue**: Double buzz only fires once +**Cause**: `_pendingSecondVibe` not being checked +**Solution**: + +- Confirm `checkPendingVibration()` called in `onUpdate()` +- Verify `_secondVibeTime` calculation +- Check timer precision + +**Issue**: Timer not pausing +**Cause**: ActivityRecording session not properly controlled +**Solution**: Check `activitySession.stop()` is called on pause + +**Issue**: Cadence data not collecting +**Cause**: State not RECORDING or sensor not connected +**Solution**: Verify `_sessionState == RECORDING` and sensor paired + +**Issue**: CQ always shows "--" +**Cause**: Less than MIN_CQ_SAMPLES (30) collected +**Solution**: Wait 30 seconds after starting, check sensor connection + +**Issue**: Chart not updating +**Cause**: View timer not running or data not flowing +**Solution**: Check `_simulationTimer` started in `onShow()` + +### Debug Checklist + +1. ✓ `DEBUG_MODE = true` in GarminApp.mc +2. ✓ Watch console for `[INFO]`, `[DEBUG]`, `[CADENCE]` messages +3. ✓ Verify state transitions match expected flow +4. ✓ Check `_cadenceCount` increments when recording +5. ✓ Confirm `activitySession != null` when active +6. ✓ Validate sensor pairing in Garmin Connect app +7. ✓ Monitor `_lastZoneState` for zone transitions +8. ✓ Verify haptic timing with stopwatch +9. ✓ Check `_alertStartTime` and `_lastAlertTime` values + +### Haptic Debugging + +**Enable Haptic Debug Logging**: + +```monkey-c +// In triggerSingleVibration() +System.println("[HAPTIC] Single buzz triggered at " + System.getTimer()); + +// In triggerDoubleVibration() +System.println("[HAPTIC] Double buzz triggered at " + System.getTimer()); + +// In checkAndTriggerAlerts() +System.println("[HAPTIC] Time since last alert: " + timeSinceLastAlert); +``` + +**Test Haptic Timing**: + +1. Start recording +2. Manually set cadence out of zone +3. Note timestamp of first alert +4. Wait 30 seconds +5. Verify second alert timing +6. Repeat for full 3-minute cycle + +--- + +## Version History + +**Current Version**: 1.1 (January 2026) + +**v1.1 Changes**: + +- ✅ Added: Haptic feedback system + - Single buzz for below-zone cadence + - Double buzz for above-zone cadence + - 30-second repeat interval + - 3-minute maximum duration + - Timer-free implementation +- ✅ Fixed: Timer creation overhead +- ✅ Added: Zone state tracking +- ✅ Improved: Memory efficiency +- ✅ Updated: Documentation with flow diagrams + +**v1.0 Changes** (from original): + +- ✅ Fixed: Uncommented critical recording check (line 270) +- ✅ Added: Full state machine (IDLE/RECORDING/PAUSED/STOPPED) +- ✅ Added: Pause/Resume functionality +- ✅ Added: Save/Discard workflow +- ✅ Added: Garmin ActivityRecording integration +- ✅ Added: Menu system for activity control +- ✅ Fixed: Timer now properly pauses/resumes +- ✅ Added: Visual state indicators +- ✅ Added: Comprehensive documentation + +**Known Limitations**: + +- No persistent storage of CQ history +- No lap/split functionality +- No custom alert thresholds +- No data export capability +- Haptic intensity not configurable +- No terrain-adaptive zones + +--- + +## Glossary + +**CQ**: Cadence Quality - composite score measuring running efficiency +**FIT File**: Flexible and Interoperable Transfer - Garmin's activity file format +**SPM**: Steps Per Minute - cadence measurement unit +**Circular Buffer**: Fixed-size buffer that wraps when full +**Activity Session**: Garmin's ActivityRecording instance managing timer/sensors +**State Machine**: System that transitions between defined states based on events +**Delegate Pattern**: Separation of input handling from view logic +**MVC**: Model-View-Controller architecture pattern +**Haptic Feedback**: Tactile vibration alerts +**Zone State**: Current cadence position relative to target range (-1/0/1) +**Alert Cycle**: Period of repeated haptic alerts (3 minutes maximum) +**Timer-Free**: Architecture using timestamps instead of dedicated timers + +--- + +## Other Info + +**Application**: Garmin Cadence Monitoring App for Forerunner 165 +**Platform**: Garmin Connect IQ SDK 8.3.0 +**Language**: Monkey C +**Target API**: 5.2.0+ +**Documentation Version**: 2.0 +**Last Updated**: January 2026 + +## Special Mentions for their amazing work this semester. + +**Dom** +**Chum** +**Jack** +**Kyle** +**Jin** + +--- diff --git a/combined-source.txt b/combined-source.txt index a69446d..bc46487 100644 --- a/combined-source.txt +++ b/combined-source.txt @@ -8,6 +8,7 @@ ===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\resources\layouts\layout.xml ===== + + + + + + ===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\resources\menus\menu.xml ===== @@ -105,638 +129,6 @@ -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\CustomizableDelegates\SelectBarChartDelegate.mc ===== - -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectBarChartDelegate extends WatchUi.Menu2InputDelegate { - - private var _menu as WatchUi.Menu2; - var app = getApp(); - var chartDuration = app.getChartDuration(); - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - _menu = menu; - var newTitle = Lang.format("Chart: $1$", [chartDuration]); - - // This updates the UI when the chart duration is changed - _menu.setTitle(newTitle); - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Try to change cadence range based off menu selection - if (id == :chart_15m){ - app.setChartDuration(GarminApp.FifteenminChart); - } - else if (id == :chart_30m){ - app.setChartDuration(GarminApp.ThirtyminChart); - } - else if (id == :chart_1h){ - app.setChartDuration(GarminApp.OneHourChart); - } - else if (id == :chart_2h){ - app.setChartDuration(GarminApp.TwoHourChart); - } - else {System.println("ERROR");} - - WatchUi.popView(WatchUi.SLIDE_RIGHT); - - } - - function onMenuItem(item as Symbol) as Void {} - - //returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} - - - -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\CustomizableDelegates\SelectCustomizableDelegate.mc ===== - -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectCutomizableDelegate extends WatchUi.Menu2InputDelegate { - - //private var _menu as WatchUi.Menu2; - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - //_menu = menu; - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Add if more customizable options are added - if (id == :cust_bar_chart){ - pushBarChartMenu(); - } - else {System.println("ERROR");} - - } - - function pushBarChartMenu() as Void { - var menu = new WatchUi.Menu2({ - :title => "Bar Chart Length:" - }); - - menu.addItem(new WatchUi.MenuItem("15 Minute", null, :chart_15m, null)); - menu.addItem(new WatchUi.MenuItem("30 Minute", null, :chart_30m, null)); - menu.addItem(new WatchUi.MenuItem("1 Hour", null, :chart_1h, null)); - menu.addItem(new WatchUi.MenuItem("2 Hour", null, :chart_2h, null)); - - //pushes the view to the screen with the relevent delegate - WatchUi.pushView(menu, new SelectBarChartDelegate(menu), WatchUi.SLIDE_LEFT); - } - - function onMenuItem(item as Symbol) as Void {} - - //returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} - - - -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\FeedbackDelegates\SelectAudibleDelegate.mc ===== - -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectAudibleDelegate extends WatchUi.Menu2InputDelegate { - - private var _menu as WatchUi.Menu2; - var app = Application.getApp() as GarminApp; - //var Audible = app.getAudible(); - var Audible = "low";// make sure to change to above!! - after feature has been added - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - _menu = menu; - - var newTitle = Lang.format("Audible: $1$", [Audible]); - - // This updates the UI when the cadence is changed - _menu.setTitle(newTitle); - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Try to change cadence range based off menu selection - if (id == :audible_low){ - System.println("Audible Feedback: LOW"); - //app.setAudible("low"); - } - else if (id == :audible_med){ - System.println("Audible Feedback: MEDIUM"); - //app.setUserAudible("med"); - } - else if (id == :audible_high){ - System.println("Audible Feedback: HIGH"); - //app.setUserAudible("high"); - } else {System.println("ERROR");} - - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } - - function onMenuItem(item as Symbol) as Void {} - - // Returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} - - - -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\FeedbackDelegates\SelectFeedbackDelegate.mc ===== - -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectFeedbackDelegate extends WatchUi.Menu2InputDelegate { - - //private var _menu as WatchUi.Menu2; - var app = Application.getApp() as GarminApp; - //var experienceLvl = app.getUserGender(); - var gender = "Other";// make sure to change to above!! - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - //_menu = menu; - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Try to change cadence range based off menu selection - if (id == :haptic_feedback){ - System.println("Haptic menu selected"); - pushHapticSettings(); - } - else if (id == :audible_feedback){ - System.println("Audible menu selected"); - pushAudibleSettings(); - } else {System.println("ERROR");} - - } - - function pushHapticSettings() as Void{ - var menu = new WatchUi.Menu2({ - :title => "Haptic Settings" - }); - //temp items since feedback has not yet been implemented - menu.addItem(new WatchUi.MenuItem("Low", null, :haptic_low, null)); - menu.addItem(new WatchUi.MenuItem("Medium", null, :haptic_med, null)); - menu.addItem(new WatchUi.MenuItem("High", null, :haptic_high, null)); - - //pushes the view to the screen with the relevent delegate - WatchUi.pushView(menu, new SelectHapticDelegate(menu), WatchUi.SLIDE_LEFT); - } - - function pushAudibleSettings() as Void{ - var menu = new WatchUi.Menu2({ - :title => "Audible Settings" - }); - - menu.addItem(new WatchUi.MenuItem("Low", null, :audible_low, null)); - menu.addItem(new WatchUi.MenuItem("Medium", null, :audible_med, null)); - menu.addItem(new WatchUi.MenuItem("High", null, :audible_high, null)); - - //pushes the view to the screen with the relevent delegate - WatchUi.pushView(menu, new SelectAudibleDelegate(menu), WatchUi.SLIDE_LEFT); - } - - function onMenuItem(item as Symbol) as Void {} - - // Returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} - - - -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\FeedbackDelegates\SelectHapticDelegate.mc ===== - -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectHapticDelegate extends WatchUi.Menu2InputDelegate { - - private var _menu as WatchUi.Menu2; - var app = Application.getApp() as GarminApp; - //var haptic = app.getHaptic(); - var haptic = "low";// make sure to change to above!! - after feature has been added - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - _menu = menu; - - var newTitle = Lang.format("Haptic: $1$", [haptic]); - - // This updates the UI when the cadence is changed - _menu.setTitle(newTitle); - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Try to change cadence range based off menu selection - if (id == :haptic_low){ - System.println("Haptic Feedback: LOW"); - //app.setHaptic("low"); - } - else if (id == :haptic_med){ - System.println("Haptic Feedback: MEDIUM"); - //app.setUserHaptic("med"); - } - else if (id == :haptic_high){ - System.println("Haptic Feedback: HIGH"); - //app.setUserHaptic("high"); - } else {System.println("ERROR");} - - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } - - function onMenuItem(item as Symbol) as Void {} - - // Returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} - - - -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\ProfileDelegates\ProfilePickerDelegate.mc ===== - -import Toybox.WatchUi; -import Toybox.System; -import Toybox.Application; -import Toybox.Lang; - -class ProfilePickerDelegate extends WatchUi.PickerDelegate { - - private var _typeId; - - function initialize(typeId) { - PickerDelegate.initialize(); - _typeId = typeId; - } - - function onAccept(values as Array) as Boolean { - var pickedValue = values[0]; // Gets the "selected" value - - var app = Application.getApp() as GarminApp; - - if (_typeId == :prof_height) { - System.println("Height Saved: " + pickedValue); - app.setUserHeight(pickedValue); - } - else if (_typeId == :prof_speed) { - System.println("Speed Saved: " + pickedValue); - app.setUserSpeed(pickedValue); - } - - app.idealCadenceCalculator(); - - WatchUi.popView(WatchUi.SLIDE_RIGHT); - return true; - } - - function onCancel() as Boolean { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - return true; - } -} - - - -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\ProfileDelegates\ProfilePickerFactory.mc ===== - -import Toybox.WatchUi; -import Toybox.Graphics; -import Toybox.Lang; - -class ProfilePickerFactory extends WatchUi.PickerFactory { - private var _start as Number; - private var _stop as Number; - private var _increment as Number; - private var _label as String; - - function initialize(start as Number, stop as Number, increment as Number, options as Dictionary?) { - PickerFactory.initialize(); - _start = start; - _stop = stop; - _increment = increment; - _label = ""; - - if (options != null) { - if (options.hasKey(:label)) { - _label = options[:label] as String; - } - } - } - - function getSize() as Number { - return (_stop - _start) / _increment + 1; - } - - function getValue(index as Number) as Object? { - return _start + (index * _increment); - } - - function getDrawable(index as Number, selected as Boolean) as Drawable? { - - // gets the selected value - var val = getValue(index); - - // converts to number if needed - if (val has :toNumber) { - val = val.toNumber(); - } - - // string that is displayed (e.g. "175" + " cm") - var displayString = Lang.format("$1$$2$", [val, _label]); - - return new WatchUi.Text({ - :text => displayString, - :color => Graphics.COLOR_WHITE, - :font => Graphics.FONT_MEDIUM, - :locX => WatchUi.LAYOUT_HALIGN_CENTER, - :locY => WatchUi.LAYOUT_VALIGN_CENTER - }); - } - - function getIndex(value as Number) as Number { - - var safeValue = value; - if (safeValue has :toNumber) { - safeValue = safeValue.toNumber(); - } - - return (safeValue - _start) / _increment; - } -} - - - -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\ProfileDelegates\SelectExperienceDelegate.mc ===== - -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectExperienceDelegate extends WatchUi.Menu2InputDelegate { - - private var _menu as WatchUi.Menu2; - var app = Application.getApp() as GarminApp; - var experienceLvl = app.getExperienceLvl(); - var experienceLvlString = "NULL"; - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - _menu = menu; - - if (experienceLvl == GarminApp.Beginner){ - experienceLvlString = "Beginner"; - } else if (experienceLvl == GarminApp.Intermediate){ - experienceLvlString = "Intermediate"; - } else if (experienceLvl == GarminApp.Advanced){ - experienceLvlString = "Advanced"; - } - var newTitle = Lang.format("Experience: $1$", [experienceLvlString]); - - // This updates the UI when the experience level is changed - _menu.setTitle(newTitle); - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Try to change user experience lvl based off menu selection - if (id == :exp_beginner){ - System.println("User ExperienceLvl: Beginner"); - app.setExperienceLvl(GarminApp.Beginner); - } - else if (id == :exp_intermediate){ - System.println("User ExperienceLvl: Intermediate"); - app.setExperienceLvl(GarminApp.Intermediate); - } - else if (id == :exp_advanced){ - System.println("User ExperienceLvl: Advanced"); - app.setExperienceLvl(GarminApp.Advanced); - } else {System.println("ERROR");} - - app.idealCadenceCalculator(); - - WatchUi.popView(WatchUi.SLIDE_RIGHT); - - } - - - function onMenuItem(item as Symbol) as Void {} - - // Returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} - - - -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\ProfileDelegates\SelectGenderDelegate.mc ===== - -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -class SelectGenderDelegate extends WatchUi.Menu2InputDelegate { - - private var _menu as WatchUi.Menu2; - var app = Application.getApp() as GarminApp; - var gender = app.getUserGender(); - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - _menu = menu; - - // need if statements to display experiencelvl string instead of float values - var newTitle = Lang.format("Gender: $1$", [gender]); - - // This updates the UI when the cadence is changed - _menu.setTitle(newTitle); - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //Try to change user gender based off menu selection - if (id == :user_male){ - app.setUserGender(GarminApp.Male); - System.println("User Gender: Male"); - } - else if (id == :user_female){ - app.setUserGender(GarminApp.Female); - System.println("User Gender: Female"); - } - else if (id == :user_other){ - app.setUserGender(GarminApp.Other); - System.println("User Gender: Other"); - } else {System.println("ERROR");} - - app.idealCadenceCalculator(); - - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } - - function onMenuItem(item as Symbol) as Void {} - - // Returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } -} - - - -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\ProfileDelegates\SelectProfileDelegate.mc ===== - -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; -import Toybox.Graphics; - -class SelectProfileDelegate extends WatchUi.Menu2InputDelegate { - - //private var _menu as WatchUi.Menu2; - var app = Application.getApp() as GarminApp; - - function initialize(menu as WatchUi.Menu2) { - Menu2InputDelegate.initialize(); - //_menu = menu; - } - - function onSelect(item) as Void { - - var id = item.getId(); - - //displays the menu for the selected item - if (id == :profile_height){ - heightPicker(); - } - else if (id == :profile_speed){ - speedPicker(); - } - else if (id == :profile_experience){ - experienceMenu(); - } - else if (id == :profile_gender){ - genderMenu(); - } - } - - function onMenuItem(item as Symbol) as Void {} - - // Returns back one menu - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } - - function heightPicker() as Void { - - var currentHeight = app.getUserHeight(); - if (currentHeight == null) { currentHeight = 175; } // Default 175 cm - - var factory = new ProfilePickerFactory(100, 250, 1, {:label=>" cm"}); - - var picker = new WatchUi.Picker({ - :title => new WatchUi.Text({:text=>"Set Height", :locX=>WatchUi.LAYOUT_HALIGN_CENTER, :locY=>WatchUi.LAYOUT_VALIGN_BOTTOM, :color=>Graphics.COLOR_WHITE}), - :pattern => [factory], - :defaults => [factory.getIndex(currentHeight)] - }); - - WatchUi.pushView(picker, new ProfilePickerDelegate(:prof_height), WatchUi.SLIDE_LEFT); - - } - - function speedPicker() as Void { - //uses number not float - var currentSpeed = app.getUserSpeed().toNumber(); - if (currentSpeed == null) { currentSpeed = 3; } // Default 3 km/h - - var factory = new ProfilePickerFactory(3, 30, 1, {:label=>" km/h"}); - - var picker = new WatchUi.Picker({ - :title => new WatchUi.Text({:text=>"Set Speed", :locX=>WatchUi.LAYOUT_HALIGN_CENTER, :locY=>WatchUi.LAYOUT_VALIGN_BOTTOM, :color=>Graphics.COLOR_WHITE}), - :pattern => [factory], - :defaults => [factory.getIndex(currentSpeed)] - }); - - WatchUi.pushView(picker, new ProfilePickerDelegate(:prof_speed), WatchUi.SLIDE_LEFT); - - } - - function experienceMenu() as Void { - var menu = new WatchUi.Menu2({ - :title => "Set Experience" - }); - - menu.addItem(new WatchUi.MenuItem("Beginner", null, :exp_beginner, null)); - menu.addItem(new WatchUi.MenuItem("Intermediate", null, :exp_intermediate, null)); - menu.addItem(new WatchUi.MenuItem("Advanced", null, :exp_advanced, null)); - - //pushes the view to the screen with the relevent delegate - WatchUi.pushView(menu, new SelectExperienceDelegate(menu), WatchUi.SLIDE_LEFT); - } - - function genderMenu() as Void { - var menu = new WatchUi.Menu2({ - :title => "Set Gender" - }); - - menu.addItem(new WatchUi.MenuItem("Male", null, :user_male, null)); - menu.addItem(new WatchUi.MenuItem("Female", null, :user_female, null)); - menu.addItem(new WatchUi.MenuItem("Other", null, :user_other, null)); - - //pushes the view to the screen with the relevent delegate - WatchUi.pushView(menu, new SelectGenderDelegate(menu), WatchUi.SLIDE_LEFT); - } - -} - - - ===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\CadenceRangePickerDelegate.mc ===== import Toybox.WatchUi; @@ -907,113 +299,65 @@ class SelectCadenceDelegate extends WatchUi.Menu2InputDelegate { } catch (ex) { System.println("[ERROR] Exception in maxCadencePicker: " + ex.getErrorMessage()); - } - } -} - - - -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\SettingsDelegate.mc ===== - -import Toybox.Lang; -import Toybox.System; -import Toybox.WatchUi; -import Toybox.Application; - -//this Delegate handels the menu items and creates the menus for each item -class SettingsMenuDelegate extends WatchUi.Menu2InputDelegate { - - function initialize() { - Menu2InputDelegate.initialize(); - } - - //triggers when user selects a menu option - function onSelect(item as WatchUi.MenuItem) as Void { - var id = item.getId(); - - //pushes next menu view based on selection - if (id == :set_profile) { - System.println("Selected: Set Profile"); - //function to push next view - pushProfileMenu(); - } - else if (id == :cust_options) { - System.println("Selected: Customizable Options"); - pushCustMenu(); - } - else if (id == :feedback_options) { - System.println("Selected: Feedback Options"); - pushFeedbackMenu(); - } - else if (id == :cadence_range) { - pushCadenceMenu(); - } - } - - //allows user to go back from the menu view - function onBack() as Void { - WatchUi.popView(WatchUi.SLIDE_RIGHT); - } - - function pushProfileMenu() as Void{ - - //creates the secondary menu and sets title - var menu = new WatchUi.Menu2({ - :title => "Profile Options" - }); - - //creates the new menu items - menu.addItem(new WatchUi.MenuItem("Height", null, :profile_height, null)); - menu.addItem(new WatchUi.MenuItem("Speed", null, :profile_speed, null)); - menu.addItem(new WatchUi.MenuItem("Experience level", null, :profile_experience, null)); - menu.addItem(new WatchUi.MenuItem("Gender", null, :profile_gender, null)); - - //pushes the view to the screen with the relevent delegate - WatchUi.pushView(menu, new SelectProfileDelegate(menu), WatchUi.SLIDE_LEFT); - - } - - function pushCustMenu() as Void{ - - var menu = new WatchUi.Menu2({ - :title => "Customization Options" - }); + } + } +} - menu.addItem(new WatchUi.MenuItem("Bar Chart", null, :cust_bar_chart, null)); - WatchUi.pushView(menu, new SelectCutomizableDelegate(menu), WatchUi.SLIDE_LEFT); - } +===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SettingsDelegates\WatchFaceMenuDelegate.mc ===== - function pushFeedbackMenu() as Void{ - - var menu = new WatchUi.Menu2({ - :title => "Feedback Options" - }); +import Toybox.Lang; +import Toybox.System; +import Toybox.WatchUi; - menu.addItem(new WatchUi.MenuItem("Haptic Feedback", null, :haptic_feedback, null)); - menu.addItem(new WatchUi.MenuItem("Audible Feedback", null, :audible_feedback, null)); +// WatchFaceMenuDelegate handles user interactions with the watch face view selection menu. +// It allows users to choose between different view options (Simple View, Time View) and +// manages the navigation between these views. +class WatchFaceMenuDelegate extends WatchUi.Menu2InputDelegate { - WatchUi.pushView(menu, new SelectFeedbackDelegate(menu), WatchUi.SLIDE_LEFT); + // Initialize the delegate with the provided menu. + function initialize(menu as WatchUi.Menu2) { + Menu2InputDelegate.initialize(); } - function pushCadenceMenu() as Void { + // Handle menu item selection by the user. + // Routes the selection to the appropriate view switching method based on the menu item ID. + function onSelect(item as WatchUi.MenuItem) as Void { + var id = item.getId(); - //sets the cadence variables to the global app variable to be used within the title - var app = Application.getApp() as GarminApp; - var minCadence = app.getMinCadence(); - var maxCadence = app.getMaxCadence(); + if (id == :simple_view) { + System.println("Selected: Simple View"); + switchToSimpleView(); + } else if (id == :time_view) { + System.println("Selected: Time View"); + switchToTimeView(); + } + } - var menu = new WatchUi.Menu2({ - :title => Lang.format("Cadence: $1$ - $2$", [minCadence, maxCadence]) - }); + // Handle back button press by closing the menu and returning to the previous view. + function onBack() as Void { + WatchUi.popView(WatchUi.SLIDE_RIGHT); + } - menu.addItem(new WatchUi.MenuItem(WatchUi.loadResource(Rez.Strings.menu_inc_min), null, :item_inc_min, null)); - menu.addItem(new WatchUi.MenuItem(WatchUi.loadResource(Rez.Strings.menu_dec_min), null, :item_dec_min, null)); - menu.addItem(new WatchUi.MenuItem(WatchUi.loadResource(Rez.Strings.menu_inc_max), null, :item_inc_max, null)); - menu.addItem(new WatchUi.MenuItem(WatchUi.loadResource(Rez.Strings.menu_dec_max), null, :item_dec_max, null)); + // Switch the current view to the Simple View by popping the menu layers and pushing the new view. + // Pops two views to clear the menu and settings layers, then displays the Simple View. + private function switchToSimpleView() as Void { + WatchUi.popView(WatchUi.SLIDE_DOWN); + WatchUi.popView(WatchUi.SLIDE_DOWN); + var view = new SimpleView(); + var delegate = new SimpleViewDelegate(); + WatchUi.pushView(view, delegate, WatchUi.SLIDE_IMMEDIATE); + } - WatchUi.pushView(menu, new SelectCadenceDelegate(menu), WatchUi.SLIDE_LEFT); + // Switch the current view to the Time View by popping the menu layers and pushing the new view. + // Pops two views to clear the menu and settings layers, then displays the Time View. + private function switchToTimeView() as Void { + WatchUi.popView(WatchUi.SLIDE_DOWN); + WatchUi.popView(WatchUi.SLIDE_DOWN); + var view = new TimeView(); + var delegate = new TimeViewDelegate(); + WatchUi.pushView(view, delegate, WatchUi.SLIDE_IMMEDIATE); } } @@ -1055,9 +399,23 @@ class AdvancedViewDelegate extends WatchUi.BehaviorDelegate { function onKey(keyEvent as WatchUi.KeyEvent) as Boolean { var key = keyEvent.getKey(); + // Scroll down to SimpleView (completing the loop) + if(key == WatchUi.KEY_DOWN) { + WatchUi.switchToView( + new SimpleView(), + new SimpleViewDelegate(), + WatchUi.SLIDE_DOWN + ); + return true; + } + // UP button - Back to SimpleView if (key == WatchUi.KEY_UP) { - WatchUi.popView(WatchUi.SLIDE_UP); + WatchUi.switchToView( + new SimpleView(), + new SimpleViewDelegate(), + WatchUi.SLIDE_UP + ); return true; } @@ -1084,7 +442,7 @@ class AdvancedViewDelegate extends WatchUi.BehaviorDelegate { } function onBack() as Boolean { - WatchUi.popView(WatchUi.SLIDE_BLINK); + // Back button disabled - no input return true; } @@ -1377,6 +735,119 @@ class ConfirmationDelegate extends WatchUi.Menu2InputDelegate { +===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\SummaryViewDelegate.mc ===== + +import Toybox.Lang; +import Toybox.WatchUi; +import Toybox.System; + +class SummaryViewDelegate extends WatchUi.BehaviorDelegate { + + function initialize() { + BehaviorDelegate.initialize(); + } + + // SELECT or any key to dismiss and return to SimpleView + function onSelect() as Boolean { + System.println("[SUMMARY] Returning to main view"); + WatchUi.popView(WatchUi.SLIDE_DOWN); + return true; + } + + // BACK button disabled - no input + function onBack() as Boolean { + return true; + } + + // Swipe left to dismiss + function onSwipe(event as WatchUi.SwipeEvent) as Boolean { + var direction = event.getDirection(); + + if (direction == WatchUi.SWIPE_LEFT || direction == WatchUi.SWIPE_DOWN) { + System.println("[SUMMARY] Swiped to dismiss"); + WatchUi.popView(WatchUi.SLIDE_DOWN); + return true; + } + + return false; + } + + // Any key press dismisses + function onKey(keyEvent as WatchUi.KeyEvent) as Boolean { + var key = keyEvent.getKey(); + + // Allow any key to dismiss + if (key == WatchUi.KEY_UP || key == WatchUi.KEY_DOWN || + key == WatchUi.KEY_ENTER || key == WatchUi.KEY_MENU) { + WatchUi.popView(WatchUi.SLIDE_DOWN); + return true; + } + + return false; + } +} + + + +===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Delegates\TimeViewDelegate.mc ===== + +import Toybox.Lang; +import Toybox.WatchUi; + +class TimeViewDelegate extends WatchUi.BehaviorDelegate { + + function initialize() { + BehaviorDelegate.initialize(); + } + + // Long-press MENU to open settings + function onMenu() as Boolean { + pushSettingsView(); + return true; + } + + // Down button to scroll to AdvancedView + function onKey(keyEvent as WatchUi.KeyEvent) as Boolean { + var key = keyEvent.getKey(); + + if (key == WatchUi.KEY_DOWN) { + var advancedView = new AdvancedView(); + WatchUi.switchToView( + advancedView, + new AdvancedViewDelegate(advancedView), + WatchUi.SLIDE_DOWN + ); + return true; + } + + if (key == WatchUi.KEY_UP) { + return true; + } + + return false; + } + + // Back button - do nothing to prevent crash + function onBack() as Boolean { + return true; + } +} + +function pushSettingsView() as Void { + var settingsView = new SettingsView(); + var settingsDelegate = new SettingsDelegate(); + WatchUi.pushView(settingsView, settingsDelegate, WatchUi.SLIDE_LEFT); +} + + +class SettingsDelegate extends WatchUi.BehaviorDelegate { + function initialize() { + BehaviorDelegate.initialize(); + } +} + + + ===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Views\AdvancedView.mc ===== import Toybox.Graphics; @@ -1385,12 +856,22 @@ import Toybox.Activity; import Toybox.Lang; import Toybox.Timer; import Toybox.System; +import Toybox.Attention; class AdvancedView extends WatchUi.View { const MAX_BARS = 280; const MAX_CADENCE_DISPLAY = 200; private var _simulationTimer; + + // Vibration alert tracking (no extra timers needed!) + private var _lastZoneState = 0; // -1 = below, 0 = inside, 1 = above + private var _alertStartTime = null; + private var _alertDuration = 180000; // 3 minutes in milliseconds + private var _alertInterval = 30000; // 30 seconds in milliseconds + private var _lastAlertTime = 0; + private var _pendingSecondVibe = false; + private var _secondVibeTime = 0; function initialize() { View.initialize(); @@ -1406,10 +887,19 @@ class AdvancedView extends WatchUi.View { _simulationTimer.stop(); _simulationTimer = null; } + // Reset alert state + _alertStartTime = null; + _lastAlertTime = 0; } function onUpdate(dc as Dc) as Void { - View.onUpdate(dc); + // Check cadence zone for vibration alerts + checkCadenceZone(); + + // Check for pending second vibration + checkPendingVibration(); + + View.onUpdate(dc); // Draw all the elements drawElements(dc); } @@ -1417,8 +907,112 @@ class AdvancedView extends WatchUi.View { function refreshScreen() as Void { WatchUi.requestUpdate(); } + + function checkPendingVibration() as Void { + if (_pendingSecondVibe) { + var currentTime = System.getTimer(); + if (currentTime >= _secondVibeTime) { + // Trigger second vibration + if (Attention has :vibrate) { + var vibeData = [new Attention.VibeProfile(50, 200)]; + Attention.vibrate(vibeData); + } + _pendingSecondVibe = false; + } + } + } + + function triggerSingleVibration() as Void { + if (Attention has :vibrate) { + var vibeData = [new Attention.VibeProfile(50, 200)]; + Attention.vibrate(vibeData); + } + } + + function triggerDoubleVibration() as Void { + if (Attention has :vibrate) { + // First vibration + var vibeData = [new Attention.VibeProfile(50, 200)]; + Attention.vibrate(vibeData); + + // Schedule second vibration after 240ms + _pendingSecondVibe = true; + _secondVibeTime = System.getTimer() + 240; + } + } + + function checkAndTriggerAlerts() as Void { + // Only check if we're in an alert period + if (_alertStartTime == null) { + return; + } + + var currentTime = System.getTimer(); + var elapsed = currentTime - _alertStartTime; + + // Stop alerting after 3 minutes + if (elapsed >= _alertDuration) { + _alertStartTime = null; + _lastAlertTime = 0; + return; + } + + // Check if it's time for the next alert (every 30 seconds) + var timeSinceLastAlert = currentTime - _lastAlertTime; + if (timeSinceLastAlert >= _alertInterval) { + _lastAlertTime = currentTime; + + // Trigger the appropriate vibration + if (_lastZoneState == -1) { + triggerSingleVibration(); + } else if (_lastZoneState == 1) { + triggerDoubleVibration(); + } + } + } + + function checkCadenceZone() as Void { + var info = Activity.getActivityInfo(); + var app = getApp(); + var minZone = app.getMinCadence(); + var maxZone = app.getMaxCadence(); + + // Determine zone state + var newZoneState = 0; + if (info != null && info.currentCadence != null) { + var c = info.currentCadence; + if (c < minZone) { + newZoneState = -1; + } else if (c > maxZone) { + newZoneState = 1; + } else { + newZoneState = 0; + } + } - + // Trigger alerts on zone crossing + if (newZoneState != _lastZoneState) { + if (newZoneState == -1) { + // Below minimum - start alert cycle + _alertStartTime = System.getTimer(); + _lastAlertTime = System.getTimer(); + triggerSingleVibration(); + } else if (newZoneState == 1) { + // Above maximum - start alert cycle + _alertStartTime = System.getTimer(); + _lastAlertTime = System.getTimer(); + triggerDoubleVibration(); + } else { + // Back in zone - stop alerts + _alertStartTime = null; + _lastAlertTime = 0; + } + _lastZoneState = newZoneState; + } else { + // Still out of zone - check if we need to alert again + checkAndTriggerAlerts(); + } + } function drawElements(dc as Dc) as Void { var width = dc.getWidth(); @@ -1631,75 +1225,6 @@ class AdvancedView extends WatchUi.View { -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Views\SettingsView.mc ===== - -import Toybox.Graphics; -import Toybox.WatchUi; -import Toybox.System; -import Toybox.Application; -import Toybox.Lang; -import Toybox.Math; - -class SettingsView extends WatchUi.View { - - // to store the coords and width/heigh of the button (for cadence for now) - private var _buttonCoords as Array?; - - - function initialize() { - View.initialize(); - _buttonCoords = [0, 0, 0, 0] as Array; - } - - - function onLayout(dc as Dc) as Void { - - // Define button dimensions based on screen size (rough values for now) - var screenWidth = dc.getWidth(); - var screenHeight = dc.getHeight(); - - var x1 = screenWidth * 0.2; - var y1 = screenHeight / 2; - var width = screenWidth - (screenWidth * 0.4); - var height = screenHeight / 3; - - // Sets button coords - _buttonCoords = [x1, y1, width, height] as Array; - System.println(x1.toString() + " and " + y1.toString() + " and " + width.toString() + " and " + height.toString()); - - } - - function onShow() as Void {} - - function onUpdate(dc as Dc) as Void { - - View.onUpdate(dc); - drawCadenceButton(dc); - - } - - // Draws the temp button - function drawCadenceButton(dc as Dc) as Void { - - dc.setColor(Graphics.COLOR_BLUE, Graphics.COLOR_BLUE); - dc.drawRoundedRectangle(_buttonCoords[0], _buttonCoords[1], _buttonCoords[2], _buttonCoords[3], 10); - - } - - // Public getter method for the button coordinates - function getButtonCoords() as Array { - return _buttonCoords; - } - - function refreshScreen() as Void{ - WatchUi.requestUpdate(); - } - - function onHide() as Void {} -} - - - ===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Views\SimpleView.mc ===== import Toybox.Graphics; @@ -1708,6 +1233,7 @@ import Toybox.Activity; import Toybox.Lang; import Toybox.Timer; import Toybox.System; +import Toybox.Attention; class SimpleView extends WatchUi.View { @@ -1718,35 +1244,21 @@ class SimpleView extends WatchUi.View { private var _timeDisplay; 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. - // Replace the println below with the device vibration call when supported, - // e.g. `Haptics.vibrate(120)` or `System.vibrate(120)` on SDKs that provide it. - System.println("[vibe] second pulse"); - } + + // Vibration alert tracking (no extra timers needed!) + private var _alertStartTime = null; + private var _alertDuration = 180000; // 3 minutes in milliseconds + private var _alertInterval = 30000; // 30 seconds in milliseconds + private var _lastAlertTime = 0; + private var _pendingSecondVibe = false; + private var _secondVibeTime = 0; function initialize() { View.initialize(); } - // Load your resources here - function onLayout(dc as Dc) as Void { - setLayout(Rez.Layouts.MainLayout(dc)); - _cadenceDisplay = findDrawableById("cadence_text"); - _cadenceZoneDisplay = findDrawableById("cadence_zone"); - _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 // the state of this View and prepare it to be shown. This includes // loading resources into memory. @@ -1760,6 +1272,9 @@ class SimpleView extends WatchUi.View { //update the display for current cadence displayCadence(); + // Check for pending second vibration + checkPendingVibration(); + // Draw recording indicator drawRecordingIndicator(dc); @@ -1775,10 +1290,76 @@ class SimpleView extends WatchUi.View { _refreshTimer.stop(); _refreshTimer = null; } - } - - function refreshScreen() as Void{ - WatchUi.requestUpdate(); + // Reset alert state + _alertStartTime = null; + _lastAlertTime = 0; + } + + function refreshScreen() as Void{ + WatchUi.requestUpdate(); + } + + function checkPendingVibration() as Void { + if (_pendingSecondVibe) { + var currentTime = System.getTimer(); + if (currentTime >= _secondVibeTime) { + // Trigger second vibration + if (Attention has :vibrate) { + var vibeData = [new Attention.VibeProfile(50, 200)]; + Attention.vibrate(vibeData); + } + _pendingSecondVibe = false; + } + } + } + + function triggerSingleVibration() as Void { + if (Attention has :vibrate) { + var vibeData = [new Attention.VibeProfile(50, 200)]; + Attention.vibrate(vibeData); + } + } + + function triggerDoubleVibration() as Void { + if (Attention has :vibrate) { + // First vibration + var vibeData = [new Attention.VibeProfile(50, 200)]; + Attention.vibrate(vibeData); + + // Schedule second vibration after 240ms + _pendingSecondVibe = true; + _secondVibeTime = System.getTimer() + 240; + } + } + + function checkAndTriggerAlerts() as Void { + // Only check if we're in an alert period + if (_alertStartTime == null) { + return; + } + + var currentTime = System.getTimer(); + var elapsed = currentTime - _alertStartTime; + + // Stop alerting after 3 minutes + if (elapsed >= _alertDuration) { + _alertStartTime = null; + _lastAlertTime = 0; + return; + } + + // Check if it's time for the next alert (every 30 seconds) + var timeSinceLastAlert = currentTime - _lastAlertTime; + if (timeSinceLastAlert >= _alertInterval) { + _lastAlertTime = currentTime; + + // Trigger the appropriate vibration + if (_lastZoneState == -1) { + triggerSingleVibration(); + } else if (_lastZoneState == 1) { + triggerDoubleVibration(); + } + } } function drawRecordingIndicator(dc as Dc) as Void { @@ -1831,7 +1412,7 @@ class SimpleView extends WatchUi.View { _cadenceZoneDisplay.setText(zoneText); } - // Trigger haptic on zone crossing: single when falling below min, double when going above max + // Trigger haptic on zone crossing with timed alerts var newZoneState = 0; if (info != null && info.currentCadence != null) { var c = info.currentCadence; @@ -1846,16 +1427,24 @@ class SimpleView extends WatchUi.View { if (newZoneState != _lastZoneState) { if (newZoneState == -1) { - // single short vibration - // single pulse (placeholder) - System.println("[vibe] single pulse (below min)"); + // Below minimum - start alert cycle + _alertStartTime = System.getTimer(); + _lastAlertTime = System.getTimer(); + triggerSingleVibration(); } else if (newZoneState == 1) { - // double short vibration: second pulse scheduled - // first pulse (placeholder) - System.println("[vibe] first pulse (above max)"); - _vibeTimer.start(method(:_secondVibe), 240, false); + // Above maximum - start alert cycle + _alertStartTime = System.getTimer(); + _lastAlertTime = System.getTimer(); + triggerDoubleVibration(); + } else { + // Back in zone - stop alerts + _alertStartTime = null; + _lastAlertTime = 0; } _lastZoneState = newZoneState; + } else { + // Still out of zone - check if we need to alert again + checkAndTriggerAlerts(); } if (info != null && info.currentHeartRate != null){ @@ -1904,6 +1493,470 @@ class SimpleView extends WatchUi.View { } + // Load your resources here + function onLayout(dc as Dc) as Void { + setLayout(Rez.Layouts.MainLayout(dc)); + _cadenceDisplay = findDrawableById("cadence_text"); + _cadenceZoneDisplay = findDrawableById("cadence_zone"); + _heartrateDisplay = findDrawableById("heartrate_text"); + _distanceDisplay = findDrawableById("distance_text"); + _timeDisplay = findDrawableById("time_text"); + _cqDisplay = findDrawableById("cq_text"); + _hardcoreDisplay = findDrawableById("hardcore_text"); + } + +} + + + +===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Views\SummaryView.mc ===== + +import Toybox.Graphics; +import Toybox.WatchUi; +import Toybox.Lang; +import Toybox.System; +import Toybox.Activity; + +class SummaryView extends WatchUi.View { + + function initialize() { + View.initialize(); + } + + function onUpdate(dc as Dc) as Void { + // Clear the screen + dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK); + dc.clear(); + + var app = getApp(); + var width = dc.getWidth(); + var height = dc.getHeight(); + + // Check if we have valid summary data + if (!app.hasValidSummaryData()) { + drawNoDataMessage(dc, width, height); + return; + } + + // Draw summary content + drawSummaryContent(dc, width, height, app); + } + + function drawNoDataMessage(dc as Dc, width as Number, height as Number) as Void { + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText( + width / 2, + height / 2, + Graphics.FONT_MEDIUM, + "No data available", + Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER + ); + } + + function drawSummaryContent(dc as Dc, width as Number, height as Number, app as GarminApp) as Void { + var yPos = 10; + var lineHeight = 25; + var sectionSpacing = 15; + + // Title + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText( + width / 2, + yPos, + Graphics.FONT_SMALL, + "SESSION SUMMARY", + Graphics.TEXT_JUSTIFY_CENTER + ); + yPos += lineHeight + sectionSpacing; + + // Draw separator line + dc.setColor(Graphics.COLOR_DK_GRAY, Graphics.COLOR_TRANSPARENT); + dc.drawLine(10, yPos, width - 10, yPos); + yPos += sectionSpacing; + + // Cadence Quality Score (Large and prominent) + var cq = app.getFinalCadenceQuality(); + if (cq != null) { + var cqColor = getCQColor(cq); + dc.setColor(cqColor, Graphics.COLOR_TRANSPARENT); + + // CQ Label + dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_TRANSPARENT); + dc.drawText( + width / 2, + yPos, + Graphics.FONT_SMALL, + "Cadence Quality", + Graphics.TEXT_JUSTIFY_CENTER + ); + yPos += lineHeight; + + // CQ Score (large) + dc.setColor(cqColor, Graphics.COLOR_TRANSPARENT); + dc.drawText( + width / 2, + yPos, + Graphics.FONT_NUMBER_HOT, + cq.format("%d") + "%", + Graphics.TEXT_JUSTIFY_CENTER + ); + yPos += lineHeight + 5; + + // CQ Confidence and Trend + var confidence = app.getFinalCQConfidence(); + var trend = app.getFinalCQTrend(); + if (confidence != null && trend != null) { + dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_TRANSPARENT); + var statusText = "(" + confidence + ", " + trend + ")"; + dc.drawText( + width / 2, + yPos, + Graphics.FONT_TINY, + statusText, + Graphics.TEXT_JUSTIFY_CENTER + ); + yPos += lineHeight + sectionSpacing; + } + } + + // Draw separator line + dc.setColor(Graphics.COLOR_DK_GRAY, Graphics.COLOR_TRANSPARENT); + dc.drawLine(10, yPos, width - 10, yPos); + yPos += sectionSpacing; + + // Time in Zone + var timeInZone = app.getTimeInZonePercentage(); + if (timeInZone >= 0) { + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText( + 15, + yPos, + Graphics.FONT_SMALL, + "Time in Zone:", + Graphics.TEXT_JUSTIFY_LEFT + ); + + dc.setColor(Graphics.COLOR_GREEN, Graphics.COLOR_TRANSPARENT); + dc.drawText( + width - 15, + yPos, + Graphics.FONT_SMALL, + timeInZone.format("%d") + "%", + Graphics.TEXT_JUSTIFY_RIGHT + ); + yPos += lineHeight + 3; + + // Draw progress bar + drawProgressBar(dc, width, yPos, timeInZone, Graphics.COLOR_GREEN); + yPos += 12 + sectionSpacing; + } + + // Average Cadence + var avgCadence = app.getAverageCadence(); + if (avgCadence > 0) { + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText( + 15, + yPos, + Graphics.FONT_SMALL, + "Avg Cadence:", + Graphics.TEXT_JUSTIFY_LEFT + ); + + dc.setColor(Graphics.COLOR_GREEN, Graphics.COLOR_TRANSPARENT); + dc.drawText( + width - 15, + yPos, + Graphics.FONT_SMALL, + avgCadence.format("%.0f") + " spm", + Graphics.TEXT_JUSTIFY_RIGHT + ); + yPos += lineHeight + sectionSpacing; + } + + // Min/Max Cadence + var minCad = app.getMinCadenceFromHistory(); + var maxCad = app.getMaxCadenceFromHistory(); + if (minCad > 0 && maxCad > 0) { + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText( + 15, + yPos, + Graphics.FONT_TINY, + "Range:", + Graphics.TEXT_JUSTIFY_LEFT + ); + + dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_TRANSPARENT); + var rangeText = minCad.format("%.0f") + "-" + maxCad.format("%.0f") + " spm"; + dc.drawText( + width - 15, + yPos, + Graphics.FONT_TINY, + rangeText, + Graphics.TEXT_JUSTIFY_RIGHT + ); + yPos += lineHeight; + } + + // Target Zone + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText( + 15, + yPos, + Graphics.FONT_TINY, + "Target:", + Graphics.TEXT_JUSTIFY_LEFT + ); + + dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_TRANSPARENT); + var targetText = app.getMinCadence().toString() + "-" + app.getMaxCadence().toString() + " spm"; + dc.drawText( + width - 15, + yPos, + Graphics.FONT_TINY, + targetText, + Graphics.TEXT_JUSTIFY_RIGHT + ); + yPos += lineHeight + sectionSpacing; + + // Draw separator line + dc.setColor(Graphics.COLOR_DK_GRAY, Graphics.COLOR_TRANSPARENT); + dc.drawLine(10, yPos, width - 10, yPos); + yPos += sectionSpacing; + + // Activity Metrics Section + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText( + width / 2, + yPos, + Graphics.FONT_SMALL, + "Activity Metrics", + Graphics.TEXT_JUSTIFY_CENTER + ); + yPos += lineHeight + sectionSpacing; + + // Duration + var duration = app.getSessionDuration(); + if (duration != null) { + var seconds = duration / 1000; + var hours = seconds / 3600; + var minutes = (seconds % 3600) / 60; + var secs = seconds % 60; + var durationText = hours.format("%02d") + ":" + minutes.format("%02d") + ":" + secs.format("%02d"); + + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText( + 15, + yPos, + Graphics.FONT_SMALL, + "Duration:", + Graphics.TEXT_JUSTIFY_LEFT + ); + + dc.setColor(Graphics.COLOR_GREEN, Graphics.COLOR_TRANSPARENT); + dc.drawText( + width - 15, + yPos, + Graphics.FONT_SMALL, + durationText, + Graphics.TEXT_JUSTIFY_RIGHT + ); + yPos += lineHeight + sectionSpacing; + } + + // Distance + var distance = app.getSessionDistance(); + if (distance != null) { + var distanceKm = distance / 100000.0; + + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText( + 15, + yPos, + Graphics.FONT_SMALL, + "Distance:", + Graphics.TEXT_JUSTIFY_LEFT + ); + + dc.setColor(Graphics.COLOR_GREEN, Graphics.COLOR_TRANSPARENT); + dc.drawText( + width - 15, + yPos, + Graphics.FONT_SMALL, + distanceKm.format("%.2f") + " km", + Graphics.TEXT_JUSTIFY_RIGHT + ); + yPos += lineHeight + sectionSpacing; + } + + // Average Heart Rate + var avgHR = app.getAvgHeartRate(); + if (avgHR != null) { + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText( + 15, + yPos, + Graphics.FONT_SMALL, + "Avg HR:", + Graphics.TEXT_JUSTIFY_LEFT + ); + + dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_TRANSPARENT); + dc.drawText( + width - 15, + yPos, + Graphics.FONT_SMALL, + avgHR.toString() + " bpm", + Graphics.TEXT_JUSTIFY_RIGHT + ); + yPos += lineHeight; + } + + // Peak Heart Rate (if different from average) + var peakHR = app.getPeakHeartRate(); + if (peakHR != null && avgHR != null && peakHR > avgHR) { + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText( + 15, + yPos, + Graphics.FONT_TINY, + "Peak HR:", + Graphics.TEXT_JUSTIFY_LEFT + ); + + dc.setColor(Graphics.COLOR_DK_RED, Graphics.COLOR_TRANSPARENT); + dc.drawText( + width - 15, + yPos, + Graphics.FONT_TINY, + peakHR.toString() + " bpm", + Graphics.TEXT_JUSTIFY_RIGHT + ); + yPos += lineHeight; + } + + // Instructions at bottom + yPos = height - 20; + dc.setColor(Graphics.COLOR_DK_GRAY, Graphics.COLOR_TRANSPARENT); + dc.drawText( + width / 2, + yPos, + Graphics.FONT_XTINY, + "Press SELECT to continue", + Graphics.TEXT_JUSTIFY_CENTER + ); + } + + function drawProgressBar(dc as Dc, width as Number, yPos as Number, percentage as Number, color as Number) as Void { + var barWidth = width - 30; // 15px margin on each side + var barHeight = 8; + var barX = 15; + var barY = yPos; + + // Draw background (empty bar) + dc.setColor(Graphics.COLOR_DK_GRAY, Graphics.COLOR_TRANSPARENT); + dc.fillRectangle(barX, barY, barWidth, barHeight); + + // Draw filled portion + if (percentage > 0) { + var filledWidth = (barWidth * percentage / 100.0).toNumber(); + if (filledWidth > barWidth) { filledWidth = barWidth; } + + dc.setColor(color, Graphics.COLOR_TRANSPARENT); + dc.fillRectangle(barX, barY, filledWidth, barHeight); + } + + // Draw border + dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_TRANSPARENT); + dc.drawRectangle(barX, barY, barWidth, barHeight); + } + + function getCQColor(cq as Number) as Number { + if (cq >= 80) { + return Graphics.COLOR_GREEN; + } else if (cq >= 60) { + return Graphics.COLOR_YELLOW; + } else if (cq >= 40) { + return Graphics.COLOR_ORANGE; + } else { + return Graphics.COLOR_RED; + } + } +} + + + +===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Views\TimeView.mc ===== + +import Toybox.Graphics; +import Toybox.WatchUi; +import Toybox.System; +import Toybox.Time; +import Toybox.Time.Gregorian; +import Toybox.Lang; + +class TimeView extends WatchUi.View { + private var _isAwake as Boolean = false; + + function initialize() { + View.initialize(); + } + + // Load your resources here + function onLayout(dc as Dc) as Void { + setLayout(Rez.Layouts.WatchFace(dc)); + } + + // Called when this View is brought to the foreground. Restore + // the state of this View and prepare it to be shown. This includes + // loading resources into memory. + function onShow() as Void { + _isAwake = true; + } + + // Update the view + function onUpdate(dc as Dc) as Void { + var date = Gregorian.info(Time.now(), Time.FORMAT_SHORT); + var dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + + // Format: "Sat 25 Jan" + (View.findDrawableById("Date") as WatchUi.Text).setText( + dayNames[date.day_of_week - 1] + " " + date.day.format("%2d") + " " + monthNames[date.month - 1] + ); + // Convert to 12-hour format with AM/PM + var hour12 = date.hour % 12; + if (hour12 == 0) { + hour12 = 12; + } + var ampm = (date.hour < 12) ? "AM" : "PM"; + (View.findDrawableById("HoursAndMinutes") as WatchUi.Text).setText( + hour12.format("%02d") + ":" + date.min.format("%02d") + ); + (View.findDrawableById("AmPm") as WatchUi.Text).setText(ampm); + + + + // Call the parent onUpdate function to redraw the layout + View.onUpdate(dc); + } + + // Called when this View is removed from the screen. Save the + // state of this View here. This includes freeing resources from + // memory. + function onHide() as Void { + } + + // The user has just looked at their watch. Timers and animations may be started here. + function onExitSleep() as Void { + _isAwake = true; + } + + // Terminate any active timers and prepare for slow updates. + function onEnterSleep() as Void { + _isAwake = false; + } } @@ -1917,7 +1970,7 @@ import Toybox.Timer; import Toybox.Activity; import Toybox.ActivityRecording; import Toybox.System; - +import Toybox.Application.Storage; class GarminApp extends Application.AppBase { const MAX_BARS = 280; @@ -1926,6 +1979,15 @@ class GarminApp extends Application.AppBase { const MIN_CQ_SAMPLES = 30; const DEBUG_MODE = true; + // Property keys for persistent storage + const PROP_USER_HEIGHT = "userHeight"; + const PROP_USER_SPEED = "userSpeed"; + const PROP_USER_GENDER = "userGender"; + const PROP_EXPERIENCE_LVL = "experienceLvl"; + const PROP_CHART_DURATION = "chartDuration"; + const PROP_MIN_CADENCE = "minCadence"; + const PROP_MAX_CADENCE = "maxCadence"; + var globalTimer; var activitySession; // Garmin activity recording session @@ -1991,6 +2053,11 @@ class GarminApp extends Application.AppBase { private var _sessionPausedTime = 0; private var _lastPauseTime = null; + // Activity metrics captured when monitoring stops + private var _sessionDuration = null; // milliseconds + private var _sessionDistance = null; // centimeters + private var _avgHeartRate = null; // bpm + private var _peakHeartRate = null; // bpm function initialize() { AppBase.initialize(); @@ -2002,6 +2069,9 @@ class GarminApp extends Application.AppBase { System.println("[INFO] App starting"); Logger.logMemoryStats("Startup"); + // Load saved settings from persistent storage + loadSettings(); + globalTimer = new Timer.Timer(); globalTimer.start(method(:updateCadenceBarAvg),1000,true); } @@ -2125,6 +2195,9 @@ class GarminApp extends Application.AppBase { _lastPauseTime = null; } + // Capture activity metrics before stopping + captureActivityMetrics(); + var cq = computeCadenceQualityScore(); if (cq >= 0) { @@ -2217,6 +2290,29 @@ class GarminApp extends Application.AppBase { } } + function captureActivityMetrics() as Void { + var info = Activity.getActivityInfo(); + + if (info != null) { + if (info.timerTime != null) { + _sessionDuration = info.timerTime; + System.println("[ACTIVITY] Duration: " + (_sessionDuration / 1000).toString() + " seconds"); + } + + if (info.elapsedDistance != null) { + _sessionDistance = info.elapsedDistance; + System.println("[ACTIVITY] Distance: " + (_sessionDistance / 100000.0).format("%.2f") + " km"); + } + + if (info.currentHeartRate != null) { + // For now, use current heart rate as average (could be enhanced with history tracking) + _avgHeartRate = info.currentHeartRate; + _peakHeartRate = info.currentHeartRate; + System.println("[ACTIVITY] Heart Rate: " + _avgHeartRate.toString() + " bpm"); + } + } + } + function updateCadenceBarAvg() as Void { // CRITICAL: Only collect data when RECORDING if (_sessionState != RECORDING) { @@ -2335,6 +2431,11 @@ class GarminApp extends Application.AppBase { _idealMaxCadence = finalCadence + 5; _idealMinCadence = finalCadence - 5; + + // Save the calculated cadence zones + saveSettings(); + + System.println("[CADENCE] Calculated ideal range: " + _idealMinCadence.toString() + "-" + _idealMaxCadence.toString() + " spm"); } function computeSmoothnessScore() as Number { @@ -2477,10 +2578,12 @@ class GarminApp extends Application.AppBase { function setMinCadence(value as Number) as Void { _idealMinCadence = value; + saveSettings(); } function setMaxCadence(value as Number) as Void { _idealMaxCadence = value; + saveSettings(); } function getCadenceHistory() as Array { @@ -2510,6 +2613,7 @@ class GarminApp extends Application.AppBase { function setUserGender(value as Number) as Void { _userGender = value; + saveSettings(); } function getUserLegLength() as Float { @@ -2518,6 +2622,7 @@ class GarminApp extends Application.AppBase { function setUserHeight(value as Number) as Void { _userHeight = value; + saveSettings(); } function getUserHeight() as Number { @@ -2530,6 +2635,7 @@ class GarminApp extends Application.AppBase { function setUserSpeed(value as Float) as Void { _userSpeed = value; + saveSettings(); } function getExperienceLvl() as Number { @@ -2538,6 +2644,7 @@ class GarminApp extends Application.AppBase { function setExperienceLvl(value as Float) as Void { _experienceLvl = value; + saveSettings(); } function min(a,b){ @@ -2582,103 +2689,98 @@ class GarminApp extends Application.AppBase { function getInitialView() as [Views] or [Views, InputDelegates] { return [ new SimpleView(), new SimpleViewDelegate() ]; } -} - -function getApp() as GarminApp { - return Application.getApp() as GarminApp; -} + // ----------------------- + // Summary Statistics Methods + // ----------------------- + function getAverageCadence() as Float { + if (_cadenceCount == 0) { + return 0.0; + } -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\Logger.mc ===== + var total = 0.0; + var validSamples = 0; -import Toybox.Lang; -import Toybox.System; + for (var i = 0; i < MAX_BARS; i++) { + var c = _cadenceHistory[i]; + if (c != null) { + total += c; + validSamples++; + } + } -/** - * Simple logger for memory monitoring only - */ -module Logger { - - /** - * Log memory statistics - */ - function logMemoryStats(tag as String) as Void { - try { - var stats = System.getSystemStats(); - var usedMemory = stats.totalMemory - stats.freeMemory; - var memoryPercent = (usedMemory.toFloat() / stats.totalMemory.toFloat() * 100).toNumber(); - - System.println("[MEMORY] " + tag + ": " + usedMemory + "/" + stats.totalMemory + - " bytes (" + memoryPercent + "% used)"); - } catch (e) { - System.println("[ERROR] Failed to log memory stats: " + e.getErrorMessage()); + if (validSamples == 0) { + return 0.0; } + + return total / validSamples; } -} + function getTimeInZonePercentage() as Number { + return computeTimeInZoneScore(); + } + function getMinCadenceFromHistory() as Number { + var minCad = null; -===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\source\SensorManager.mc ===== + for (var i = 0; i < MAX_BARS; i++) { + var c = _cadenceHistory[i]; + if (c != null) { + if (minCad == null || c < minCad) { + minCad = c; + } + } + } -using Toybox.Activity; -using Toybox.System; -using Toybox.Lang; + return (minCad != null) ? minCad.toNumber() : 0; + } -class SensorManager { + function getMaxCadenceFromHistory() as Number { + var maxCad = null; - // ----------------------- - // Static configuration - // ----------------------- + for (var i = 0; i < MAX_BARS; i++) { + var c = _cadenceHistory[i]; + if (c != null) { + if (maxCad == null || c > maxCad) { + maxCad = c; + } + } + } - // Use simulator mode by default - static var useSimulator = true; + return (maxCad != null) ? maxCad.toNumber() : 0; + } - // Simulated cadence value for testing - static var simulatedCadence = 0; + function hasValidSummaryData() as Boolean { + return _cadenceCount >= MIN_CQ_SAMPLES && _finalCQ != null; + } - // ----------------------- - // Public Methods - // ----------------------- + // Activity metrics getters + function getSessionDuration() { + return _sessionDuration; + } - // Set simulated cadence (for testing) - public static function setSimCadence(value) { - if (value == null || !(value instanceof Lang.Number)) { - System.println("[SensorManager] ERROR: simulated cadence must be a number"); - return; - } + function getSessionDistance() { + return _sessionDistance; + } - SensorManager.simulatedCadence = value; - System.println("[SensorManager] Simulated cadence set to: " + SensorManager.simulatedCadence.toString()); + function getAvgHeartRate() { + return _avgHeartRate; } - // Switch mode between simulator and real sensor - public static function setMode(simulator) { - // No strict type check needed - SensorManager.useSimulator = simulator ? true : false; - System.println("[SensorManager] Mode set to: " + (SensorManager.useSimulator ? "SIM" : "REAL")); + function getPeakHeartRate() { + return _peakHeartRate; } +} - // Get current cadence - public static function getCadence() { - var cadence = 0; +function getApp() as GarminApp { + return Application.getApp() as GarminApp; +} - if (SensorManager.useSimulator) { - cadence = SensorManager.simulatedCadence; - System.println("[SensorManager] Returning SIM cadence: " + cadence); - } else { - var info = Activity.getActivityInfo(); - if (info != null && info.currentCadence != null) { - cadence = info.currentCadence; - System.println("[SensorManager] Returning REAL cadence: " + cadence); - } else { - System.println("[SensorManager] REAL cadence unavailable, returning 0"); - } - } - return cadence; - } -} + +===== FILE: C:\Users\teapot\Documents\Projects\GitHub\garmin-smartwatch\combined-source.txt ===== + @@ -2691,7 +2793,7 @@ class SensorManager { Use "Monkey C: Edit Application" from the Visual Studio Code command palette to update the application attributes. --> - + >Main: "Warning: Branch is out-of-date" - - Note over You,Main: Thursday 2pm - Teammate->>Main: Merge feature/settings (main@110) - - Note over You,Main: Friday 11am - You->>Main: Attempt merge - Note over Main: Conflicts! Tests fail! Team sad! -``` - -### The Solution: (My) Daily Rebase Ritual - -**Run this every morning** (or after getting coffee): - -```bash / powershell -# Step 1: Fetch latest changes from GitHub -# (Downloads new commits without modifying your files) -git fetch origin - -# Step 2: Optional - See what changed on main -# (Shows commits that have been added since you branched) -git log --oneline --graph origin/main..HEAD - -# Step 3: Save any uncommitted work -# (Rebase requires a clean working directory) -git stash push -m "WIP: morning sync $(date +%Y-%m-%d)" - -# Step 4: Replay your commits on top of latest main -# (This is the key step - makes your branch current) -git rebase origin/main - -# Step 5: Handle conflicts (if any appear) -# Git will pause and show conflicting files -# Edit the files, then: -git add -git rebase --continue - -# If you get stuck or panic: -# git rebase --abort # Returns to pre-rebase state - -# Step 6: Restore your uncommitted work -git stash pop - -# Step 7: Update your remote branch -# (--force-with-lease is safer than --force) -git push --force-with-lease -``` - -### When to Sync Your Branch - -| Timing | Action | Command | -| --------------------- | ----------------------- | --------------------------------------------------------------------- | -| **Start work** | Branch from latest main | `git fetch && git checkout -b feature/name origin/main` | -| **Daily** (minimum) | Rebase on main | `git fetch && git rebase origin/main && git push --force-with-lease` | -| **Before opening PR** | Final rebase + cleanup | `git rebase -i origin/main` (squash "WIP" commits if needed) | -| **After PR approved** | Last sync before merge | `git fetch && git rebase origin/main` | -| **After PR merged** | Delete branch | `git push origin --delete feature/name && git branch -D feature/name` | - ---- - -## Common Situations & Solutions - -### Situation 1: GitHub Shows "This branch is out-of-date" - -**Don't click "Update branch" button** - it creates a merge commit! - -**Instead, use terminal:** - -```bash -git fetch origin -git rebase origin/main -git push --force-with-lease -``` - -Then refresh the PR page - warning disappears. - -### Situation 2: Someone Else Pushed to Your Branch - -If `--force-with-lease` fails with "rejected", someone else modified your branch: - -```bash -# Fetch their changes -git fetch origin - -# Rebase on your remote branch first (get their work) -git rebase origin/feature/your-branch - -# Then rebase on main -git rebase origin/main - -# Communicate with teammate before force-pushing! -git push --force-with-lease -``` - -### Situation 3: Rebase Goes Wrong - -**I have done this more times than i care to remember** - -If you make a mistake during rebase: - -```bash -# Abort and return to pre-rebase state -git rebase --abort - -# Check the reflog to see history -git reflog - -# Jump back to a specific commit if needed -git reset --hard HEAD@{5} # Numbers from reflog -``` - ---- - -## Branch Protection Rules - -**some of these are active on the bramch** - -Our repository enforces these rules on the `main` branch (Settings → Branches): - -| Setting | Value | Why | -| ------------------------------- | ------------ | -------------------------------------------------- | -| **Require pull request** | Enabled | Prevents accidental `git push origin main` | -| **Required approvals** | 1 reviewer | Ensures code review before merge | -| **Dismiss stale approvals** | Enabled | New commits after approval require re-review | -| **Require status checks** | CI must pass | Code must build and pass tests | -| **Require branches up to date** | **Critical** | PR must include latest `main` commits before merge | -| **Allow force pushes** | Disabled | Protects main's history | -| **Allow deletions** | Disabled | Prevents accidental branch deletion | - -**Why "Require branches up to date" is critical:** -This forces you to rebase before merging, ensuring the final merge result was actually tested in CI. Without this, two "green" PRs can be merged sequentially and break `main`. - ---- - -## Branch Naming Conventions - -\*\*this is my preffered naming convention - i try an incorparte naming conventions into everything, even university submissions, so instead of SIT782_5_4HDV3r5edit8 i have a meaning name. - -Use these prefixes to keep branches organized: - -| Prefix | Purpose | Example | -| ----------- | ------------------------------------- | --------------------------------- | -| `feature/` | New functionality | `feature/vibration-alerts` | -| `fix/` | Bug fixes | `fix/timer-crash` | -| `refactor/` | Code improvement (no behavior change) | `refactor/extract-chart-renderer` | -| `docs/` | Documentation only | `docs/api-examples` | -| `hotfix/` | Urgent production fix | `hotfix/memory-leak` | - -**Format:** `prefix/descriptive-name-in-kebab-case` - -**Examples:** - -- Good: `feature/haptic-feedback` -- Good: `fix/null-pointer-crash` -- Bad: `my-branch` (no prefix) -- Bad: `Feature/VibrationStuff` (wrong case) - ---- - -## Pull Request Best Practices - -### PR Size Guidelines - -| Lines Changed | Status | Recommendation | -| ------------------- | --------- | ---------------------------- | -| Less than 200 lines | Excellent | Ideal for fast review | -| 200-400 lines | Large | Consider splitting | -| 400+ lines | Too large | Must split into multiple PRs | - -**Why small PRs are better-ish:** - -- Faster reviews -- Lower chance of conflicts -- Easier to understand changes -- Less likely to introduce bugs - -### PR Template - -\*\*Does my head in when there is no comments or something generic like 'added stuff", admittedly its easier but its a crappy mindset because the person reviewing has no idea. - -When opening a PR, include: - -```markdown -## What - -Brief description of changes (1-2 sentences) - -## Why - -Problem being solved or feature being added - -## How - -Technical approach taken - -## Testing - -How to verify these changes work - -## Screenshots/Videos - -If UI changes, show before/after -- not always relevant -``` - -### Review Process - -1. **Open PR** when code is ready for review (not draft) -2. **Respond to feedback** within 24 hours -3. **Keep PR updated** - rebase when main moves forward -4. **Squash "fix review comments" commits** before final merge -5. **Delete branch** immediately after merge - ---- - -## Continuous Integration (CI) - -Every PR automatically runs: - -```mermaid -graph LR - A[Push to PR] --> B[Lint Check] - B --> C[Type Check] - C --> D[Unit Tests] - D --> E[Integration Tests] - E --> F[Build] - F --> G{All Pass?} - G -->|Yes| H[Ready to Merge] - G -->|No| I[Fix and Push Again] -``` - -**CI must pass before merge button activates.** - -If CI fails: - -1. Check the logs in the "Checks" tab -2. Fix the issue locally -3. Commit and push -4. CI runs automatically again - ---- - -## Merge Strategy - -We use **Squash and Merge** for all PRs: -**this is what i like, there is more than one way to skin this cat, find what works for your project.** - -**Benefits:** - -- Each PR becomes a single commit on `main` -- Clean, readable history -- Easy to revert entire features -- Encourages frequent commits during development - -**Process:** - -1. PR gets approved and CI passes -2. Click "Squash and merge" -3. Edit commit message (auto-generated from PR) -4. GitHub automatically deletes the branch - ---- - -## Alternative Workflows (For Reference ONly) - -While we use rebase-focused workflow, other valid approaches exist: - -### Git Flow - -- Uses `develop` branch as integration branch -- `main` only for releases -- More complex, better for teams with scheduled releases - -### Trunk-Based Development - -- Everyone commits to `main` frequently -- Heavy use of feature flags -- Requires strong CI/CD and testing discipline - -### GitHub Flow - -- Similar to our approach -- Allows merge commits instead of rebase -- Simpler but creates non-linear history - -**Why we chose rebase workflow:** -Balances simplicity (better than Git Flow) with code quality (cleaner than merge-heavy approaches). Ideal for learning teams transitioning from solo to collaborative development. - ---- - -## Quick Reference Guide - -**Starting a new feature:** - -```bash -git fetch origin -git checkout -b feature/my-feature origin/main -``` - -**Daily sync:** - -```bash -git fetch origin -git stash -git rebase origin/main -git stash pop -git push --force-with-lease -``` - -**Opening a PR:** - -1. Push branch: `git push -u origin feature/my-feature` -2. Open PR on GitHub -3. Request review -4. Respond to feedback - -**After PR merged:** - -```bash -git checkout main -git pull -git branch -D feature/my-feature -``` - ---- - -## Getting Help - -**Stuck during rebase?** - -1. Don't panic -- i cant stress this enough.. see below. -2. Run `git status` to see what's happening -3. Ask in team chat with: - - Output of `git status` - - What you were trying to do - - Current branch name -4. DONT Panic. Everyone screws up and makes mistakes. If you f*ck up, fix it, have an RCA and move on. I have bunged code and production systems over the years. F*ck up, fix it, move on. - **Common commands for troubleshooting:** - -```bash -git status # See current state -git log --oneline -10 # Recent commits -git reflog # History of HEAD movements -git diff # Uncommitted changes -``` - -**Remember:** Git is forgiving, and i cannot stress this enough - almost nothing is truly lost. We can always recover from mistakes with `reflog`. - ---- - -## Workflow Checklist - -Before starting work: - -- [ ] `git fetch origin` -- [ ] `git checkout -b feature/name origin/main` - -During development: - -- [ ] Commit frequently with clear messages -- [ ] Rebase on `main` daily -- [ ] Keep PR less than 400 lines if possible - -Before opening PR: - -- [ ] Final rebase: `git rebase -i origin/main` -- [ ] Squash WIP commits if needed -- [ ] Run tests locally -- [ ] Push: `git push -u origin feature/name` - -During review: - -- [ ] Respond to feedback within 24h -- [ ] Keep branch updated with `main` -- [ ] Address all review comments - -After merge: - -- [ ] Delete branch locally: `git branch -D feature/name` -- [ ] Pull latest main: `git checkout main && git pull` -- [ ] Celebrate! - ---- - -_This workflow is designed to minimize conflicts and maximize collaboration. When in doubt, communicate early with the team and sync your branch often!_ - ---- - -## Architecture Overview - -### Application Type - -- **Type**: Garmin Watch App (not data field or widget) -- **Target Devices**: Forerunner 165, Forerunner 165 Music -- **SDK Version**: Minimum API Level 5.2.0 -- **Architecture**: MVC (Model-View-Controller/Delegate pattern) - -### High-Level Structure - -``` -GarminApp (Application Core) - ├── Views - │ ├── SimpleView (Main activity view + haptic alerts) - │ └── AdvancedView (Chart visualization + haptic alerts) - ├── Delegates (Input handlers) - │ ├── SimpleViewDelegate (Main controls) - │ ├── AdvancedViewDelegate (Chart controls) - │ └── Settings Delegates (Configuration) - ├── Managers - │ ├── SensorManager (Cadence sensor) - │ └── Logger (Memory tracking) - └── Data Processing - ├── Cadence Quality Calculator - ├── Activity Recording Session - └── Haptic Alert Manager -``` - -### Component Interaction Flow - -```mermaid -graph TB - A[User] -->|Input| B[ViewDelegate] - B -->|Commands| C[GarminApp] - C -->|State Changes| D[View] - D -->|Display| A - - E[Cadence Sensor] -->|Data| C - C -->|Process| F[CQ Algorithm] - C -->|Monitor| G[Haptic Manager] - G -->|Vibrations| H[Watch Hardware] - - C -->|Records| I[ActivitySession] - I -->|Saves| J[FIT File] - J -->|Syncs| K[Garmin Connect] - - style G fill:#ff9999 - style H fill:#ff9999 -``` - ---- - -## Core Components - -### 1. GarminApp.mc - -**Purpose**: Central application controller and data manager - -**Key Responsibilities**: - -- Activity session lifecycle management (start/pause/resume/stop/save/discard) -- Cadence data collection and storage -- Cadence quality score computation -- State machine management -- Timer management -- Integration with Garmin Activity Recording API - -#### 1a. Memory Footprint (Cold Numbers) - -- Static allocation: ≈ 2.8 kB - - 280 cadence samples × 4 B = 1.1 kB - - 280 EMA smoothed values = 1.1 kB - - 10 CQ history = 40 B - - Misc buffers / state ≈ 600 B -- Peak stack during draw: ≈ 400 B -- Total at run-time: < 3.5 kB → fits easily into the 32 kB heap of the FR165 - -**Important Constants**: - -```monkey-c -MAX_BARS = 280 // Maximum cadence samples to store -BASELINE_AVG_CADENCE = 160 // Minimum acceptable cadence -MAX_CADENCE = 190 // Maximum cadence for calculations -MIN_CQ_SAMPLES = 30 // Minimum samples for CQ calculation -DEBUG_MODE = true // Enable debug logging -``` - -**State Variables**: - -- `_sessionState`: Current session state (IDLE/RECORDING/PAUSED/STOPPED) -- `activitySession`: Garmin ActivityRecording session object -- `_cadenceHistory`: Circular buffer storing 280 cadence samples -- `_cadenceBarAvg`: Rolling average buffer for chart display -- `_cqHistory`: Last 10 CQ scores for trend analysis - -### 2. SimpleView.mc & AdvancedView.mc - -**Purpose**: Display interfaces with integrated haptic feedback - -**SimpleView Responsibilities**: - -- Display current cadence, heart rate, distance, time -- Show cadence zone status (In Zone/Out Zone) -- Trigger haptic alerts when out of zone -- Update UI every second - -**AdvancedView Responsibilities**: - -- Render 28-minute cadence histogram -- Display heart rate and distance circles -- Show zone boundaries on chart -- Trigger haptic alerts when out of zone -- Color-code bars based on cadence zones - -**Haptic Alert Variables** (both views): - -```monkey-c -private var _lastZoneState = 0; // -1=below, 0=in zone, 1=above -private var _alertStartTime = null; // When alerts began -private var _alertDuration = 180000; // 3 minutes in milliseconds -private var _alertInterval = 30000; // 30 seconds between alerts -private var _lastAlertTime = 0; // Last alert timestamp -private var _pendingSecondVibe = false; // Double-buzz tracking -private var _secondVibeTime = 0; // When second buzz should fire -``` - ---- - -## Data Flow - -### 1. Cadence Data Collection Pipeline - -```mermaid -graph TD - A[Cadence Sensor] -->|Raw Data| B[Activity.getActivityInfo] - B -->|currentCadence| C[updateCadenceBarAvg] - C -->|Every 1s| D[_cadenceBarAvg Buffer] - D -->|Buffer Full| E[Calculate Average] - E --> F[updateCadenceHistory] - F --> G[_cadenceHistory 280 samples] - G --> H[computeCadenceQualityScore] - H --> I[_cqHistory Last 10 scores] - - G -->|Monitor| J[Zone Detection] - J -->|Out of Zone| K[Trigger Haptic Alert] - K --> L[User Feedback] - - style K fill:#ff9999 - style L fill:#ff9999 -``` - -### 2. Haptic Alert Data Flow - -```mermaid -sequenceDiagram - participant User - participant View - participant ZoneChecker - participant HapticManager - participant Hardware - - User->>View: Running (cadence changes) - View->>ZoneChecker: Check current cadence - - alt Cadence drops below minimum - ZoneChecker->>HapticManager: Trigger single buzz pattern - HapticManager->>Hardware: Vibrate 200ms - HapticManager->>HapticManager: Start 30s timer - Note over HapticManager: Repeat every 30s for 3 min - else Cadence exceeds maximum - ZoneChecker->>HapticManager: Trigger double buzz pattern - HapticManager->>Hardware: Vibrate 200ms - HapticManager->>Hardware: Wait 240ms - HapticManager->>Hardware: Vibrate 200ms - HapticManager->>HapticManager: Start 30s timer - Note over HapticManager: Repeat every 30s for 3 min - else Returns to zone - ZoneChecker->>HapticManager: Stop alerts - HapticManager->>HapticManager: Cancel timer - end -``` - -### 3. Timer System - -**Global Timer** (`globalTimer`): - -- Frequency: Every 1 second -- Callback: `updateCadenceBarAvg()` -- Runs: Always (from app start to stop) -- Purpose: Collect cadence data when recording - -**View Refresh Timers**: - -- SimpleView: Refresh every 1 second (reused for haptic checks) -- AdvancedView: Refresh every 1 second (reused for haptic checks) -- Purpose: Update UI elements and monitor zone status - -**Haptic Alert System** (NO dedicated timers): - -- Uses existing view refresh cycle -- Checks zone status on each UI update -- Triggers vibrations when appropriate -- No additional timer overhead - -### 4. Data Averaging System - -The app uses a two-tier averaging system: - -**Tier 1: Bar Averaging** - -``` -Chart Duration = 6 seconds (ThirtyminChart default) -↓ -Collect 6 cadence readings (1 per second) -↓ -Calculate average of these 6 readings -↓ -Store as single bar value -``` - -**Tier 2: Historical Storage** - -``` -280 bar values stored -↓ -Each bar = average of 6 seconds -↓ -Total history = 280 × 6 = 1680 seconds = 28 minutes -``` - -**Chart Duration Options**: - -- FifteenminChart = 3 seconds per bar -- ThirtyminChart = 6 seconds per bar (default) -- OneHourChart = 13 seconds per bar -- TwoHourChart = 26 seconds per bar - -### 4a. Sensor Manager Abstraction - -`SensorManager.mc` decouples real vs. simulated cadence: - -```monkey-c -useSimulator = true → returns hard-coded value (for desk testing) -useSimulator = false → reads Activity.getActivityInfo().currentCadence -``` - ---- - -## State Management - -### Session State Machine - -```mermaid -stateDiagram-v2 - [*] --> IDLE: App Start - IDLE --> RECORDING: startRecording() - RECORDING --> PAUSED: pauseRecording() - PAUSED --> RECORDING: resumeRecording() - RECORDING --> STOPPED: stopRecording() - PAUSED --> STOPPED: stopRecording() - STOPPED --> IDLE: saveSession() / discardSession() - - note right of RECORDING - - Timer active - - Data collecting - - Haptic alerts enabled - - UI updating - end note - - note right of PAUSED - - Timer stopped - - Data frozen - - Haptic alerts disabled - - UI static - end note - - note right of STOPPED - - Final CQ calculated - - Haptic alerts disabled - - Awaiting user decision - end note -``` - -### State Transition Rules - -**IDLE → RECORDING**: - -- User presses START/STOP button -- Creates new ActivityRecording session -- Starts Garmin timer -- Resets all cadence data arrays -- Initializes timestamps -- **Enables haptic zone monitoring** - -**RECORDING → PAUSED**: - -- User selects "Pause" from menu -- Stops Garmin timer (timer pauses) -- Records pause timestamp -- Data collection stops -- **Disables haptic alerts** - -**PAUSED → RECORDING**: - -- User selects "Resume" from menu -- Restarts Garmin timer -- Accumulates paused time -- Data collection resumes -- **Re-enables haptic zone monitoring** - -**RECORDING/PAUSED → STOPPED**: - -- User selects "Stop" from menu -- Stops Garmin timer -- Computes final CQ score -- Freezes all metrics -- **Disables haptic alerts** -- Awaits save/discard decision - -**STOPPED → IDLE**: - -- User selects "Save": Saves to FIT file -- User selects "Discard": Deletes session -- Resets all data structures -- Ready for new session - ---- - -## Activity Recording System - -### Garmin ActivityRecording Integration - -**Session Creation** (`startRecording()`): - -```monkey-c -activitySession = ActivityRecording.createSession({ - :name => "Running", - :sport => ActivityRecording.SPORT_RUNNING, - :subSport => ActivityRecording.SUB_SPORT_GENERIC -}); -activitySession.start(); -``` - -**What This Does**: - -- Creates official Garmin activity -- Starts timer (visible in UI) -- Records GPS, heart rate, cadence automatically -- Manages distance calculation -- Handles sensor data collection - -**Pause/Resume** (`pauseRecording()` / `resumeRecording()`): - -```monkey-c -// Pause -activitySession.stop(); // Pauses timer - -// Resume -activitySession.start(); // Resumes timer -``` - -**Save** (`saveSession()`): - -```monkey-c -activitySession.save(); -``` - -- Writes FIT file to device -- Syncs to Garmin Connect -- Appears in activity history -- Includes all sensor data - -**Discard** (`discardSession()`): - -```monkey-c -activitySession.discard(); -``` - -- Deletes session completely -- No FIT file created -- No sync to Garmin Connect - ---- - -## Haptic Feedback System - -### Overview - -The haptic feedback system provides real-time tactile alerts when the runner's cadence drifts outside their optimal zone. This helps maintain proper running form without constantly looking at the watch. - -### Design Philosophy - -**Timer-Free Architecture**: Instead of creating additional timers (which are limited on Garmin devices), the system piggybacks on the existing 1-second view refresh cycle. This approach: - -- Eliminates "Too Many Timers" errors -- Reduces memory overhead -- Maintains precise timing through timestamp tracking -- Seamlessly integrates with existing UI updates - -### Alert Patterns - -```mermaid -graph LR - A[Zone Detection] --> B{Cadence Status?} - B -->|Below Min| C[Single Buzz] - B -->|Above Max| D[Double Buzz] - B -->|In Zone| E[No Alert] - - C --> F[200ms vibration] - D --> G[200ms vibration] - G --> H[240ms pause] - H --> I[200ms vibration] - - F --> J[Repeat every 30s for 3 min] - I --> J - - style C fill:#9999ff - style D fill:#ff9999 - style E fill:#99ff99 -``` - -**Single Buzz** (Below Minimum Cadence): - -- Pattern: One 200ms vibration -- Meaning: Speed up your steps -- Repeat: Every 30 seconds -- Duration: 3 minutes max - -**Double Buzz** (Above Maximum Cadence): - -- Pattern: Two 200ms vibrations with 240ms gap -- Meaning: Slow down your steps -- Repeat: Every 30 seconds -- Duration: 3 minutes max - -**No Alert** (In Target Zone): - -- Pattern: Silence -- Meaning: Perfect cadence, keep going! - -### Implementation Details - -#### Zone State Tracking - -```monkey-c -private var _lastZoneState = 0; // -1 = below, 0 = in zone, 1 = above - -// Determine current zone -if (cadence < minZone) { - newZoneState = -1; // Below minimum -} else if (cadence > maxZone) { - newZoneState = 1; // Above maximum -} else { - newZoneState = 0; // In target zone -} -``` - -#### Alert Triggering Logic - -```monkey-c -if (newZoneState != _lastZoneState) { - if (newZoneState == -1) { - // Just dropped below minimum - triggerSingleVibration(); - startAlertCycle(); - } else if (newZoneState == 1) { - // Just exceeded maximum - triggerDoubleVibration(); - startAlertCycle(); - } else { - // Returned to zone - stopAlertCycle(); - } - _lastZoneState = newZoneState; -} -``` - -#### Alert Cycle Management - -```monkey-c -function startAlertCycle() as Void { - _alertStartTime = System.getTimer(); - _lastAlertTime = System.getTimer(); - // Initial alert already fired -} - -function checkAndTriggerAlerts() as Void { - if (_alertStartTime == null) { return; } - - var currentTime = System.getTimer(); - var elapsed = currentTime - _alertStartTime; - - // Stop after 3 minutes - if (elapsed >= 180000) { - _alertStartTime = null; - return; - } - - // Check if 30 seconds passed since last alert - var timeSinceLastAlert = currentTime - _lastAlertTime; - if (timeSinceLastAlert >= 30000) { - _lastAlertTime = currentTime; - - if (_lastZoneState == -1) { - triggerSingleVibration(); - } else if (_lastZoneState == 1) { - triggerDoubleVibration(); - } - } -} -``` - -#### Double Buzz Implementation - -```monkey-c -function triggerDoubleVibration() as Void { - if (Attention has :vibrate) { - // First vibration - var vibeData = [new Attention.VibeProfile(50, 200)]; - Attention.vibrate(vibeData); - - // Schedule second vibration - _pendingSecondVibe = true; - _secondVibeTime = System.getTimer() + 240; - } -} - -function checkPendingVibration() as Void { - if (_pendingSecondVibe) { - var currentTime = System.getTimer(); - if (currentTime >= _secondVibeTime) { - var vibeData = [new Attention.VibeProfile(50, 200)]; - Attention.vibrate(vibeData); - _pendingSecondVibe = false; - } - } -} -``` - -### Integration with Views - -Both `SimpleView` and `AdvancedView` include identical haptic feedback implementations: - -```mermaid -graph TD - A[onUpdate Called] --> B[displayCadence / drawElements] - B --> C[Get current cadence] - C --> D[Determine zone state] - D --> E{State changed?} - E -->|Yes| F[Trigger appropriate alert] - E -->|No| G[Check if alert needed] - F --> H[Start/stop alert cycle] - G --> H - H --> I[checkPendingVibration] - I --> J[Continue UI update] -``` - -**SimpleView Integration**: - -```monkey-c -function displayCadence() as Void { - // ... update UI elements ... - - // Determine zone state - var newZoneState = 0; - if (currentCadence < minZone) { - newZoneState = -1; - } else if (currentCadence > maxZone) { - newZoneState = 1; - } - - // Handle zone transitions - if (newZoneState != _lastZoneState) { - // Trigger appropriate alert and start cycle - } else { - // Check if periodic alert needed - checkAndTriggerAlerts(); - } -} -``` - -**AdvancedView Integration**: - -```monkey-c -function checkCadenceZone() as Void { - // Get activity info and determine zone - // Same logic as SimpleView - // Integrated with chart rendering -} -``` - -### Timing Accuracy - -The system achieves accurate 30-second intervals through timestamp comparison: - -``` -Initial Alert: T = 0s [BUZZ] -Check at: T = 1s (29s remaining - no alert) -Check at: T = 2s (28s remaining - no alert) -... -Check at: T = 30s (0s remaining - BUZZ!) -Check at: T = 31s (new cycle starts) -``` - -Actual timing variance: ±1 second (due to 1Hz refresh rate) - -### Memory Overhead - -**Additional Memory per View**: - -- State tracking: 3 integers (12 bytes) -- Timestamps: 3 longs (24 bytes) -- Boolean flags: 1 boolean (1 byte) -- **Total: ~40 bytes per view** - -**No Additional Timers Required**: - -- Reuses existing `_refreshTimer` (SimpleView) -- Reuses existing `_simulationTimer` (AdvancedView) -- Zero timer creation overhead - -### User Experience Flow - -```mermaid -sequenceDiagram - participant Runner - participant Watch - participant App - - Runner->>Watch: Start activity - Watch->>App: BEGIN RECORDING - - Note over App: Monitoring cadence... - - Runner->>Watch: Cadence drops to 115 SPM - App->>Watch: [SINGLE BUZZ] - Note over Watch: Below minimum alert - - Note over App: Wait 30 seconds... - - App->>Watch: [SINGLE BUZZ] - Note over Watch: Still below minimum - - Runner->>Watch: Increases cadence to 145 SPM - Note over App: Back in zone - stop alerts - - Runner->>Watch: Cadence spikes to 165 SPM - App->>Watch: [DOUBLE BUZZ] - Note over Watch: Above maximum alert - - Runner->>Watch: Reduces cadence to 150 SPM - Note over App: Back in zone - stop alerts -``` - -### Haptic Feedback Best Practices - -**For Developers**: - -1. Always check `Attention has :vibrate` before calling vibration -2. Reuse existing timers rather than creating new ones -3. Use timestamp-based tracking for precise intervals -4. Clean up state in `onHide()` to prevent orphaned alerts -5. Test thoroughly with rapid zone transitions - -### Future Enhancements - -Potential improvements to the haptic system: - -1. **Configurable Alert Patterns** - - User-selectable vibration duration - - Custom interval timing (15s, 45s, 60s) - - Triple-buzz for extreme deviations - -2. **Progressive Alert Intensity** - - Gentle buzz for minor deviations (±3 SPM) - - Strong buzz for major deviations (±10 SPM) - - Requires multi-pattern vibration support - -3. **Smart Alert Suppression** - - Disable during warm-up (first 5 minutes) - - Pause alerts on steep hills (using GPS grade) - - Adaptive zones based on fatigue detection - -4. **Audio Cues** (device-dependent) - - Combine vibration with tones - - Voice feedback for major transitions - - Requires audio hardware support - ---- - -## Cadence Quality Algorithm - -### Overview - -The Cadence Quality (CQ) score is a composite metric that measures running efficiency based on two factors: - -1. **Time in Zone** (70% weight): Percentage of time spent within ideal cadence range -2. **Smoothness** (30% weight): Consistency of cadence over time - -### Algorithm Flow - -```mermaid -graph TD - A[Collect Cadence Sample] --> B{Min 30 samples?} - B -->|No| C[Display: CQ --] - B -->|Yes| D[Calculate Time in Zone] - D --> E[Calculate Smoothness] - E --> F[Compute Weighted Average] - F --> G[CQ Score 0-100%] - G --> H[Store in History] - H --> I[Update Display] -``` - -### Time in Zone Calculation - -**Purpose**: Measures what percentage of your running time is spent at the optimal cadence - -**Formula**: - -``` -Time in Zone % = (samples in zone / total samples) × 100 -``` - -**Implementation**: - -```monkey-c -function computeTimeInZoneScore() as Number { - if (_cadenceCount < MIN_CQ_SAMPLES) { - return -1; // Not enough data yet - } - - 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(); -} -``` - -**Example**: - -- 280 total samples collected -- 210 samples within zone [145-155 SPM] -- Time in Zone = (210/280) × 100 = 75% - -### Smoothness Calculation - -**Purpose**: Measures cadence consistency (low variance = better form) - -**Formula**: - -``` -Average Difference = Σ |current - previous| / number of transitions -Smoothness % = 100 - (average difference × 10) -``` - -**Implementation**: - -```monkey-c -function computeSmoothnessScore() as Number { - if (_cadenceCount < MIN_CQ_SAMPLES) { - return -1; - } - - 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++; - } - } - - if (diffCount == 0) { - return -1; - } - - var avgDiff = totalDiff / diffCount; - var rawScore = 100 - (avgDiff * 10); - - // Clamp to 0-100 range - if (rawScore < 0) { rawScore = 0; } - if (rawScore > 100) { rawScore = 100; } - - return rawScore; -} -``` - -**Example**: - -- Sample transitions: 145→148 (3), 148→147 (1), 147→150 (3) -- Average difference = (3+1+3)/3 = 2.33 -- Smoothness = 100 - (2.33 × 10) = 76.7% - -### Final CQ Score - -**Weighted Combination**: - -``` -CQ = (Time in Zone × 0.7) + (Smoothness × 0.3) -``` - -**Implementation**: - -```monkey-c -function computeCadenceQualityScore() as Number { - var timeInZone = computeTimeInZoneScore(); - var smoothness = computeSmoothnessScore(); - - if (timeInZone < 0 || smoothness < 0) { - return -1; // Not enough data - } - - var cq = (timeInZone * 0.7) + (smoothness * 0.3); - return cq.toNumber(); -} -``` - -**Example**: - -- Time in Zone = 75% -- Smoothness = 76.7% -- CQ = (75 × 0.7) + (76.7 × 0.3) = 52.5 + 23.01 = **75.5%** - -### CQ Score Interpretation - -| Score Range | Rating | Interpretation | -| ----------- | --------- | -------------------------- | -| 90-100% | Excellent | Elite running form | -| 80-89% | Very Good | Consistent optimal cadence | -| 70-79% | Good | Generally on target | -| 60-69% | Fair | Room for improvement | -| 50-59% | Poor | Frequent zone violations | -| 0-49% | Very Poor | Needs significant work | - -### Confidence Calculation - -**Purpose**: Indicates reliability of CQ score based on missing data - -```monkey-c -function computeCQConfidence() as String { - if (_cadenceCount < MIN_CQ_SAMPLES) { - return "Low"; - } - - var missingRatio = _missingCadenceCount.toFloat() / - (_cadenceCount + _missingCadenceCount).toFloat(); - - if (missingRatio > 0.2) { - return "Low"; // >20% missing data - } else if (missingRatio > 0.1) { - return "Medium"; // 10-20% missing - } else { - return "High"; // <10% missing - } -} -``` - -### Trend Analysis - -**Purpose**: Shows if cadence quality is improving, stable, or declining - -```monkey-c -function computeCQTrend() as String { - if (_cqHistory.size() < 5) { - return "Insufficient data"; - } - - // Compare recent half vs. older half - var midpoint = _cqHistory.size() / 2; - var olderAvg = 0.0; - var recentAvg = 0.0; - - for (var i = 0; i < midpoint; i++) { - olderAvg += _cqHistory[i]; - } - olderAvg /= midpoint; - - for (var i = midpoint; i < _cqHistory.size(); i++) { - recentAvg += _cqHistory[i]; - } - recentAvg /= (_cqHistory.size() - midpoint); - - var diff = recentAvg - olderAvg; - - if (diff > 5) { - return "Improving"; - } else if (diff < -5) { - return "Declining"; - } else { - return "Stable"; - } -} -``` - -### CQ Storage in FIT File - -When the activity is stopped, the final CQ score is frozen and written to the FIT file: - -```monkey-c -function stopRecording() as Void { - // ... stop activity session ... - - 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(); - } - - _sessionState = STOPPED; -} -``` - -This frozen CQ score: - -- Appears in the activity summary -- Syncs to Garmin Connect -- Provides historical tracking -- Can be compared across runs - ---- - -## User Interface - -### SimpleView (Main Display) - -**Layout**: - -``` - ┌─────────────────────┐ - │ [REC] 00:12:34 │ ← Time + Recording Indicator - ├─────────────────────┤ - │ ❤ │ │ ⚡ │ - │ 152 │ 148 │ 180 │ ← Heart Rate, Cadence, Steps - ├─────────────────────┤ - │ In Zone (145-155) │ ← Zone Status - ├─────────────────────┤ - │ 2.45 km │ ← Distance - ├─────────────────────┤ - │ CQ: 75% │ ← Cadence Quality - └─────────────────────┘ -``` - -**Color Coding**: - -- **Green**: Cadence in optimal zone -- **Blue**: Slightly below zone (within threshold) -- **Grey**: Well below zone -- **Orange**: Slightly above zone (within threshold) -- **Red**: Well above zone - -**Haptic Feedback Integration**: - -- Single buzz when cadence drops below minimum -- Double buzz when cadence exceeds maximum -- Repeats every 30 seconds if still out of zone -- Automatically stops after 3 minutes or when returning to zone - -### AdvancedView (Chart Display) - -**Layout**: - -``` - ┌─────────────────────┐ - │ 1:23:45 │ ← Session Time - ├──────┬─────────┬────┤ - │ ❤ │ │ 🏃 │ - │ 152 │ │2.4 │ ← HR Circle + Distance Circle - ├──────┴─────────┴────┤ - │ 148 spm │ ← Current Cadence - ├─────────────────────┤ - │ ▂▅▇█▆▅▃▂▃▄▅▆▇█▆▄▃ │ ← 28-min Histogram - │ │ - ├─────────────────────┤ - │ Zone: 145-155 spm │ ← Zone Range - └─────────────────────┘ -``` - -**Chart Features**: - -- 280 bars representing 28 minutes of data -- Fixed vertical scale (0-200 SPM) -- Color-coded bars matching zone status -- Real-time updates every second -- Smooth scrolling as new data arrives - -**Haptic Feedback Integration**: - -- Same alert patterns as SimpleView -- Integrated with chart updates -- Visual + tactile feedback for optimal learning - -### Navigation - -```mermaid -graph TD - A[SimpleView] -->|Swipe Up / Press Down| B[AdvancedView] - B -->|Swipe Down / Press Up| A - A -->|Swipe Left / Press Up| C[Settings] - B -->|Swipe Left| C - C -->|Back| A - A -->|Press Select| D{Activity State?} - D -->|IDLE| E[Start Recording] - D -->|RECORDING| F[Activity Menu] - D -->|PAUSED| G[Paused Menu] - D -->|STOPPED| H[Save/Discard Menu] -``` - -**Button Mapping**: - -- **SELECT**: Start/Stop activity or open control menu -- **UP**: Navigate to settings or previous view -- **DOWN**: Navigate to next view -- **BACK**: Exit menus (disabled during active session) -- **MENU**: Open cadence zone settings - -### Activity Control Menus - -**During Recording**: - -``` -┌──────────────────────┐ -│ Activity │ -├──────────────────────┤ -│ > Resume │ -│ > Pause │ -│ > Stop │ -└──────────────────────┘ -``` - -**When Paused**: - -``` -┌──────────────────────┐ -│ Activity Paused │ -├──────────────────────┤ -│ > Resume │ -│ > Stop │ -└──────────────────────┘ -``` - -**After Stopping**: - -``` -┌──────────────────────┐ -│ Save Activity? │ -├──────────────────────┤ -│ > Save │ -│ > Discard │ -└──────────────────────┘ -``` - -### Settings Menu - -``` -┌──────────────────────┐ -│ Settings │ -├──────────────────────┤ -│ > Profile │ -│ > Customization │ -│ > Feedback │ -│ > Cadence Range │ -└──────────────────────┘ -``` - -**Profile Settings**: - -- Height (cm) -- Speed (km/h) -- Gender (Male/Female/Other) -- Experience Level (Beginner/Intermediate/Advanced) - -**Customization**: - -- Chart Duration (15min/30min/1hr/2hr) - -**Feedback** (Future): - -- Haptic intensity -- Alert interval -- Alert duration - -**Cadence Range**: - -- Set Min Cadence (manual adjustment) -- Set Max Cadence (manual adjustment) - ---- - -## Settings System - -### User Profile Configuration - -**Purpose**: Calculate personalized ideal cadence based on biomechanics - -**Formula** (from research): - -``` -Reference Cadence = (-1.251 × leg_length) + (3.665 × speed_m/s) + 254.858 -Final Cadence = Reference × Experience_Factor -``` - -**Gender-Specific Adjustments**: - -```monkey-c -function idealCadenceCalculator() as Void { - var referenceCadence = 0; - var userLegLength = _userHeight * 0.53; // 53% of height - var userSpeedms = _userSpeed / 3.6; // Convert km/h to m/s - - switch (_userGender) { - case Male: - referenceCadence = (-1.268 × userLegLength) + - (3.471 × userSpeedms) + 261.378; - break; - case Female: - referenceCadence = (-1.190 × userLegLength) + - (3.705 × userSpeedms) + 249.688; - break; - default: - referenceCadence = (-1.251 × userLegLength) + - (3.665 × userSpeedms) + 254.858; - break; - } - - referenceCadence *= _experienceLvl; - referenceCadence = Math.round(referenceCadence); - - var finalCadence = max(BASELINE_AVG_CADENCE, - min(referenceCadence, MAX_CADENCE)); - - _idealMaxCadence = finalCadence + 5; - _idealMinCadence = finalCadence - 5; -} -``` - -**Experience Level Multipliers**: - -- Beginner: 1.06 (higher cadence for learning) -- Intermediate: 1.04 (moderate adjustment) -- Advanced: 1.02 (minimal adjustment) - -### Persistent Storage - -**Storage Keys**: - -```monkey-c -const PROP_USER_HEIGHT = "user_height"; -const PROP_USER_SPEED = "user_speed"; -const PROP_USER_GENDER = "user_gender"; -const PROP_EXPERIENCE_LVL = "experience_level"; -const PROP_CHART_DURATION = "chart_duration"; -const PROP_MIN_CADENCE = "min_cadence"; -const PROP_MAX_CADENCE = "max_cadence"; -``` - -**Save Settings**: - -```monkey-c -function saveSettings() as Void { - Storage.setValue(PROP_USER_HEIGHT, _userHeight); - Storage.setValue(PROP_USER_SPEED, _userSpeed); - Storage.setValue(PROP_USER_GENDER, _userGender); - Storage.setValue(PROP_EXPERIENCE_LVL, _experienceLvl); - Storage.setValue(PROP_CHART_DURATION, _chartDuration); - Storage.setValue(PROP_MIN_CADENCE, _idealMinCadence); - Storage.setValue(PROP_MAX_CADENCE, _idealMaxCadence); -} -``` - -**Load Settings**: - -```monkey-c -function loadSettings() as Void { - var height = Storage.getValue(PROP_USER_HEIGHT); - if (height != null) { - _userHeight = height as Number; - } - // ... load other settings ... -} -``` - -Settings are automatically: - -- Loaded on app start -- Saved when modified -- Persisted between sessions -- Restored after watch reboot - ---- - -## Documentation Reference - ---- - -This reference covers the formatting used throughout this documentation. Use it when contributing updates or creating new documentation. markdown can be -intimidating at first, but onve you master it, you will use it for everything. - ---- - -## Markdown Basics - -### Headers - -```markdown -# H1 - Main Title - -## H2 - Major Section - -### H3 - Subsection - -#### H4 - Minor Heading -``` - -**Usage in this doc:** - -- H1: Document title only -- H2: Major sections (Architecture, Core Components, etc.) -- H3: Subsections within major sections -- H4: Rarely used, for very specific details - ---- - -### Text Formatting - -```markdown -**Bold text** for emphasis -_Italic text_ for subtle emphasis -`Inline code` for commands, variables, filenames -~~Strikethrough~~ for deprecated content -``` - -**Examples:** - -- **Bold**: Important terms, warnings -- _Italic_: Notes, asides -- `Code`: `git push`, `_cadenceHistory`, `SimpleView.mc` - ---- - -### Links - -```markdown -[Link text](https://example.com) -[Internal link](#section-name) -[Link with title](https://example.com "Hover text") -``` - -**Internal link rules:** - -- Section names become anchors automatically -- Convert to lowercase -- Replace spaces with hyphens -- Remove special characters or convert to hyphens - -**Examples:** - -```markdown -[Architecture Overview](#architecture-overview) # Correct -[GitHub Workflow & Collaboration](#github-workflow--collaboration) # & becomes -- -[State Management](#state-management) # Simple case -``` - ---- - -### Lists - -**Unordered lists:** - -```markdown -- Item 1 -- Item 2 - - Nested item 2a - - Nested item 2b -- Item 3 -``` - -**Ordered lists:** - -```markdown -1. First item -2. Second item -3. Third item -``` - -**Checklists:** - -```markdown -- [ ] Incomplete task -- [x] Completed task -``` - ---- - -### Code Blocks - -**Inline code:** - -```markdown -Use `git status` to check your working directory. -``` - -**Fenced code blocks with syntax highlighting:** - -````markdown -```bash -git fetch origin -git rebase origin/main -``` - -```monkey-c -function initialize() { - View.initialize(); -} -``` - -```javascript -const response = await fetch(url); -``` -```` - -**Supported languages in this doc:** - -- `bash` - Shell commands -- `monkey-c` - Monkey C code -- `javascript` - JS examples -- `yaml` - GitHub Actions workflows -- `markdown` - Markdown examples -- No language tag - Plain text - ---- - -### Tables - -**Basic table:** - -```markdown -| Column 1 | Column 2 | Column 3 | -| -------- | -------- | -------- | -| Data 1 | Data 2 | Data 3 | -| Data 4 | Data 5 | Data 6 | -``` - -**Table with alignment:** - -```markdown -| Left-aligned | Center-aligned | Right-aligned | -| :----------- | :------------: | ------------: | -| Left | Center | Right | -``` - -**Tips:** - -- Use `|:---` for left align (default) -- Use `|:---:|` for center align -- Use `|---:|` for right align -- Don't worry about perfect spacing - Markdown handles it - ---- - -### Blockquotes - -```markdown -> This is a blockquote -> It can span multiple lines -> -> And include multiple paragraphs -``` - -**Usage:** Notes, warnings, important callouts - ---- - -### Horizontal Rules - -```markdown ---- -``` - -**Usage:** Separate major sections (used throughout this doc) - ---- - -## Mermaid Diagrams - -Mermaid creates diagrams from text. All diagrams must be in fenced code blocks with `mermaid` language tag. - -### Flowcharts (Graph) - -**Basic syntax:** - -````markdown -```mermaid -graph TD - A[Start] --> B[Process] - B --> C{Decision?} - C -->|Yes| D[Action 1] - C -->|No| E[Action 2] - D --> F[End] - E --> F -``` -```` - -**Node shapes:** - -```markdown -A[Rectangle] # Square corners -B(Rounded) # Rounded corners -C([Stadium]) # Pill shape -D[[Subroutine]] # Double border -E[(Database)] # Cylinder -F((Circle)) # Circle -G>Flag] # Flag shape -H{Diamond} # Diamond (decision) -I{{Hexagon}} # Hexagon -``` - -**Arrow types:** - -```markdown -A --> B # Solid arrow -A -.-> B # Dotted arrow -A ==> B # Thick arrow -A --- B # Line (no arrow) -A -- Text --> B # Labeled arrow -A -->|Text| B # Labeled arrow (compact) -``` - -**Direction:** - -```markdown -graph TD # Top to Down -graph LR # Left to Right -graph BT # Bottom to Top -graph RL # Right to Left -``` - -**Example from this doc (Data Flow):** - -````markdown -```mermaid -graph TD - A[Cadence Sensor] -->|Raw Data| B[Activity.getActivityInfo] - B -->|currentCadence| C[updateCadenceBarAvg] - C -->|Every 1s| D[_cadenceBarAvg Buffer] - D -->|Buffer Full| E[Calculate Average] - E --> F[updateCadenceHistory] -``` -```` - ---- - -### Sequence Diagrams - -**Basic syntax:** - -````markdown -```mermaid -sequenceDiagram - participant A as Alice - participant B as Bob - - A->>B: Hello Bob! - B->>A: Hi Alice! - - Note over A,B: This is a note - Note right of A: Note on right - Note left of B: Note on left -``` -```` - -**Arrow types:** - -```markdown -A->>B: Solid arrow (message) -A-->>B: Dotted arrow (return) -A-xB: Cross (lost message) -``` - -**Special syntax:** - -```markdown -alt Alternative 1 -A->>B: Do this -else Alternative 2 -A->>B: Do that -end - -loop Every 30s -A->>B: Repeat this -end -``` - -**Example from this doc (Haptic Alerts):** - -````markdown -```mermaid -sequenceDiagram - participant User - participant View - participant ZoneChecker - participant HapticManager - - User->>View: Running (cadence changes) - View->>ZoneChecker: Check current cadence - - alt Cadence drops below minimum - ZoneChecker->>HapticManager: Trigger single buzz - else Cadence exceeds maximum - ZoneChecker->>HapticManager: Trigger double buzz - else Returns to zone - ZoneChecker->>HapticManager: Stop alerts - end -``` -```` - ---- - -### State Diagrams - -**Basic syntax:** - -````markdown -```mermaid -stateDiagram-v2 - [*] --> State1 - State1 --> State2: Transition - State2 --> State3: Another transition - State3 --> [*] - - note right of State1 - This is a note - end note -``` -```` - -**Example from this doc (Session States):** - -````markdown -```mermaid -stateDiagram-v2 - [*] --> IDLE: App Start - IDLE --> RECORDING: startRecording() - RECORDING --> PAUSED: pauseRecording() - PAUSED --> RECORDING: resumeRecording() - RECORDING --> STOPPED: stopRecording() - STOPPED --> IDLE: saveSession() / discardSession() - - note right of RECORDING - Timer active - Data collecting - Haptic alerts enabled - end note -``` -```` - ---- - -## Documentation Best Practices - -### When to Use Which Diagram - -| Diagram Type | Use When | Example in This Doc | -| -------------------- | --------------------------------------------------- | -------------------------------------------- | -| **Flowchart** | Showing process flow, data pipeline, decision trees | Data collection pipeline, CI workflow | -| **Sequence Diagram** | Showing interactions over time between components | Haptic alert timing, merge conflict scenario | -| **State Diagram** | Showing state transitions and lifecycle | Session state machine | - -### Formatting Guidelines - -**DO:** - -- Use consistent header levels (don't skip levels) -- Include code language tags in fenced blocks -- Use tables for structured data comparisons -- Add horizontal rules between major sections -- Keep lines under 120 characters when possible -- Use relative links for internal references - -**DON'T:** - -- Use HTML unless absolutely necessary -- Skip header levels (H2 → H4 without H3) -- Use images for text content (accessibility) -- Hard-code line breaks (let Markdown handle wrapping) -- Use bare URLs (always use link syntax) - -### Code Block Guidelines - -**For commands:** - -```bash -# Good: Show full command with context -git fetch origin -git rebase origin/main -git push --force-with-lease - -# Bad: No context, unclear -git push -f -``` - -**For code:** - -```monkey-c -// Good: Include relevant context and comments -function startRecording() as Void { - // Create Garmin activity session - activitySession = ActivityRecording.createSession({ - :name => "Running", - :sport => ActivityRecording.SPORT_RUNNING - }); -} - -// Bad: No context or explanation -activitySession.start(); -``` - -### Table Guidelines - -**Comparison tables:** - -- Left column: Item being compared -- Other columns: Attributes or options -- Use bold for headers - -**Decision tables:** - -- Left column: Condition/situation -- Right columns: Action or outcome -- Consider using "When/Action/Command" structure - -**Examples:** - -```markdown -| When | Action | Command | -| ---------- | ----------------------- | ------------------------------ | -| Start work | Branch from latest main | `git checkout -b feature/name` | -``` - ---- - -## Mermaid Troubleshooting - -### Common Issues - -**Diagram not rendering?** - -1. Check for typos in `mermaid` tag -2. Ensure proper indentation -3. Close all parentheses and brackets -4. Check for special characters in node names - -**Arrows not connecting?** - -- Make sure node IDs match exactly (case-sensitive) -- Check arrow syntax (`-->` not `->`) - -**Text not showing?** - -- Wrap text with spaces in quotes: `A["My Text"]` -- Use `|Text|` for inline labels on arrows - -### Testing Diagrams - -**Before committing:** - -1. View in GitHub's preview tab -2. Or use online editor: https://mermaid.live -3. Check that text is readable -4. Verify all arrows point correctly - ---- - -## Contributing to Documentation - -### Before You Edit - -1. Read through existing docs to understand style -2. Check this reference for syntax -3. Test Mermaid diagrams in preview - -### Making Changes - -1. Create branch: `git checkout -b docs/your-update` -2. Edit markdown files -3. Preview changes locally or on GitHub -4. Commit with clear message: "docs: update haptic feedback section" -5. Open PR with description of changes - -### Review Checklist - -- [ ] Headers follow hierarchy (no skipped levels) -- [ ] Links work (test internal anchors) -- [ ] Code blocks have language tags -- [ ] Tables align properly -- [ ] Mermaid diagrams render correctly -- [ ] No spelling errors in headers or links -- [ ] Follows existing document style - ---- - -## Quick Reference: Common Patterns - -### Section Header Pattern - -```markdown ---- - -## Section Name - -Brief introduction paragraph explaining what this section covers. - -### Subsection - -Content here... - -**Key Points:** - -- Point 1 -- Point 2 -- Point 3 -``` - -### Code Example Pattern - -````markdown -**Implementation:** - -```monkey-c -function example() as Void { - // Explanation comment - var result = doSomething(); -} -``` -```` - -**What this does:** - -- Explains the code -- Provides context - -```` - -### Comparison Table Pattern -```markdown -| Feature | Option A | Option B | -|---------|----------|----------| -| Speed | Fast | Slow | -| Memory | High | Low | -| Complexity | Simple | Complex | -```` - ---- - ---- - -## Features Reference - -### Current Features (v1.0) - -✅ **Core Functionality** - -- Real-time cadence monitoring -- 28-minute rolling histogram -- Cadence Quality (CQ) scoring -- Activity recording to FIT file -- Pause/Resume functionality -- Save/Discard workflow - -✅ **User Interface** - -- SimpleView (main display) -- AdvancedView (chart visualization) -- Settings menus -- Activity control menus -- Recording indicator - -✅ **Smart Features** - -- Personalized cadence zones -- Gender-specific calculations -- Experience level adjustment -- Color-coded zone feedback -- CQ trend analysis -- **Haptic zone alerts** - -✅ **Data Management** - -- Circular buffer storage -- Two-tier averaging system -- Persistent settings -- FIT file integration -- Memory optimization - -### Haptic Feedback Feature (v1.1) - -✅ **Alert Patterns** - -- Single buzz for below-zone cadence -- Double buzz for above-zone cadence -- 30-second repeat interval -- 3-minute maximum duration -- Automatic stop on zone re-entry - -✅ **Technical Implementation** - -- Timer-free architecture -- Timestamp-based tracking -- Integrated with view refresh cycle -- No additional memory overhead -- Works on both SimpleView and AdvancedView - -✅ **User Benefits** - -- No need to constantly watch screen -- Tactile feedback during runs -- Non-intrusive alerts -- Customizable zone ranges -- Improves form awareness - -### Future Enhancements - -#### 🔴 High Priority - -1. **Configurable Alert Settings** - - Alert interval (15s/30s/45s/60s) - - Alert duration (1min/3min/5min/continuous) - - Vibration intensity (light/medium/strong) - -2. **Battery Optimization** - - Adaptive refresh rate based on battery level - - Low-power mode during steady-state running - - Smart sensor polling - -3. **Chart Rendering Optimization** - - Reduce draw calls - - Cache static elements - - Optimize bar calculations - -#### 🟡 Medium Priority - -4. **Smooth Bars** - - Gradient transitions between zones - - Anti-aliased rendering - - Sub-pixel accuracy - -5. **Zone Boundary Lines** - - Visual indicators on chart - - Min/max cadence markers - - Target zone highlighting - -6. **Statistical Overlays** - - Average line - - Standard deviation bands - - Trend line - -7. **Terrain-Adaptive Zones** - - Adjust zones for hills (using GPS elevation) - - Compensate for terrain difficulty - - Smart zone boundaries - -#### 🟢 Low Priority - -8. **Fade Old Bars** - - Opacity gradient for time perspective - - Highlight recent data - - Visual age indication - -9. **Auto-Adjust Chart Duration** - - Extend duration for long runs - - Compress for short workouts - - Dynamic time window - -10. **CSV Export** - - Export cadence history - - Include all metrics - - Bluetooth transfer to phone - -11. **Dynamic Memory Management** - - Adapt buffer sizes to available memory - - Graceful degradation on low memory - - Device-specific optimization - -12. **Night Mode** - - Auto-detect sunrise/sunset - - Red/orange color palette - - Preserve dark adaptation - -13. **Progressive Alert Intensity** - - Gentle buzz for minor deviations - - Strong buzz for major deviations - - Gradient feedback system - ---- - -## Implementation Priority Matrix - -### Phase 1: Core Stability (Completed) - -- ✅ State machine -- ✅ Activity recording -- ✅ Pause/Resume -- ✅ Save/Discard -- ✅ Basic haptic alerts - -### Phase 2: User Customization (Current) - -- 🔄 Configurable alert settings -- 🔄 Battery optimization -- 🔄 Chart rendering optimization - -### Phase 3: Advanced Features (Future) - -- 📋 Smooth bars -- 📋 Zone boundary lines -- 📋 Statistical overlays -- 📋 Terrain-adaptive zones - -### Phase 4: Polish & Enhancement (Future) - -- 📋 Fade old bars -- 📋 Auto-adjust chart duration -- 📋 CSV export -- 📋 Dynamic memory management -- 📋 Night mode -- 📋 Progressive alert intensity - ---- - -## Technical Debt & Code Quality - -### Refactoring Needed - -- [ ] Extract chart rendering to `ChartRenderer.mc` class -- [ ] Create `CircularBuffer.mc` reusable class -- [ ] Consolidate color constants into `Colors.mc` -- [ ] Create `HapticManager.mc` for centralized vibration control -- [ ] Add input validation layer for all settings -- [ ] Document all public methods with JSDoc-style comments - -### Testing & Quality - -- [ ] Add unit tests for CQ algorithm -- [ ] Add integration tests for state machine -- [ ] Add haptic feedback timing tests -- [ ] Profile memory usage during 2+ hour activities -- [ ] Benchmark chart rendering on FR165 vs FR165 Music -- [ ] Test sensor disconnection recovery -- [ ] Test haptic alerts across rapid zone transitions - -### Performance Profiling Targets - -- [ ] Chart draw time: <50ms per frame -- [ ] Memory usage: <5% of total device memory -- [ ] Battery drain: <5% per hour (GPS active) -- [ ] Haptic timing accuracy: ±1 second - ---- - -## Debugging Guide - -### Common Issues - -**Issue**: Haptic alerts not firing -**Cause**: Attention module not supported or state not RECORDING -**Solution**: - -- Check `Attention has :vibrate` capability -- Verify `_sessionState == RECORDING` -- Confirm cadence is actually out of zone - -**Issue**: Alerts continue after returning to zone -**Cause**: Zone state not properly updated -**Solution**: - -- Check `_lastZoneState` variable -- Verify zone detection logic -- Ensure `stopAlertCycle()` is called - -**Issue**: Double buzz only fires once -**Cause**: `_pendingSecondVibe` not being checked -**Solution**: - -- Confirm `checkPendingVibration()` called in `onUpdate()` -- Verify `_secondVibeTime` calculation -- Check timer precision - -**Issue**: Timer not pausing -**Cause**: ActivityRecording session not properly controlled -**Solution**: Check `activitySession.stop()` is called on pause - -**Issue**: Cadence data not collecting -**Cause**: State not RECORDING or sensor not connected -**Solution**: Verify `_sessionState == RECORDING` and sensor paired - -**Issue**: CQ always shows "--" -**Cause**: Less than MIN_CQ_SAMPLES (30) collected -**Solution**: Wait 30 seconds after starting, check sensor connection - -**Issue**: Chart not updating -**Cause**: View timer not running or data not flowing -**Solution**: Check `_simulationTimer` started in `onShow()` - -### Debug Checklist - -1. ✓ `DEBUG_MODE = true` in GarminApp.mc -2. ✓ Watch console for `[INFO]`, `[DEBUG]`, `[CADENCE]` messages -3. ✓ Verify state transitions match expected flow -4. ✓ Check `_cadenceCount` increments when recording -5. ✓ Confirm `activitySession != null` when active -6. ✓ Validate sensor pairing in Garmin Connect app -7. ✓ Monitor `_lastZoneState` for zone transitions -8. ✓ Verify haptic timing with stopwatch -9. ✓ Check `_alertStartTime` and `_lastAlertTime` values - -### Haptic Debugging - -**Enable Haptic Debug Logging**: - -```monkey-c -// In triggerSingleVibration() -System.println("[HAPTIC] Single buzz triggered at " + System.getTimer()); - -// In triggerDoubleVibration() -System.println("[HAPTIC] Double buzz triggered at " + System.getTimer()); - -// In checkAndTriggerAlerts() -System.println("[HAPTIC] Time since last alert: " + timeSinceLastAlert); -``` - -**Test Haptic Timing**: - -1. Start recording -2. Manually set cadence out of zone -3. Note timestamp of first alert -4. Wait 30 seconds -5. Verify second alert timing -6. Repeat for full 3-minute cycle - ---- - -## Version History - -**Current Version**: 1.1 (January 2026) - -**v1.1 Changes**: - -- ✅ Added: Haptic feedback system - - Single buzz for below-zone cadence - - Double buzz for above-zone cadence - - 30-second repeat interval - - 3-minute maximum duration - - Timer-free implementation -- ✅ Fixed: Timer creation overhead -- ✅ Added: Zone state tracking -- ✅ Improved: Memory efficiency -- ✅ Updated: Documentation with flow diagrams - -**v1.0 Changes** (from original): - -- ✅ Fixed: Uncommented critical recording check (line 270) -- ✅ Added: Full state machine (IDLE/RECORDING/PAUSED/STOPPED) -- ✅ Added: Pause/Resume functionality -- ✅ Added: Save/Discard workflow -- ✅ Added: Garmin ActivityRecording integration -- ✅ Added: Menu system for activity control -- ✅ Fixed: Timer now properly pauses/resumes -- ✅ Added: Visual state indicators -- ✅ Added: Comprehensive documentation - -**Known Limitations**: - -- No persistent storage of CQ history -- No lap/split functionality -- No custom alert thresholds -- No data export capability -- Haptic intensity not configurable -- No terrain-adaptive zones - ---- - -## Glossary - -**CQ**: Cadence Quality - composite score measuring running efficiency -**FIT File**: Flexible and Interoperable Transfer - Garmin's activity file format -**SPM**: Steps Per Minute - cadence measurement unit -**Circular Buffer**: Fixed-size buffer that wraps when full -**Activity Session**: Garmin's ActivityRecording instance managing timer/sensors -**State Machine**: System that transitions between defined states based on events -**Delegate Pattern**: Separation of input handling from view logic -**MVC**: Model-View-Controller architecture pattern -**Haptic Feedback**: Tactile vibration alerts -**Zone State**: Current cadence position relative to target range (-1/0/1) -**Alert Cycle**: Period of repeated haptic alerts (3 minutes maximum) -**Timer-Free**: Architecture using timestamps instead of dedicated timers - ---- - -## Other Info - -**Application**: Garmin Cadence Monitoring App for Forerunner 165 -**Platform**: Garmin Connect IQ SDK 8.3.0 -**Language**: Monkey C -**Target API**: 5.2.0+ -**Documentation Version**: 2.0 -**Last Updated**: January 2026 - -## Special Mentions for their amazing work this semester. - -**Dom** -**Chum** -**Jack** -**Kyle** -**Jin** - ----