diff --git a/manifest.xml b/manifest.xml index 594fc80..8d2c269 100644 --- a/manifest.xml +++ b/manifest.xml @@ -24,6 +24,8 @@ + + diff --git a/source/Delegates/SimpleViewDelegate.mc b/source/Delegates/SimpleViewDelegate.mc index 36a84e2..75f9531 100644 --- a/source/Delegates/SimpleViewDelegate.mc +++ b/source/Delegates/SimpleViewDelegate.mc @@ -19,6 +19,24 @@ class SimpleViewDelegate extends WatchUi.BehaviorDelegate { } + function onSelect() as Boolean { + // Toggle recording on/off with SELECT button + var app = getApp(); + + if (app.isActivityRecording()) { + // Show stop menu + var menu = new WatchUi.Menu2({:title => "Stop Recording?"}); + menu.addItem(new WatchUi.MenuItem("Yes", null, :stop_yes, {})); + menu.addItem(new WatchUi.MenuItem("No", null, :stop_no, {})); + WatchUi.pushView(menu, new StopMenuDelegate(), WatchUi.SLIDE_IMMEDIATE); + } else { + // Start recording immediately + app.startRecording(); + WatchUi.requestUpdate(); + } + + return true; + } function onKey(keyEvent as WatchUi.KeyEvent){ var key = keyEvent.getKey(); @@ -65,4 +83,65 @@ class SimpleViewDelegate extends WatchUi.BehaviorDelegate { return true; } -} \ No newline at end of file +} + +// Delegate for handling stop menu selection +class StopMenuDelegate extends WatchUi.Menu2InputDelegate { + + function initialize() { + Menu2InputDelegate.initialize(); + } + + function onSelect(item as MenuItem) as Void { + var id = item.getId(); + + if (id == :stop_yes) { + // User selected YES to stop - show save menu + var menu = new WatchUi.Menu2({:title => "Save Activity?"}); + menu.addItem(new WatchUi.MenuItem("Save", null, :save_yes, {})); + menu.addItem(new WatchUi.MenuItem("Discard", null, :save_no, {})); + WatchUi.pushView(menu, new SaveMenuDelegate(), WatchUi.SLIDE_IMMEDIATE); + } else if (id == :stop_no) { + // User selected NO - continue recording + WatchUi.popView(WatchUi.SLIDE_IMMEDIATE); + } + } + + function onBack() as Void { + // BACK button cancels + WatchUi.popView(WatchUi.SLIDE_IMMEDIATE); + } +} + +// Delegate for handling save menu selection +class SaveMenuDelegate extends WatchUi.Menu2InputDelegate { + + function initialize() { + Menu2InputDelegate.initialize(); + } + + function onSelect(item as MenuItem) as Void { + var id = item.getId(); + var app = getApp(); + + if (id == :save_yes) { + // Save the activity + app.stopRecording(); + app.saveRecording(); + } else if (id == :save_no) { + // Discard the activity + app.stopRecording(); + app.discardRecording(); + } + + // Pop both menus (save and stop) + WatchUi.popView(WatchUi.SLIDE_IMMEDIATE); + WatchUi.popView(WatchUi.SLIDE_IMMEDIATE); + WatchUi.requestUpdate(); + } + + function onBack() as Void { + // BACK button goes back to stop menu + WatchUi.popView(WatchUi.SLIDE_IMMEDIATE); + } +} diff --git a/source/GarminApp.mc b/source/GarminApp.mc index 4ec1535..c030c3c 100644 --- a/source/GarminApp.mc +++ b/source/GarminApp.mc @@ -1,6 +1,10 @@ import Toybox.Application; import Toybox.Lang; import Toybox.WatchUi; +import Toybox.Timer; +import Toybox.Activity; +import Toybox.System; +import Toybox.ActivityRecording; class GarminApp extends Application.AppBase { const MAX_BARS = 60; @@ -8,6 +12,8 @@ class GarminApp extends Application.AppBase { const MAX_CADENCE = 190; var globalTimer; + var session as ActivityRecording.Session?; + var isRecording as Boolean = false; enum { Beginner = 1.06, @@ -25,7 +31,7 @@ class GarminApp extends Application.AppBase { private var _idealMaxCadence = 100; private var _cadenceIndex = 0; private var _cadenceCount = 0; - private var _cadenceHistory as Array = new [MAX_BARS]; // Store 60 data points (1 minutes at 1-second intervals) + private var _cadenceHistory as Array = new [MAX_BARS]; private var _userHeight = null;//>>cm private var _userSpeed = null;//>>m/s @@ -41,12 +47,17 @@ class GarminApp extends Application.AppBase { function initialize() { AppBase.initialize(); + System.println("[INFO] App initialized"); } - // onStart() is called on application start up function onStart(state as Dictionary?) as Void { + System.println("[INFO] App starting"); + + // Log memory on startup + Logger.logMemoryStats("Startup"); + globalTimer = new Timer.Timer(); - globalTimer.start(method(:updateCadence),1000,true); + globalTimer.start(method(:updateCadence), 1000, true); dummyValueTesting(); /* remember to remove after testing @@ -54,29 +65,121 @@ class GarminApp extends Application.AppBase { idealCadenceCalculator(); } - // onStop() is called when your application is exiting function onStop(state as Dictionary?) as Void { + System.println("[INFO] App stopping"); + + // Stop recording if active + if (isRecording && session != null) { + stopRecording(); + } + if(globalTimer != null){ globalTimer.stop(); globalTimer = null; } + + // Log memory on shutdown + Logger.logMemoryStats("Shutdown"); + } + + function startRecording() as Void { + if (!isRecording) { + System.println("[INFO] Starting activity recording"); + + // Create a new session + session = ActivityRecording.createSession({ + :name => "Cadence Training", + :sport => ActivityRecording.SPORT_RUNNING, + :subSport => ActivityRecording.SUB_SPORT_GENERIC + }); + + if (session != null) { + session.start(); + isRecording = true; + System.println("[INFO] Recording started successfully"); + } else { + System.println("[ERROR] Failed to create session"); + } + } } + function stopRecording() as Void { + if (isRecording && session != null) { + System.println("[INFO] Stopping activity recording"); + session.stop(); + isRecording = false; + System.println("[INFO] Recording stopped"); + } + } + + function saveRecording() as Void { + if (session != null) { + System.println("[INFO] Saving activity"); + session.save(); + session = null; + isRecording = false; + System.println("[INFO] Activity saved"); + } + } + + function discardRecording() as Void { + if (session != null) { + System.println("[INFO] Discarding activity"); + session.discard(); + session = null; + isRecording = false; + System.println("[INFO] Activity discarded"); + } + } + + function isActivityRecording() as Boolean { + return isRecording; + } function updateCadence() as Void { var info = Activity.getActivityInfo(); - //var zoneState = null; if (info != null && info.currentCadence != null) { var newCadence = info.currentCadence; _cadenceHistory[_cadenceIndex] = newCadence.toFloat(); - // Add to circular buffer _cadenceIndex = (_cadenceIndex + 1) % MAX_BARS; if (_cadenceCount < MAX_BARS) { _cadenceCount++; } } + + // Log memory every 60 seconds + if (_cadenceIndex % 60 == 0 && _cadenceIndex > 0) { + Logger.logMemoryStats("Runtime"); + } + } - //WatchUi.requestUpdate(); + function idealCadenceCalculator() as Void { + var referenceCadence = 0; + var finalCadence = 0; + var userLegLength = _userHeight * 0.53; + + //reference cadence + switch (_userGender) { + case Male: + referenceCadence = (-1.268 * userLegLength) + (3.471 * _userSpeed) + 261.378; + break; + case Female: + referenceCadence = (-1.190 * userLegLength) + (3.705 * _userSpeed) + 249.688; + break; + default: + referenceCadence = (-1.251 * userLegLength) + (3.665 * _userSpeed) + 254.858; + break; + } + + //experience adjustment + referenceCadence = referenceCadence * _experienceLvl; + //apply threshold + referenceCadence = Math.round(referenceCadence); + finalCadence = max(BASELINE_AVG_CADENCE,min(referenceCadence,MAX_CADENCE)).toNumber(); + + //set new min max ideal cadence + _idealMaxCadence = finalCadence + 5; + _idealMinCadence = finalCadence - 5; } function idealCadenceCalculator() as Void { @@ -185,9 +288,8 @@ 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; -} +} \ No newline at end of file diff --git a/source/Logger.mc b/source/Logger.mc new file mode 100644 index 0000000..1c97e52 --- /dev/null +++ b/source/Logger.mc @@ -0,0 +1,24 @@ +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/Views/AdvancedView.mc b/source/Views/AdvancedView.mc index 6f2d476..616e93b 100644 --- a/source/Views/AdvancedView.mc +++ b/source/Views/AdvancedView.mc @@ -191,4 +191,4 @@ function correctColor(cadence as Number, idealMinCadence as Number, idealMaxCade { dc.setColor(0x00FF00, Graphics.COLOR_TRANSPARENT);//green } -} \ No newline at end of file +} diff --git a/source/Views/SimpleView.mc b/source/Views/SimpleView.mc index 8cb1d38..ff004b8 100644 --- a/source/Views/SimpleView.mc +++ b/source/Views/SimpleView.mc @@ -49,6 +49,10 @@ class SimpleView extends WatchUi.View { function onUpdate(dc as Dc) as Void { //update the display for current cadence displayCadence(); + + // Draw recording indicator + drawRecordingIndicator(dc); + // Call the parent onUpdate function to redraw the layout View.onUpdate(dc); } @@ -67,6 +71,28 @@ class SimpleView extends WatchUi.View { WatchUi.requestUpdate(); } + function drawRecordingIndicator(dc as Dc) as Void { + var app = getApp(); + + if (app.isActivityRecording()) { + // Draw a red recording indicator in top-right corner + dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_TRANSPARENT); + var width = dc.getWidth(); + var radius = 8; + dc.fillCircle(width - 15, 15, radius); + + // Add "REC" text next to the indicator + dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); + dc.drawText(width - 35, 5, Graphics.FONT_TINY, "REC", Graphics.TEXT_JUSTIFY_RIGHT); + } else { + // Draw instruction text at bottom + dc.setColor(Graphics.COLOR_LT_GRAY, Graphics.COLOR_TRANSPARENT); + var width = dc.getWidth(); + var height = dc.getHeight(); + dc.drawText(width / 2, height - 25, Graphics.FONT_TINY, "Press SELECT to start", Graphics.TEXT_JUSTIFY_CENTER); + } + } + function displayCadence() as Void{ var info = Activity.getActivityInfo();