diff --git a/source/Delegates/SimpleViewDelegate.mc b/source/Delegates/SimpleViewDelegate.mc index 163d145..be21c43 100644 --- a/source/Delegates/SimpleViewDelegate.mc +++ b/source/Delegates/SimpleViewDelegate.mc @@ -1,5 +1,6 @@ import Toybox.Lang; import Toybox.WatchUi; +import Toybox.System; class SimpleViewDelegate extends WatchUi.BehaviorDelegate { @@ -22,6 +23,17 @@ class SimpleViewDelegate extends WatchUi.BehaviorDelegate { if (app.isActivityRecording()) { app.stopRecording(); System.println("[UI] Cadence monitoring stopped"); + + // Auto-navigate to summary screen if we have valid data + if (app.hasValidSummaryData()) { + System.println("[UI] Showing activity summary"); + var summaryView = new SummaryView(); + WatchUi.pushView( + summaryView, + new SummaryViewDelegate(), + WatchUi.SLIDE_UP + ); + } } else { app.startRecording(); System.println("[UI] Cadence monitoring started"); diff --git a/source/Delegates/SummaryViewDelegate.mc b/source/Delegates/SummaryViewDelegate.mc new file mode 100644 index 0000000..85e15bd --- /dev/null +++ b/source/Delegates/SummaryViewDelegate.mc @@ -0,0 +1,51 @@ +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 to dismiss + function onBack() as Boolean { + System.println("[SUMMARY] Back pressed, returning to main view"); + WatchUi.popView(WatchUi.SLIDE_DOWN); + 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; + } +} diff --git a/source/GarminApp.mc b/source/GarminApp.mc index fc575ce..7f2d9d9 100644 --- a/source/GarminApp.mc +++ b/source/GarminApp.mc @@ -67,6 +67,11 @@ class GarminApp extends Application.AppBase { private var _finalCQTrend = null; private var _cqHistory as Array = []; + // 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(); @@ -110,6 +115,12 @@ class GarminApp extends Application.AppBase { _cqHistory = []; _cadenceCount = 0; _missingCadenceCount = 0; + + // Reset activity metrics + _sessionDuration = null; + _sessionDistance = null; + _avgHeartRate = null; + _peakHeartRate = null; isRecording = true; } @@ -121,6 +132,9 @@ class GarminApp extends Application.AppBase { System.println("[INFO] Stopping cadence monitoring"); + // Capture activity metrics before stopping + captureActivityMetrics(); + var cq = computeCadenceQualityScore(); if (cq >= 0) { @@ -141,6 +155,29 @@ class GarminApp extends Application.AppBase { isRecording = false; } + 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 { //if (!isRecording) { return;} // ignore samples when not actively monitoring @@ -524,6 +561,88 @@ class GarminApp extends Application.AppBase { function getInitialView() as [Views] or [Views, InputDelegates] { return [ new SimpleView(), new SimpleViewDelegate() ]; } + + // ----------------------- + // Summary Statistics Methods + // ----------------------- + + function getAverageCadence() as Float { + if (_cadenceCount == 0) { + return 0.0; + } + + var total = 0.0; + var validSamples = 0; + + for (var i = 0; i < MAX_BARS; i++) { + var c = _cadenceHistory[i]; + if (c != null) { + total += c; + validSamples++; + } + } + + if (validSamples == 0) { + return 0.0; + } + + return total / validSamples; + } + + function getTimeInZonePercentage() as Number { + return computeTimeInZoneScore(); + } + + function getMinCadenceFromHistory() as Number { + var minCad = null; + + for (var i = 0; i < MAX_BARS; i++) { + var c = _cadenceHistory[i]; + if (c != null) { + if (minCad == null || c < minCad) { + minCad = c; + } + } + } + + return (minCad != null) ? minCad.toNumber() : 0; + } + + function getMaxCadenceFromHistory() as Number { + var maxCad = null; + + for (var i = 0; i < MAX_BARS; i++) { + var c = _cadenceHistory[i]; + if (c != null) { + if (maxCad == null || c > maxCad) { + maxCad = c; + } + } + } + + return (maxCad != null) ? maxCad.toNumber() : 0; + } + + function hasValidSummaryData() as Boolean { + return _cadenceCount >= MIN_CQ_SAMPLES && _finalCQ != null; + } + + // Activity metrics getters + function getSessionDuration() { + return _sessionDuration; + } + + function getSessionDistance() { + return _sessionDistance; + } + + function getAvgHeartRate() { + return _avgHeartRate; + } + + function getPeakHeartRate() { + return _peakHeartRate; + } } function getApp() as GarminApp { diff --git a/source/Views/SummaryView.mc b/source/Views/SummaryView.mc new file mode 100644 index 0000000..5a471d6 --- /dev/null +++ b/source/Views/SummaryView.mc @@ -0,0 +1,373 @@ +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; + } + } +}