Skip to content
2 changes: 2 additions & 0 deletions manifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
<iq:permissions>
<iq:uses-permission id="Fit"/>
<iq:uses-permission id="Notifications"/>
<iq:uses-permission id="PersistedLocations"/>
<iq:uses-permission id="Positioning"/>
<iq:uses-permission id="Sensor"/>
<iq:uses-permission id="SensorHistory"/>
<iq:uses-permission id="SensorLogging"/>
Expand Down
81 changes: 80 additions & 1 deletion source/Delegates/SimpleViewDelegate.mc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -65,4 +83,65 @@ class SimpleViewDelegate extends WatchUi.BehaviorDelegate {
return true;
}

}
}

// Delegate for handling stop menu selection
class StopMenuDelegate extends WatchUi.Menu2InputDelegate {

function initialize() {
Menu2InputDelegate.initialize();
}

function onSelect(item as MenuItem) as Void {
var id = item.getId();

if (id == :stop_yes) {
// User selected YES to stop - show save menu
var menu = new WatchUi.Menu2({:title => "Save Activity?"});
menu.addItem(new WatchUi.MenuItem("Save", null, :save_yes, {}));
menu.addItem(new WatchUi.MenuItem("Discard", null, :save_no, {}));
WatchUi.pushView(menu, new SaveMenuDelegate(), WatchUi.SLIDE_IMMEDIATE);
} else if (id == :stop_no) {
// User selected NO - continue recording
WatchUi.popView(WatchUi.SLIDE_IMMEDIATE);
}
}

function onBack() as Void {
// BACK button cancels
WatchUi.popView(WatchUi.SLIDE_IMMEDIATE);
}
}

// Delegate for handling save menu selection
class SaveMenuDelegate extends WatchUi.Menu2InputDelegate {

function initialize() {
Menu2InputDelegate.initialize();
}

function onSelect(item as MenuItem) as Void {
var id = item.getId();
var app = getApp();

if (id == :save_yes) {
// Save the activity
app.stopRecording();
app.saveRecording();
} else if (id == :save_no) {
// Discard the activity
app.stopRecording();
app.discardRecording();
}

// Pop both menus (save and stop)
WatchUi.popView(WatchUi.SLIDE_IMMEDIATE);
WatchUi.popView(WatchUi.SLIDE_IMMEDIATE);
WatchUi.requestUpdate();
}

function onBack() as Void {
// BACK button goes back to stop menu
WatchUi.popView(WatchUi.SLIDE_IMMEDIATE);
}
}
122 changes: 112 additions & 10 deletions source/GarminApp.mc
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
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;
const BASELINE_AVG_CADENCE = 160;
const MAX_CADENCE = 190;

var globalTimer;
var session as ActivityRecording.Session?;
var isRecording as Boolean = false;

enum {
Beginner = 1.06,
Expand All @@ -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<Float?> = new [MAX_BARS]; // Store 60 data points (1 minutes at 1-second intervals)
private var _cadenceHistory as Array<Float?> = new [MAX_BARS];

private var _userHeight = null;//>>cm
private var _userSpeed = null;//>>m/s
Expand All @@ -41,42 +47,139 @@ 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
*/
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 {
Expand Down Expand Up @@ -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;
}
}
24 changes: 24 additions & 0 deletions source/Logger.mc
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
2 changes: 1 addition & 1 deletion source/Views/AdvancedView.mc
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,4 @@ function correctColor(cadence as Number, idealMinCadence as Number, idealMaxCade
{
dc.setColor(0x00FF00, Graphics.COLOR_TRANSPARENT);//green
}
}
}
26 changes: 26 additions & 0 deletions source/Views/SimpleView.mc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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();

Expand Down