From ae2265aa2fa5a798441b4e7839a8501cdb86fb94 Mon Sep 17 00:00:00 2001 From: Pasta1779 Date: Mon, 15 Dec 2025 22:44:28 +1100 Subject: [PATCH 1/9] Added System Logging --- source/GarminApp.mc | 168 ++++++++++++++++++++---- source/Logger.mc | 310 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 454 insertions(+), 24 deletions(-) create mode 100644 source/Logger.mc diff --git a/source/GarminApp.mc b/source/GarminApp.mc index c48b26d..36b2a9e 100644 --- a/source/GarminApp.mc +++ b/source/GarminApp.mc @@ -1,6 +1,9 @@ import Toybox.Application; import Toybox.Lang; import Toybox.WatchUi; +import Toybox.Timer; +import Toybox.Activity; +import Toybox.System; class GarminApp extends Application.AppBase { const MAX_BARS = 60; @@ -9,7 +12,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]; // Store 60 data points (1 minute at 1-second intervals) var globalTimer; @@ -26,37 +29,115 @@ class GarminApp extends Application.AppBase { function initialize() { AppBase.initialize(); + + // Initialize the logging system + try { + Logger.initialize(); + Logger.log(Logger.INFO, "GarminApp", "Application initialized"); + Logger.log(Logger.INFO, "GarminApp", "Training level: " + _trainingLvl); + Logger.log(Logger.INFO, "GarminApp", "Cadence range: " + _idealMinCadence + "-" + _idealMaxCadence); + } catch (e) { + // Fallback to System.println if logger fails + System.println("Failed to initialize logger: " + e.getErrorMessage()); + } } // onStart() is called on application start up function onStart(state as Dictionary?) as Void { - globalTimer = new Timer.Timer(); - globalTimer.start(method(:updateCadence),1000,true); + try { + Logger.log(Logger.INFO, "GarminApp", "Application starting"); + Logger.logMemoryStats("GarminApp"); + + // Initialize and start the global timer + globalTimer = new Timer.Timer(); + globalTimer.start(method(:updateCadence), 1000, true); + + Logger.log(Logger.INFO, "GarminApp", "Global cadence timer started (1s interval)"); + + } catch (e) { + Logger.logCrash("GarminApp", "Failed to start application", e); + // Re-throw critical errors + throw e; + } } // onStop() is called when your application is exiting function onStop(state as Dictionary?) as Void { - if(globalTimer != null){ - globalTimer.stop(); - globalTimer = null; + try { + Logger.log(Logger.INFO, "GarminApp", "Application stopping"); + Logger.log(Logger.INFO, "GarminApp", "Total cadence readings: " + _cadenceCount); + + // Stop and cleanup the global timer + if(globalTimer != null){ + globalTimer.stop(); + globalTimer = null; + Logger.log(Logger.INFO, "GarminApp", "Global timer stopped"); + } + + // Log final memory state + Logger.logMemoryStats("GarminApp"); + + // Ensure all logs are flushed to storage + Logger.shutdown(); + + } catch (e) { + // Even if logging fails, try to output to console + System.println("Error during app stop: " + e.getErrorMessage()); } } - + /** + * Update cadence data from activity information + * Called every second by the global timer + */ 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++; } + try { + var info = Activity.getActivityInfo(); + + if (info != null && info.currentCadence != null) { + var newCadence = info.currentCadence; + + // Store in circular buffer + _cadenceHistory[_cadenceIndex] = newCadence.toFloat(); + _cadenceIndex = (_cadenceIndex + 1) % MAX_BARS; + + if (_cadenceCount < MAX_BARS) { + _cadenceCount++; + } + + // Log cadence periodically (every 10 seconds) to avoid log spam + if (_cadenceIndex % 10 == 0) { + Logger.log(Logger.DEBUG, "GarminApp", "Cadence update: " + newCadence + " spm (count: " + _cadenceCount + ")"); + } + + // Log if cadence is outside ideal range + if (newCadence < _idealMinCadence || newCadence > _idealMaxCadence) { + if (_cadenceIndex % 5 == 0) { // Log every 5 seconds when out of range + var status = newCadence < _idealMinCadence ? "below" : "above"; + Logger.log(Logger.WARNING, "GarminApp", "Cadence " + status + " target range: " + newCadence + " spm"); + } + } + + } else { + // Log when activity info is unavailable (but not too frequently) + if (_cadenceIndex % 30 == 0) { // Log every 30 seconds + if (info == null) { + Logger.log(Logger.WARNING, "GarminApp", "Activity info unavailable"); + } else { + Logger.log(Logger.WARNING, "GarminApp", "Current cadence is null"); + } + } + } + + // Periodically log memory stats (every minute) + if (_cadenceIndex % 60 == 0 && _cadenceIndex > 0) { + Logger.logMemoryStats("GarminApp"); + } + + } catch (e) { + Logger.logError("GarminApp", "Error updating cadence", e); + // Don't re-throw - we want the timer to continue } - - //WatchUi.requestUpdate(); - } function getMinCadence() as Number { @@ -80,20 +161,59 @@ class GarminApp extends Application.AppBase { } function setMinCadence(value as Number) as Void { - _idealMinCadence = value; + try { + var oldValue = _idealMinCadence; + _idealMinCadence = value; + Logger.log(Logger.INFO, "GarminApp", "Min cadence updated: " + oldValue + " -> " + value); + } catch (e) { + Logger.logError("GarminApp", "Error setting min cadence", e); + // Still try to set the value + _idealMinCadence = value; + } } function setMaxCadence(value as Number) as Void { - _idealMaxCadence = value; + try { + var oldValue = _idealMaxCadence; + _idealMaxCadence = value; + Logger.log(Logger.INFO, "GarminApp", "Max cadence updated: " + oldValue + " -> " + value); + } catch (e) { + Logger.logError("GarminApp", "Error setting max cadence", e); + // Still try to set the value + _idealMaxCadence = value; + } + } + + // Additional getters for user info (if needed for future features) + function getUserHeight() as Number { + return _userHeight; + } + + function getUserSpeed() as Number { + return _userSpeed; + } + + function getTrainingLevel() as Number { + return _trainingLvl; } // Return the initial view of your application here function getInitialView() as [Views] or [Views, InputDelegates] { - return [ new SimpleView(), new SimpleViewDelegate() ]; + try { + Logger.log(Logger.INFO, "GarminApp", "Loading initial view (SimpleView)"); + return [ new SimpleView(), new SimpleViewDelegate() ]; + } catch (e) { + Logger.logCrash("GarminApp", "Critical: Failed to load initial view", e); + // Re-throw since we can't function without a view + throw e; + } } - - } + +/** + * Global helper function to get the app instance + * This is used throughout the app to access shared state + */ function getApp() as GarminApp { return Application.getApp() as GarminApp; } diff --git a/source/Logger.mc b/source/Logger.mc new file mode 100644 index 0000000..8a88980 --- /dev/null +++ b/source/Logger.mc @@ -0,0 +1,310 @@ +import Toybox.Application.Storage; +import Toybox.Lang; +import Toybox.System; +import Toybox.Time; + +/** + * Logger module for debugging, system events, and crash logging + * Logs are stored in persistent storage and can be retrieved for analysis + */ +module Logger { + + enum LogLevel { + DEBUG = 0, + INFO = 1, + WARNING = 2, + ERROR = 3, + CRITICAL = 4 + } + + const MAX_LOG_ENTRIES = 100; // Maximum number of log entries to keep + const LOG_STORAGE_KEY = "app_logs"; + const CRASH_LOG_KEY = "crash_logs"; + const SESSION_START_KEY = "session_start"; + + var currentSession as String = ""; + var logBuffer as Array = []; + var logCount = 0; + + /** + * Initialize the logger system + */ + function initialize() as Void { + currentSession = generateSessionId(); + logBuffer = []; + logCount = 0; + + // Log session start + log(INFO, "Logger", "Session started: " + currentSession); + logSystemInfo(); + + // Store session start time + Storage.setValue(SESSION_START_KEY, Time.now().value()); + } + + /** + * Generate a unique session identifier + */ + function generateSessionId() as String { + var now = Time.now(); + var info = Time.Gregorian.info(now, Time.FORMAT_MEDIUM); + return info.year.format("%04d") + info.month.format("%02d") + info.day.format("%02d") + + "_" + info.hour.format("%02d") + info.min.format("%02d") + info.sec.format("%02d"); + } + + /** + * Log system information at startup + */ + function logSystemInfo() as Void { + var stats = System.getSystemStats(); + var deviceSettings = System.getDeviceSettings(); + + log(INFO, "System", "Battery: " + stats.battery + "%"); + log(INFO, "System", "Memory: " + stats.totalMemory + " bytes"); + log(INFO, "System", "Free Memory: " + stats.freeMemory + " bytes"); + log(INFO, "System", "Device: " + deviceSettings.partNumber); + log(INFO, "System", "FW Version: " + deviceSettings.firmwareVersion); + } + + /** + * Main logging function + * @param level - Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + * @param tag - Category or component name + * @param message - Log message + */ + function log(level as LogLevel, tag as String, message as String) as Void { + try { + var timestamp = Time.now().value(); + var levelStr = getLevelString(level); + var logEntry = timestamp + "|" + levelStr + "|" + tag + "|" + message; + + // Add to buffer + logBuffer.add(logEntry); + logCount++; + + // If buffer is full, flush to storage + if (logBuffer.size() >= 10) { + flushLogs(); + } + + // Also output to system for real-time debugging + System.println("[" + levelStr + "] " + tag + ": " + message); + + } catch (e) { + System.println("Logger error: " + e.getErrorMessage()); + } + } + + /** + * Convert log level to string + */ + function getLevelString(level as LogLevel) as String { + if (level == DEBUG) { return "DEBUG"; } + else if (level == INFO) { return "INFO"; } + else if (level == WARNING) { return "WARN"; } + else if (level == ERROR) { return "ERROR"; } + else if (level == CRITICAL) { return "CRIT"; } + return "UNKNOWN"; + } + + /** + * Flush buffered logs to persistent storage + */ + function flushLogs() as Void { + try { + if (logBuffer.size() == 0) { + return; + } + + // Get existing logs + var existingLogs = Storage.getValue(LOG_STORAGE_KEY); + var allLogs = []; + + if (existingLogs != null && existingLogs instanceof Array) { + allLogs = existingLogs as Array; + } + + // Add new logs + for (var i = 0; i < logBuffer.size(); i++) { + allLogs.add(logBuffer[i]); + } + + // Trim if too many entries + while (allLogs.size() > MAX_LOG_ENTRIES) { + allLogs = allLogs.slice(1, allLogs.size()); + } + + // Save to storage + Storage.setValue(LOG_STORAGE_KEY, allLogs); + + // Clear buffer + logBuffer = []; + + } catch (e) { + System.println("Failed to flush logs: " + e.getErrorMessage()); + } + } + + /** + * Log an error with exception details + */ + function logError(tag as String, message as String, exception as Exception) as Void { + var errorMsg = message + " - " + exception.getErrorMessage(); + log(ERROR, tag, errorMsg); + flushLogs(); // Immediately flush errors + } + + /** + * Log a crash with full context + */ + function logCrash(tag as String, message as String, exception as Exception) as Void { + try { + var timestamp = Time.now().value(); + var stats = System.getSystemStats(); + + var crashData = { + "timestamp" => timestamp, + "session" => currentSession, + "tag" => tag, + "message" => message, + "error" => exception.getErrorMessage(), + "battery" => stats.battery, + "freeMemory" => stats.freeMemory, + "totalMemory" => stats.totalMemory + }; + + // Get existing crash logs + var crashLogs = Storage.getValue(CRASH_LOG_KEY); + var allCrashes = []; + + if (crashLogs != null && crashLogs instanceof Array) { + allCrashes = crashLogs as Array; + } + + allCrashes.add(crashData); + + // Keep last 20 crashes + while (allCrashes.size() > 20) { + allCrashes = allCrashes.slice(1, allCrashes.size()); + } + + Storage.setValue(CRASH_LOG_KEY, allCrashes); + + // Also log normally + log(CRITICAL, tag, "CRASH: " + message + " - " + exception.getErrorMessage()); + flushLogs(); + + } catch (e) { + System.println("Failed to log crash: " + e.getErrorMessage()); + } + } + + /** + * Log memory statistics + */ + function logMemoryStats(tag as String) as Void { + var stats = System.getSystemStats(); + var usedMemory = stats.totalMemory - stats.freeMemory; + var memoryPercent = (usedMemory.toFloat() / stats.totalMemory.toFloat() * 100).toNumber(); + + log(INFO, tag, "Memory: " + usedMemory + "/" + stats.totalMemory + + " (" + memoryPercent + "% used)"); + } + + /** + * Log activity data for debugging + */ + function logActivityInfo(tag as String, info as Activity.Info?) as Void { + if (info == null) { + log(WARNING, tag, "Activity info is null"); + return; + } + + var msg = "Activity - "; + if (info.currentCadence != null) { + msg += "Cadence:" + info.currentCadence + " "; + } + if (info.currentHeartRate != null) { + msg += "HR:" + info.currentHeartRate + " "; + } + if (info.elapsedDistance != null) { + msg += "Dist:" + (info.elapsedDistance / 100000.0).format("%.2f") + "km "; + } + if (info.timerTime != null) { + msg += "Time:" + (info.timerTime / 1000) + "s"; + } + + log(DEBUG, tag, msg); + } + + /** + * Get all logs as formatted string for display or export + */ + function getLogsAsString() as String { + flushLogs(); // Ensure all logs are saved + + var logs = Storage.getValue(LOG_STORAGE_KEY); + if (logs == null || !(logs instanceof Array)) { + return "No logs available"; + } + + var result = "=== APP LOGS ===\n"; + var logArray = logs as Array; + + for (var i = 0; i < logArray.size(); i++) { + result += logArray[i] + "\n"; + } + + return result; + } + + /** + * Get crash logs as formatted string + */ + function getCrashLogsAsString() as String { + var crashes = Storage.getValue(CRASH_LOG_KEY); + if (crashes == null || !(crashes instanceof Array)) { + return "No crash logs available"; + } + + var result = "=== CRASH LOGS ===\n"; + var crashArray = crashes as Array; + + for (var i = 0; i < crashArray.size(); i++) { + var crash = crashArray[i]; + if (crash instanceof Dictionary) { + var crashDict = crash as Dictionary; + result += "Crash " + (i + 1) + ":\n"; + result += " Time: " + crashDict["timestamp"] + "\n"; + result += " Session: " + crashDict["session"] + "\n"; + result += " Tag: " + crashDict["tag"] + "\n"; + result += " Message: " + crashDict["message"] + "\n"; + result += " Error: " + crashDict["error"] + "\n"; + result += " Battery: " + crashDict["battery"] + "%\n"; + result += " Memory: " + crashDict["freeMemory"] + "/" + + crashDict["totalMemory"] + "\n\n"; + } + } + + return result; + } + + /** + * Clear all logs + */ + function clearLogs() as Void { + Storage.deleteValue(LOG_STORAGE_KEY); + Storage.deleteValue(CRASH_LOG_KEY); + logBuffer = []; + logCount = 0; + log(INFO, "Logger", "Logs cleared"); + } + + /** + * Shutdown logger and ensure all logs are saved + */ + function shutdown() as Void { + log(INFO, "Logger", "Session ended: " + currentSession); + flushLogs(); + } +} From 5958aa79969de4b9f19bfd53a49ce1497797634e Mon Sep 17 00:00:00 2001 From: Pasta1779 Date: Mon, 15 Dec 2025 23:18:37 +1100 Subject: [PATCH 2/9] bug fix in logger.mc where crash on startup --- source/Logger.mc | 32 +-- source/Views/AdvancedView.mc | 393 ++++++++++++++++++++++------------- 2 files changed, 274 insertions(+), 151 deletions(-) diff --git a/source/Logger.mc b/source/Logger.mc index 8a88980..b784e76 100644 --- a/source/Logger.mc +++ b/source/Logger.mc @@ -2,6 +2,7 @@ import Toybox.Application.Storage; import Toybox.Lang; import Toybox.System; import Toybox.Time; +import Toybox.Activity; /** * Logger module for debugging, system events, and crash logging @@ -214,24 +215,31 @@ module Logger { /** * Log activity data for debugging */ - function logActivityInfo(tag as String, info as Activity.Info?) as Void { + function logActivityInfo(tag as String, info as Lang.Object) as Void { if (info == null) { log(WARNING, tag, "Activity info is null"); return; } + var activityInfo = info; var msg = "Activity - "; - if (info.currentCadence != null) { - msg += "Cadence:" + info.currentCadence + " "; - } - if (info.currentHeartRate != null) { - msg += "HR:" + info.currentHeartRate + " "; - } - if (info.elapsedDistance != null) { - msg += "Dist:" + (info.elapsedDistance / 100000.0).format("%.2f") + "km "; - } - if (info.timerTime != null) { - msg += "Time:" + (info.timerTime / 1000) + "s"; + + // Safely access properties + try { + if (activityInfo has :currentCadence && activityInfo.currentCadence != null) { + msg += "Cadence:" + activityInfo.currentCadence + " "; + } + if (activityInfo has :currentHeartRate && activityInfo.currentHeartRate != null) { + msg += "HR:" + activityInfo.currentHeartRate + " "; + } + if (activityInfo has :elapsedDistance && activityInfo.elapsedDistance != null) { + msg += "Dist:" + (activityInfo.elapsedDistance / 100000.0).format("%.2f") + "km "; + } + if (activityInfo has :timerTime && activityInfo.timerTime != null) { + msg += "Time:" + (activityInfo.timerTime / 1000) + "s"; + } + } catch (e) { + msg += "Error reading activity info"; } log(DEBUG, tag, msg); diff --git a/source/Views/AdvancedView.mc b/source/Views/AdvancedView.mc index 86d4992..0b07967 100644 --- a/source/Views/AdvancedView.mc +++ b/source/Views/AdvancedView.mc @@ -13,184 +13,299 @@ class AdvancedView extends WatchUi.View { const STEP_RATE = 6; private var _simulationTimer; + private var _updateCount = 0; function initialize() { View.initialize(); - + Logger.log(Logger.INFO, "AdvancedView", "View initialized"); } function onShow() as Void { - _simulationTimer = new Timer.Timer(); - _simulationTimer.start(method(:refreshScreen), 1000, true); + try { + Logger.log(Logger.INFO, "AdvancedView", "View shown"); + _simulationTimer = new Timer.Timer(); + _simulationTimer.start(method(:refreshScreen), 1000, true); + _updateCount = 0; + } catch (e) { + Logger.logError("AdvancedView", "Error in onShow", e); + } } function onHide() as Void { - if (_simulationTimer != null) { - _simulationTimer.stop(); - _simulationTimer = null; + try { + Logger.log(Logger.INFO, "AdvancedView", "View hidden (updates: " + _updateCount + ")"); + if (_simulationTimer != null) { + _simulationTimer.stop(); + _simulationTimer = null; + } + } catch (e) { + Logger.logError("AdvancedView", "Error in onHide", e); } } function onUpdate(dc as Dc) as Void { - View.onUpdate(dc); - // Draw all the elements - drawElements(dc); + try { + View.onUpdate(dc); + drawElements(dc); + _updateCount++; + + // Log memory periodically + if (_updateCount % 30 == 0) { + Logger.logMemoryStats("AdvancedView"); + } + } catch (e) { + Logger.logCrash("AdvancedView", "Critical error in onUpdate", e); + // Try to show error message to user + try { + dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_BLACK); + dc.clear(); + dc.drawText(dc.getWidth()/2, dc.getHeight()/2, + Graphics.FONT_SMALL, "Display Error", Graphics.TEXT_JUSTIFY_CENTER); + } catch (ex) { + // If even error display fails, just log it + System.println("Failed to display error: " + ex.getErrorMessage()); + } + } } function refreshScreen() as Void { - WatchUi.requestUpdate(); + try { + WatchUi.requestUpdate(); + } catch (e) { + Logger.logError("AdvancedView", "Error requesting update", e); + } } function drawElements(dc as Dc) as Void { - var width = dc.getWidth(); - var height = dc.getHeight(); - var info = Activity.getActivityInfo(); - var app = getApp(); - - // Draw elapsed time at top (yellow RGB: 255,248,18 = 0xFFF, using picker in paint to get RGB then convert to hex - if (info != null && info.timerTime != null) { - var seconds = info.timerTime / 1000; - var hours = seconds / 3600; - var minutes = (seconds % 3600) / 60; - var secs = seconds % 60; - var timeStr = hours.format("%01d") + ":" + minutes.format("%02d") + "." + secs.format("%02d"); - dc.setColor(0xFFF813, Graphics.COLOR_TRANSPARENT); - dc.drawText(width / 2, 3, Graphics.FONT_LARGE, timeStr, Graphics.TEXT_JUSTIFY_CENTER); - } - - // Draw heart rate circle (left, dark red RGB: 211,19,2519 - var hrX = width / 4; - var hrY = (height * 2) / 5; - var circleRadius = 42; - - dc.setColor(0x9D0000, Graphics.COLOR_TRANSPARENT); - dc.fillCircle(hrX, hrY, circleRadius); - - if (info != null && info.currentHeartRate != null) { - dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); // White RGB: 255,255,255 - dc.drawText(hrX, hrY - 25, Graphics.FONT_TINY, info.currentHeartRate.toString(), Graphics.TEXT_JUSTIFY_CENTER); - dc.drawText(hrX, hrY + 8, Graphics.FONT_XTINY, "bpm", Graphics.TEXT_JUSTIFY_CENTER); - } - - // Draw distance circle (right, dark green RGB: 24,19,24 = 0x1D5E11) - var distX = (width * 3) / 4; - var distY = hrY; - - dc.setColor(0x1D5E11, Graphics.COLOR_TRANSPARENT); - dc.fillCircle(distX, distY, circleRadius); - - if (info != null && info.elapsedDistance != null) { - var distanceKm = info.elapsedDistance / 100000.0; - dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); // White RGB: 255,255,255 - dc.drawText(distX, distY - 25, Graphics.FONT_TINY, distanceKm.format("%.2f"), Graphics.TEXT_JUSTIFY_CENTER); - dc.drawText(distX, distY + 8, Graphics.FONT_XTINY, "km", Graphics.TEXT_JUSTIFY_CENTER); + try { + var width = dc.getWidth(); + var height = dc.getHeight(); + var info = Activity.getActivityInfo(); + var app = getApp(); + + // Validate dimensions + if (width <= 0 || height <= 0) { + Logger.log(Logger.ERROR, "AdvancedView", "Invalid display dimensions: " + width + "x" + height); + return; + } + + // Draw elapsed time at top + drawElapsedTime(dc, width, height, info); + + // Draw heart rate circle (left) + drawHeartRate(dc, width, height, info); + + // Draw distance circle (right) + drawDistance(dc, width, height, info); + + // Draw ideal cadence range + drawIdealCadenceRange(dc, width, height, app); + + // Draw current cadence + drawCurrentCadence(dc, width, height, info, app); + + // Draw chart + drawChart(dc); + + } catch (e) { + Logger.logCrash("AdvancedView", "Error in drawElements", e); + throw e; // Re-throw to trigger onUpdate error handling } + } - //draw ideal cadence range - var idealMinCadence = app.getMinCadence(); - var idealMaxCadence = app.getMaxCadence(); - var idealCadenceY = height * 0.45; - + function drawElapsedTime(dc as Dc, width as Number, height as Number, info as Lang.Object) as Void { + try { + if (info != null && info.timerTime != null) { + var seconds = info.timerTime / 1000; + var hours = seconds / 3600; + var minutes = (seconds % 3600) / 60; + var secs = seconds % 60; + var timeStr = hours.format("%01d") + ":" + minutes.format("%02d") + "." + secs.format("%02d"); + dc.setColor(0xFFF813, Graphics.COLOR_TRANSPARENT); + dc.drawText(width / 2, 3, Graphics.FONT_LARGE, timeStr, Graphics.TEXT_JUSTIFY_CENTER); + } + } catch (e) { + Logger.logError("AdvancedView", "Error drawing elapsed time", e); + } + } - if(idealMinCadence != null && idealMaxCadence != null){ - var displayString = (idealMinCadence + " - " + idealMaxCadence).toString(); - dc.setColor(0xAAAAAA, Graphics.COLOR_TRANSPARENT); - dc.drawText(width / 2,idealCadenceY , Graphics.FONT_XTINY, displayString, Graphics.TEXT_JUSTIFY_CENTER); + function drawHeartRate(dc as Dc, width as Number, height as Number, info as Lang.Object) as Void { + try { + var hrX = width / 4; + var hrY = (height * 2) / 5; + var circleRadius = 42; + + dc.setColor(0x9D0000, Graphics.COLOR_TRANSPARENT); + dc.fillCircle(hrX, hrY, circleRadius); + + if (info != null && info.currentHeartRate != null) { + dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); + dc.drawText(hrX, hrY - 25, Graphics.FONT_TINY, info.currentHeartRate.toString(), Graphics.TEXT_JUSTIFY_CENTER); + dc.drawText(hrX, hrY + 8, Graphics.FONT_XTINY, "bpm", Graphics.TEXT_JUSTIFY_CENTER); + } + } catch (e) { + Logger.logError("AdvancedView", "Error drawing heart rate", e); } + } - var cadenceY = height * 0.8; + function drawDistance(dc as Dc, width as Number, height as Number, info as Lang.Object) as Void { + try { + var distX = (width * 3) / 4; + var distY = (height * 2) / 5; + var circleRadius = 42; + + dc.setColor(0x1D5E11, Graphics.COLOR_TRANSPARENT); + dc.fillCircle(distX, distY, circleRadius); + + if (info != null && info.elapsedDistance != null) { + var distanceKm = info.elapsedDistance / 100000.0; + dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); + dc.drawText(distX, distY - 25, Graphics.FONT_TINY, distanceKm.format("%.2f"), Graphics.TEXT_JUSTIFY_CENTER); + dc.drawText(distX, distY + 8, Graphics.FONT_XTINY, "km", Graphics.TEXT_JUSTIFY_CENTER); + } + } catch (e) { + Logger.logError("AdvancedView", "Error drawing distance", e); + } + } - if (info != null && info.currentCadence != null) { - // Draw "CADENCE" label (light gray RGB: 170,170,170 = 0xAAAAAA) - dc.setColor(0xAAAAAA, Graphics.COLOR_TRANSPARENT); - dc.drawText(width / 2, cadenceY, Graphics.FONT_XTINY, "CADENCE", Graphics.TEXT_JUSTIFY_CENTER); + function drawIdealCadenceRange(dc as Dc, width as Number, height as Number, app as GarminApp) as Void { + try { + var idealMinCadence = app.getMinCadence(); + var idealMaxCadence = app.getMaxCadence(); + var idealCadenceY = height * 0.45; - // Draw cadence value in green (RGB: 0,255,0 = 0x00FF00) - correctColor(info.currentCadence, idealMinCadence, idealMaxCadence, dc); - dc.drawText(width / 2, cadenceY + 20, Graphics.FONT_XTINY, info.currentCadence.toString() + " spm", Graphics.TEXT_JUSTIFY_CENTER); + if(idealMinCadence != null && idealMaxCadence != null){ + var displayString = (idealMinCadence + " - " + idealMaxCadence).toString(); + dc.setColor(0xAAAAAA, Graphics.COLOR_TRANSPARENT); + dc.drawText(width / 2, idealCadenceY, Graphics.FONT_XTINY, displayString, Graphics.TEXT_JUSTIFY_CENTER); + } + } catch (e) { + Logger.logError("AdvancedView", "Error drawing ideal cadence range", e); } + } + + function drawCurrentCadence(dc as Dc, width as Number, height as Number, info as Lang.Object, app as GarminApp) as Void { + try { + var cadenceY = height * 0.8; + var idealMinCadence = app.getMinCadence(); + var idealMaxCadence = app.getMaxCadence(); - drawChart(dc); + if (info != null && info.currentCadence != null) { + dc.setColor(0xAAAAAA, Graphics.COLOR_TRANSPARENT); + dc.drawText(width / 2, cadenceY, Graphics.FONT_XTINY, "CADENCE", Graphics.TEXT_JUSTIFY_CENTER); + + correctColor(info.currentCadence, idealMinCadence, idealMaxCadence, dc); + dc.drawText(width / 2, cadenceY + 20, Graphics.FONT_XTINY, info.currentCadence.toString() + " spm", Graphics.TEXT_JUSTIFY_CENTER); + } + } catch (e) { + Logger.logError("AdvancedView", "Error drawing current cadence", e); + } } /** - Functions to continous update the chart with live cadence data. - The chart is split into bars each representing a candence reading, - Each bar data is retrieve from an cadencecadence array which is updated every tick + Functions to continuously update the chart with live cadence data. + The chart is split into bars each representing a cadence reading, + Each bar data is retrieved from a cadence array which is updated every tick Each update the watchUI redraws the chart with the latest data. - } **/ function drawChart(dc as Dc) as Void { - var width = dc.getWidth(); - var height = dc.getHeight(); - - //margins value - var margin = width * 0.1; - var marginLeftRightMultiplier = 1.2; - var marginTopMultiplier = 0.5; - var marginBottomMultiplier = 2; - - //chart position - var chartLeft = margin * marginLeftRightMultiplier; - var chartRight = width - chartLeft; - var chartTop = height * 0.5 + margin * marginTopMultiplier; - var chartBottom = height - margin*marginBottomMultiplier; - var chartWidth = chartRight - chartLeft; - var chartHeight = chartBottom - chartTop; - - // Draw white border around chart (RGB: 255,255,255 = 0xFFFFFF) - dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); - dc.drawRectangle(chartLeft, chartTop, chartWidth, chartHeight); - - // Get data from app - var app = getApp(); - var idealMinCadence = app.getMinCadence(); - var idealMaxCadence = app.getMaxCadence(); - var cadenceHistory = app.getCadenceHistory(); - var cadenceIndex = app.getCadenceIndex(); - var cadenceCount = app.getCadenceCount(); - //check array ?null - if(cadenceCount == 0) {return;} - - // Calculate bar width - var numBars = cadenceCount; - if(numBars == 0) { return; } - var barWidth = chartWidth / MAX_BARS; - - var startIndex = (cadenceIndex - numBars + MAX_BARS) % MAX_BARS; - - // Draw bars - for (var i = 0; i < numBars; i++) { - var index = (startIndex + i) % MAX_BARS; // Start from oldest data - var cadence = cadenceHistory[index]; - if(cadence == null) {cadence = 0;} - - //calculate bar height and position - var barHeight = (cadence / MAX_CADENCE_DISPLAY) * chartHeight; - var x = chartLeft + i * barWidth; - var y = chartBottom - barHeight; - - //seperation between each bar - var barOffset = 1; - correctColor(cadence, idealMinCadence, idealMaxCadence, dc); - dc.fillRectangle(x, y, barWidth-barOffset, barHeight); + try { + var width = dc.getWidth(); + var height = dc.getHeight(); + + //margins value + var margin = width * 0.1; + var marginLeftRightMultiplier = 1.2; + var marginTopMultiplier = 0.5; + var marginBottomMultiplier = 2; + + //chart position + var chartLeft = margin * marginLeftRightMultiplier; + var chartRight = width - chartLeft; + var chartTop = height * 0.5 + margin * marginTopMultiplier; + var chartBottom = height - margin*marginBottomMultiplier; + var chartWidth = chartRight - chartLeft; + var chartHeight = chartBottom - chartTop; + + // Validate chart dimensions + if (chartWidth <= 0 || chartHeight <= 0) { + Logger.log(Logger.WARNING, "AdvancedView", "Invalid chart dimensions"); + return; + } + + // Draw white border around chart + dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); + dc.drawRectangle(chartLeft, chartTop, chartWidth, chartHeight); + + // Get data from app + var app = getApp(); + var idealMinCadence = app.getMinCadence(); + var idealMaxCadence = app.getMaxCadence(); + var cadenceHistory = app.getCadenceHistory(); + var cadenceIndex = app.getCadenceIndex(); + var cadenceCount = app.getCadenceCount(); + + //check array ?null + if(cadenceCount == 0) { + Logger.log(Logger.DEBUG, "AdvancedView", "No cadence data to display"); + return; + } + + // Calculate bar width + var numBars = cadenceCount; + if(numBars == 0) { return; } + var barWidth = chartWidth / MAX_BARS; + + var startIndex = (cadenceIndex - numBars + MAX_BARS) % MAX_BARS; + + // Draw bars + for (var i = 0; i < numBars; i++) { + try { + var index = (startIndex + i) % MAX_BARS; + var cadence = cadenceHistory[index]; + if(cadence == null) {cadence = 0;} + + //calculate bar height and position + var barHeight = (cadence / MAX_CADENCE_DISPLAY) * chartHeight; + var x = chartLeft + i * barWidth; + var y = chartBottom - barHeight; + + //separation between each bar + var barOffset = 1; + correctColor(cadence, idealMinCadence, idealMaxCadence, dc); + dc.fillRectangle(x, y, barWidth-barOffset, barHeight); + } catch (e) { + // Log but continue drawing other bars + if (i == 0) { // Only log first error to avoid spam + Logger.logError("AdvancedView", "Error drawing bar " + i, e); + } + } + } + } catch (e) { + Logger.logError("AdvancedView", "Error in drawChart", e); } } } function correctColor(cadence as Number, idealMinCadence as Number, idealMaxCadence as Number, dc as Dc) as Void{ - if(cadence <= idealMinCadence) - { - dc.setColor(0x0000FF, Graphics.COLOR_TRANSPARENT);//blue - } - else if (cadence >= idealMaxCadence) - { - dc.setColor(0xFF0000, Graphics.COLOR_TRANSPARENT);//red - } - else - { - dc.setColor(0x00FF00, Graphics.COLOR_TRANSPARENT);//green + try { + if(cadence <= idealMinCadence) + { + dc.setColor(0x0000FF, Graphics.COLOR_TRANSPARENT);//blue + } + else if (cadence >= idealMaxCadence) + { + dc.setColor(0xFF0000, Graphics.COLOR_TRANSPARENT);//red + } + else + { + dc.setColor(0x00FF00, Graphics.COLOR_TRANSPARENT);//green + } + } catch (e) { + Logger.logError("AdvancedView", "Error setting color", e); + // Default to white if color setting fails + dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); } -} \ No newline at end of file +} From d544cbe672702d02b610aeb1b3d669b5fed72ace Mon Sep 17 00:00:00 2001 From: Pasta1779 Date: Mon, 15 Dec 2025 23:27:45 +1100 Subject: [PATCH 3/9] created more bugs than i fixed --- source/Logger.mc | 34 ++++++++++++++++++++-------------- source/Views/AdvancedView.mc | 8 ++++---- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/source/Logger.mc b/source/Logger.mc index b784e76..88f2a02 100644 --- a/source/Logger.mc +++ b/source/Logger.mc @@ -215,32 +215,38 @@ module Logger { /** * Log activity data for debugging */ - function logActivityInfo(tag as String, info as Lang.Object) as Void { + function logActivityInfo(tag as String, info) as Void { if (info == null) { log(WARNING, tag, "Activity info is null"); return; } - var activityInfo = info; var msg = "Activity - "; - // Safely access properties + // Directly access properties - Monkey C will handle gracefully try { - if (activityInfo has :currentCadence && activityInfo.currentCadence != null) { - msg += "Cadence:" + activityInfo.currentCadence + " "; + if (info.currentCadence != null) { + msg += "Cadence:" + info.currentCadence + " "; } - if (activityInfo has :currentHeartRate && activityInfo.currentHeartRate != null) { - msg += "HR:" + activityInfo.currentHeartRate + " "; + } catch (e) {} + + try { + if (info.currentHeartRate != null) { + msg += "HR:" + info.currentHeartRate + " "; } - if (activityInfo has :elapsedDistance && activityInfo.elapsedDistance != null) { - msg += "Dist:" + (activityInfo.elapsedDistance / 100000.0).format("%.2f") + "km "; + } catch (e) {} + + try { + if (info.elapsedDistance != null) { + msg += "Dist:" + (info.elapsedDistance / 100000.0).format("%.2f") + "km "; } - if (activityInfo has :timerTime && activityInfo.timerTime != null) { - msg += "Time:" + (activityInfo.timerTime / 1000) + "s"; + } catch (e) {} + + try { + if (info.timerTime != null) { + msg += "Time:" + (info.timerTime / 1000) + "s"; } - } catch (e) { - msg += "Error reading activity info"; - } + } catch (e) {} log(DEBUG, tag, msg); } diff --git a/source/Views/AdvancedView.mc b/source/Views/AdvancedView.mc index 0b07967..8d6fe0e 100644 --- a/source/Views/AdvancedView.mc +++ b/source/Views/AdvancedView.mc @@ -115,7 +115,7 @@ class AdvancedView extends WatchUi.View { } } - function drawElapsedTime(dc as Dc, width as Number, height as Number, info as Lang.Object) as Void { + function drawElapsedTime(dc as Dc, width as Number, height as Number, info) as Void { try { if (info != null && info.timerTime != null) { var seconds = info.timerTime / 1000; @@ -131,7 +131,7 @@ class AdvancedView extends WatchUi.View { } } - function drawHeartRate(dc as Dc, width as Number, height as Number, info as Lang.Object) as Void { + function drawHeartRate(dc as Dc, width as Number, height as Number, info) as Void { try { var hrX = width / 4; var hrY = (height * 2) / 5; @@ -150,7 +150,7 @@ class AdvancedView extends WatchUi.View { } } - function drawDistance(dc as Dc, width as Number, height as Number, info as Lang.Object) as Void { + function drawDistance(dc as Dc, width as Number, height as Number, info) as Void { try { var distX = (width * 3) / 4; var distY = (height * 2) / 5; @@ -186,7 +186,7 @@ class AdvancedView extends WatchUi.View { } } - function drawCurrentCadence(dc as Dc, width as Number, height as Number, info as Lang.Object, app as GarminApp) as Void { + function drawCurrentCadence(dc as Dc, width as Number, height as Number, info, app as GarminApp) as Void { try { var cadenceY = height * 0.8; var idealMinCadence = app.getMinCadence(); From d95e1117d15bd3322c2ff5ad17135d9989305e36 Mon Sep 17 00:00:00 2001 From: Pasta1779 Date: Tue, 16 Dec 2025 00:10:48 +1100 Subject: [PATCH 4/9] removed logginf features, reverted to just memory. Try -catch kept failing --- source/GarminApp.mc | 176 +++------------- source/Logger.mc | 318 +--------------------------- source/Views/AdvancedView.mc | 390 ++++++++++++----------------------- 3 files changed, 178 insertions(+), 706 deletions(-) diff --git a/source/GarminApp.mc b/source/GarminApp.mc index 36b2a9e..e5db05e 100644 --- a/source/GarminApp.mc +++ b/source/GarminApp.mc @@ -12,7 +12,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 minute at 1-second intervals) + private var _cadenceHistory as Array = new [MAX_BARS]; var globalTimer; @@ -22,121 +22,50 @@ class GarminApp extends Application.AppBase { Advanced = 1.04 } - //user info (testing with dummy value rn, implement user profile input later) private var _userHeight = 160; private var _userSpeed = 0; private var _trainingLvl = Beginner; function initialize() { AppBase.initialize(); - - // Initialize the logging system - try { - Logger.initialize(); - Logger.log(Logger.INFO, "GarminApp", "Application initialized"); - Logger.log(Logger.INFO, "GarminApp", "Training level: " + _trainingLvl); - Logger.log(Logger.INFO, "GarminApp", "Cadence range: " + _idealMinCadence + "-" + _idealMaxCadence); - } catch (e) { - // Fallback to System.println if logger fails - System.println("Failed to initialize logger: " + e.getErrorMessage()); - } + System.println("[INFO] App initialized"); } - // onStart() is called on application start up function onStart(state as Dictionary?) as Void { - try { - Logger.log(Logger.INFO, "GarminApp", "Application starting"); - Logger.logMemoryStats("GarminApp"); - - // Initialize and start the global timer - globalTimer = new Timer.Timer(); - globalTimer.start(method(:updateCadence), 1000, true); - - Logger.log(Logger.INFO, "GarminApp", "Global cadence timer started (1s interval)"); - - } catch (e) { - Logger.logCrash("GarminApp", "Failed to start application", e); - // Re-throw critical errors - throw e; - } + System.println("[INFO] App starting"); + + // Log memory on startup + Logger.logMemoryStats("Startup"); + + globalTimer = new Timer.Timer(); + globalTimer.start(method(:updateCadence), 1000, true); } - // onStop() is called when your application is exiting function onStop(state as Dictionary?) as Void { - try { - Logger.log(Logger.INFO, "GarminApp", "Application stopping"); - Logger.log(Logger.INFO, "GarminApp", "Total cadence readings: " + _cadenceCount); - - // Stop and cleanup the global timer - if(globalTimer != null){ - globalTimer.stop(); - globalTimer = null; - Logger.log(Logger.INFO, "GarminApp", "Global timer stopped"); - } - - // Log final memory state - Logger.logMemoryStats("GarminApp"); - - // Ensure all logs are flushed to storage - Logger.shutdown(); - - } catch (e) { - // Even if logging fails, try to output to console - System.println("Error during app stop: " + e.getErrorMessage()); + System.println("[INFO] App stopping"); + + if(globalTimer != null){ + globalTimer.stop(); + globalTimer = null; } + + // Log memory on shutdown + Logger.logMemoryStats("Shutdown"); } - /** - * Update cadence data from activity information - * Called every second by the global timer - */ function updateCadence() as Void { - try { - var info = Activity.getActivityInfo(); - - if (info != null && info.currentCadence != null) { - var newCadence = info.currentCadence; - - // Store in circular buffer - _cadenceHistory[_cadenceIndex] = newCadence.toFloat(); - _cadenceIndex = (_cadenceIndex + 1) % MAX_BARS; - - if (_cadenceCount < MAX_BARS) { - _cadenceCount++; - } - - // Log cadence periodically (every 10 seconds) to avoid log spam - if (_cadenceIndex % 10 == 0) { - Logger.log(Logger.DEBUG, "GarminApp", "Cadence update: " + newCadence + " spm (count: " + _cadenceCount + ")"); - } - - // Log if cadence is outside ideal range - if (newCadence < _idealMinCadence || newCadence > _idealMaxCadence) { - if (_cadenceIndex % 5 == 0) { // Log every 5 seconds when out of range - var status = newCadence < _idealMinCadence ? "below" : "above"; - Logger.log(Logger.WARNING, "GarminApp", "Cadence " + status + " target range: " + newCadence + " spm"); - } - } - - } else { - // Log when activity info is unavailable (but not too frequently) - if (_cadenceIndex % 30 == 0) { // Log every 30 seconds - if (info == null) { - Logger.log(Logger.WARNING, "GarminApp", "Activity info unavailable"); - } else { - Logger.log(Logger.WARNING, "GarminApp", "Current cadence is null"); - } - } - } - - // Periodically log memory stats (every minute) - if (_cadenceIndex % 60 == 0 && _cadenceIndex > 0) { - Logger.logMemoryStats("GarminApp"); - } - - } catch (e) { - Logger.logError("GarminApp", "Error updating cadence", e); - // Don't re-throw - we want the timer to continue + var info = Activity.getActivityInfo(); + + if (info != null && info.currentCadence != null) { + var newCadence = info.currentCadence; + _cadenceHistory[_cadenceIndex] = newCadence.toFloat(); + _cadenceIndex = (_cadenceIndex + 1) % MAX_BARS; + if (_cadenceCount < MAX_BARS) { _cadenceCount++; } + } + + // Log memory every 60 seconds + if (_cadenceIndex % 60 == 0 && _cadenceIndex > 0) { + Logger.logMemoryStats("Runtime"); } } @@ -161,59 +90,18 @@ class GarminApp extends Application.AppBase { } function setMinCadence(value as Number) as Void { - try { - var oldValue = _idealMinCadence; - _idealMinCadence = value; - Logger.log(Logger.INFO, "GarminApp", "Min cadence updated: " + oldValue + " -> " + value); - } catch (e) { - Logger.logError("GarminApp", "Error setting min cadence", e); - // Still try to set the value - _idealMinCadence = value; - } + _idealMinCadence = value; } function setMaxCadence(value as Number) as Void { - try { - var oldValue = _idealMaxCadence; - _idealMaxCadence = value; - Logger.log(Logger.INFO, "GarminApp", "Max cadence updated: " + oldValue + " -> " + value); - } catch (e) { - Logger.logError("GarminApp", "Error setting max cadence", e); - // Still try to set the value - _idealMaxCadence = value; - } + _idealMaxCadence = value; } - // Additional getters for user info (if needed for future features) - function getUserHeight() as Number { - return _userHeight; - } - - function getUserSpeed() as Number { - return _userSpeed; - } - - function getTrainingLevel() as Number { - return _trainingLvl; - } - - // Return the initial view of your application here function getInitialView() as [Views] or [Views, InputDelegates] { - try { - Logger.log(Logger.INFO, "GarminApp", "Loading initial view (SimpleView)"); - return [ new SimpleView(), new SimpleViewDelegate() ]; - } catch (e) { - Logger.logCrash("GarminApp", "Critical: Failed to load initial view", e); - // Re-throw since we can't function without a view - throw e; - } + return [ new SimpleView(), new SimpleViewDelegate() ]; } } -/** - * Global helper function to get the app instance - * This is used throughout the app to access shared state - */ function getApp() as GarminApp { return Application.getApp() as GarminApp; } diff --git a/source/Logger.mc b/source/Logger.mc index 88f2a02..1c97e52 100644 --- a/source/Logger.mc +++ b/source/Logger.mc @@ -1,324 +1,24 @@ -import Toybox.Application.Storage; import Toybox.Lang; import Toybox.System; -import Toybox.Time; -import Toybox.Activity; /** - * Logger module for debugging, system events, and crash logging - * Logs are stored in persistent storage and can be retrieved for analysis + * Simple logger for memory monitoring only */ module Logger { - enum LogLevel { - DEBUG = 0, - INFO = 1, - WARNING = 2, - ERROR = 3, - CRITICAL = 4 - } - - const MAX_LOG_ENTRIES = 100; // Maximum number of log entries to keep - const LOG_STORAGE_KEY = "app_logs"; - const CRASH_LOG_KEY = "crash_logs"; - const SESSION_START_KEY = "session_start"; - - var currentSession as String = ""; - var logBuffer as Array = []; - var logCount = 0; - - /** - * Initialize the logger system - */ - function initialize() as Void { - currentSession = generateSessionId(); - logBuffer = []; - logCount = 0; - - // Log session start - log(INFO, "Logger", "Session started: " + currentSession); - logSystemInfo(); - - // Store session start time - Storage.setValue(SESSION_START_KEY, Time.now().value()); - } - - /** - * Generate a unique session identifier - */ - function generateSessionId() as String { - var now = Time.now(); - var info = Time.Gregorian.info(now, Time.FORMAT_MEDIUM); - return info.year.format("%04d") + info.month.format("%02d") + info.day.format("%02d") + - "_" + info.hour.format("%02d") + info.min.format("%02d") + info.sec.format("%02d"); - } - /** - * Log system information at startup - */ - function logSystemInfo() as Void { - var stats = System.getSystemStats(); - var deviceSettings = System.getDeviceSettings(); - - log(INFO, "System", "Battery: " + stats.battery + "%"); - log(INFO, "System", "Memory: " + stats.totalMemory + " bytes"); - log(INFO, "System", "Free Memory: " + stats.freeMemory + " bytes"); - log(INFO, "System", "Device: " + deviceSettings.partNumber); - log(INFO, "System", "FW Version: " + deviceSettings.firmwareVersion); - } - - /** - * Main logging function - * @param level - Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) - * @param tag - Category or component name - * @param message - Log message - */ - function log(level as LogLevel, tag as String, message as String) as Void { - try { - var timestamp = Time.now().value(); - var levelStr = getLevelString(level); - var logEntry = timestamp + "|" + levelStr + "|" + tag + "|" + message; - - // Add to buffer - logBuffer.add(logEntry); - logCount++; - - // If buffer is full, flush to storage - if (logBuffer.size() >= 10) { - flushLogs(); - } - - // Also output to system for real-time debugging - System.println("[" + levelStr + "] " + tag + ": " + message); - - } catch (e) { - System.println("Logger error: " + e.getErrorMessage()); - } - } - - /** - * Convert log level to string - */ - function getLevelString(level as LogLevel) as String { - if (level == DEBUG) { return "DEBUG"; } - else if (level == INFO) { return "INFO"; } - else if (level == WARNING) { return "WARN"; } - else if (level == ERROR) { return "ERROR"; } - else if (level == CRITICAL) { return "CRIT"; } - return "UNKNOWN"; - } - - /** - * Flush buffered logs to persistent storage - */ - function flushLogs() as Void { - try { - if (logBuffer.size() == 0) { - return; - } - - // Get existing logs - var existingLogs = Storage.getValue(LOG_STORAGE_KEY); - var allLogs = []; - - if (existingLogs != null && existingLogs instanceof Array) { - allLogs = existingLogs as Array; - } - - // Add new logs - for (var i = 0; i < logBuffer.size(); i++) { - allLogs.add(logBuffer[i]); - } - - // Trim if too many entries - while (allLogs.size() > MAX_LOG_ENTRIES) { - allLogs = allLogs.slice(1, allLogs.size()); - } - - // Save to storage - Storage.setValue(LOG_STORAGE_KEY, allLogs); - - // Clear buffer - logBuffer = []; - - } catch (e) { - System.println("Failed to flush logs: " + e.getErrorMessage()); - } - } - - /** - * Log an error with exception details - */ - function logError(tag as String, message as String, exception as Exception) as Void { - var errorMsg = message + " - " + exception.getErrorMessage(); - log(ERROR, tag, errorMsg); - flushLogs(); // Immediately flush errors - } - - /** - * Log a crash with full context + * Log memory statistics */ - function logCrash(tag as String, message as String, exception as Exception) as Void { + function logMemoryStats(tag as String) as Void { try { - var timestamp = Time.now().value(); var stats = System.getSystemStats(); + var usedMemory = stats.totalMemory - stats.freeMemory; + var memoryPercent = (usedMemory.toFloat() / stats.totalMemory.toFloat() * 100).toNumber(); - var crashData = { - "timestamp" => timestamp, - "session" => currentSession, - "tag" => tag, - "message" => message, - "error" => exception.getErrorMessage(), - "battery" => stats.battery, - "freeMemory" => stats.freeMemory, - "totalMemory" => stats.totalMemory - }; - - // Get existing crash logs - var crashLogs = Storage.getValue(CRASH_LOG_KEY); - var allCrashes = []; - - if (crashLogs != null && crashLogs instanceof Array) { - allCrashes = crashLogs as Array; - } - - allCrashes.add(crashData); - - // Keep last 20 crashes - while (allCrashes.size() > 20) { - allCrashes = allCrashes.slice(1, allCrashes.size()); - } - - Storage.setValue(CRASH_LOG_KEY, allCrashes); - - // Also log normally - log(CRITICAL, tag, "CRASH: " + message + " - " + exception.getErrorMessage()); - flushLogs(); - + System.println("[MEMORY] " + tag + ": " + usedMemory + "/" + stats.totalMemory + + " bytes (" + memoryPercent + "% used)"); } catch (e) { - System.println("Failed to log crash: " + e.getErrorMessage()); - } - } - - /** - * Log memory statistics - */ - function logMemoryStats(tag as String) as Void { - var stats = System.getSystemStats(); - var usedMemory = stats.totalMemory - stats.freeMemory; - var memoryPercent = (usedMemory.toFloat() / stats.totalMemory.toFloat() * 100).toNumber(); - - log(INFO, tag, "Memory: " + usedMemory + "/" + stats.totalMemory + - " (" + memoryPercent + "% used)"); - } - - /** - * Log activity data for debugging - */ - function logActivityInfo(tag as String, info) as Void { - if (info == null) { - log(WARNING, tag, "Activity info is null"); - return; - } - - var msg = "Activity - "; - - // Directly access properties - Monkey C will handle gracefully - try { - if (info.currentCadence != null) { - msg += "Cadence:" + info.currentCadence + " "; - } - } catch (e) {} - - try { - if (info.currentHeartRate != null) { - msg += "HR:" + info.currentHeartRate + " "; - } - } catch (e) {} - - try { - if (info.elapsedDistance != null) { - msg += "Dist:" + (info.elapsedDistance / 100000.0).format("%.2f") + "km "; - } - } catch (e) {} - - try { - if (info.timerTime != null) { - msg += "Time:" + (info.timerTime / 1000) + "s"; - } - } catch (e) {} - - log(DEBUG, tag, msg); - } - - /** - * Get all logs as formatted string for display or export - */ - function getLogsAsString() as String { - flushLogs(); // Ensure all logs are saved - - var logs = Storage.getValue(LOG_STORAGE_KEY); - if (logs == null || !(logs instanceof Array)) { - return "No logs available"; - } - - var result = "=== APP LOGS ===\n"; - var logArray = logs as Array; - - for (var i = 0; i < logArray.size(); i++) { - result += logArray[i] + "\n"; + System.println("[ERROR] Failed to log memory stats: " + e.getErrorMessage()); } - - return result; - } - - /** - * Get crash logs as formatted string - */ - function getCrashLogsAsString() as String { - var crashes = Storage.getValue(CRASH_LOG_KEY); - if (crashes == null || !(crashes instanceof Array)) { - return "No crash logs available"; - } - - var result = "=== CRASH LOGS ===\n"; - var crashArray = crashes as Array; - - for (var i = 0; i < crashArray.size(); i++) { - var crash = crashArray[i]; - if (crash instanceof Dictionary) { - var crashDict = crash as Dictionary; - result += "Crash " + (i + 1) + ":\n"; - result += " Time: " + crashDict["timestamp"] + "\n"; - result += " Session: " + crashDict["session"] + "\n"; - result += " Tag: " + crashDict["tag"] + "\n"; - result += " Message: " + crashDict["message"] + "\n"; - result += " Error: " + crashDict["error"] + "\n"; - result += " Battery: " + crashDict["battery"] + "%\n"; - result += " Memory: " + crashDict["freeMemory"] + "/" + - crashDict["totalMemory"] + "\n\n"; - } - } - - return result; - } - - /** - * Clear all logs - */ - function clearLogs() as Void { - Storage.deleteValue(LOG_STORAGE_KEY); - Storage.deleteValue(CRASH_LOG_KEY); - logBuffer = []; - logCount = 0; - log(INFO, "Logger", "Logs cleared"); - } - - /** - * Shutdown logger and ensure all logs are saved - */ - function shutdown() as Void { - log(INFO, "Logger", "Session ended: " + currentSession); - flushLogs(); } -} +} \ No newline at end of file diff --git a/source/Views/AdvancedView.mc b/source/Views/AdvancedView.mc index 8d6fe0e..f16e20f 100644 --- a/source/Views/AdvancedView.mc +++ b/source/Views/AdvancedView.mc @@ -13,299 +13,183 @@ class AdvancedView extends WatchUi.View { const STEP_RATE = 6; private var _simulationTimer; - private var _updateCount = 0; function initialize() { View.initialize(); - Logger.log(Logger.INFO, "AdvancedView", "View initialized"); } function onShow() as Void { - try { - Logger.log(Logger.INFO, "AdvancedView", "View shown"); - _simulationTimer = new Timer.Timer(); - _simulationTimer.start(method(:refreshScreen), 1000, true); - _updateCount = 0; - } catch (e) { - Logger.logError("AdvancedView", "Error in onShow", e); - } + _simulationTimer = new Timer.Timer(); + _simulationTimer.start(method(:refreshScreen), 1000, true); } function onHide() as Void { - try { - Logger.log(Logger.INFO, "AdvancedView", "View hidden (updates: " + _updateCount + ")"); - if (_simulationTimer != null) { - _simulationTimer.stop(); - _simulationTimer = null; - } - } catch (e) { - Logger.logError("AdvancedView", "Error in onHide", e); + if (_simulationTimer != null) { + _simulationTimer.stop(); + _simulationTimer = null; } } function onUpdate(dc as Dc) as Void { - try { - View.onUpdate(dc); - drawElements(dc); - _updateCount++; - - // Log memory periodically - if (_updateCount % 30 == 0) { - Logger.logMemoryStats("AdvancedView"); - } - } catch (e) { - Logger.logCrash("AdvancedView", "Critical error in onUpdate", e); - // Try to show error message to user - try { - dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_BLACK); - dc.clear(); - dc.drawText(dc.getWidth()/2, dc.getHeight()/2, - Graphics.FONT_SMALL, "Display Error", Graphics.TEXT_JUSTIFY_CENTER); - } catch (ex) { - // If even error display fails, just log it - System.println("Failed to display error: " + ex.getErrorMessage()); - } - } + View.onUpdate(dc); + // Draw all the elements + drawElements(dc); } function refreshScreen() as Void { - try { - WatchUi.requestUpdate(); - } catch (e) { - Logger.logError("AdvancedView", "Error requesting update", e); - } + WatchUi.requestUpdate(); } function drawElements(dc as Dc) as Void { - try { - var width = dc.getWidth(); - var height = dc.getHeight(); - var info = Activity.getActivityInfo(); - var app = getApp(); - - // Validate dimensions - if (width <= 0 || height <= 0) { - Logger.log(Logger.ERROR, "AdvancedView", "Invalid display dimensions: " + width + "x" + height); - return; - } - - // Draw elapsed time at top - drawElapsedTime(dc, width, height, info); - - // Draw heart rate circle (left) - drawHeartRate(dc, width, height, info); - - // Draw distance circle (right) - drawDistance(dc, width, height, info); - - // Draw ideal cadence range - drawIdealCadenceRange(dc, width, height, app); - - // Draw current cadence - drawCurrentCadence(dc, width, height, info, app); - - // Draw chart - drawChart(dc); - - } catch (e) { - Logger.logCrash("AdvancedView", "Error in drawElements", e); - throw e; // Re-throw to trigger onUpdate error handling + var width = dc.getWidth(); + var height = dc.getHeight(); + var info = Activity.getActivityInfo(); + var app = getApp(); + + // Draw elapsed time at top (yellow RGB: 255,248,18 = 0xFFF, using picker in paint to get RGB then convert to hex + if (info != null && info.timerTime != null) { + var seconds = info.timerTime / 1000; + var hours = seconds / 3600; + var minutes = (seconds % 3600) / 60; + var secs = seconds % 60; + var timeStr = hours.format("%01d") + ":" + minutes.format("%02d") + "." + secs.format("%02d"); + dc.setColor(0xFFF813, Graphics.COLOR_TRANSPARENT); + dc.drawText(width / 2, 3, Graphics.FONT_LARGE, timeStr, Graphics.TEXT_JUSTIFY_CENTER); } - } - - function drawElapsedTime(dc as Dc, width as Number, height as Number, info) as Void { - try { - if (info != null && info.timerTime != null) { - var seconds = info.timerTime / 1000; - var hours = seconds / 3600; - var minutes = (seconds % 3600) / 60; - var secs = seconds % 60; - var timeStr = hours.format("%01d") + ":" + minutes.format("%02d") + "." + secs.format("%02d"); - dc.setColor(0xFFF813, Graphics.COLOR_TRANSPARENT); - dc.drawText(width / 2, 3, Graphics.FONT_LARGE, timeStr, Graphics.TEXT_JUSTIFY_CENTER); - } - } catch (e) { - Logger.logError("AdvancedView", "Error drawing elapsed time", e); + + // Draw heart rate circle (left, dark red RGB: 211,19,2519 + var hrX = width / 4; + var hrY = (height * 2) / 5; + var circleRadius = 42; + + dc.setColor(0x9D0000, Graphics.COLOR_TRANSPARENT); + dc.fillCircle(hrX, hrY, circleRadius); + + if (info != null && info.currentHeartRate != null) { + dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); // White RGB: 255,255,255 + dc.drawText(hrX, hrY - 25, Graphics.FONT_TINY, info.currentHeartRate.toString(), Graphics.TEXT_JUSTIFY_CENTER); + dc.drawText(hrX, hrY + 8, Graphics.FONT_XTINY, "bpm", Graphics.TEXT_JUSTIFY_CENTER); } - } - - function drawHeartRate(dc as Dc, width as Number, height as Number, info) as Void { - try { - var hrX = width / 4; - var hrY = (height * 2) / 5; - var circleRadius = 42; - - dc.setColor(0x9D0000, Graphics.COLOR_TRANSPARENT); - dc.fillCircle(hrX, hrY, circleRadius); - - if (info != null && info.currentHeartRate != null) { - dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); - dc.drawText(hrX, hrY - 25, Graphics.FONT_TINY, info.currentHeartRate.toString(), Graphics.TEXT_JUSTIFY_CENTER); - dc.drawText(hrX, hrY + 8, Graphics.FONT_XTINY, "bpm", Graphics.TEXT_JUSTIFY_CENTER); - } - } catch (e) { - Logger.logError("AdvancedView", "Error drawing heart rate", e); + + // Draw distance circle (right, dark green RGB: 24,19,24 = 0x1D5E11) + var distX = (width * 3) / 4; + var distY = hrY; + + dc.setColor(0x1D5E11, Graphics.COLOR_TRANSPARENT); + dc.fillCircle(distX, distY, circleRadius); + + if (info != null && info.elapsedDistance != null) { + var distanceKm = info.elapsedDistance / 100000.0; + dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); // White RGB: 255,255,255 + dc.drawText(distX, distY - 25, Graphics.FONT_TINY, distanceKm.format("%.2f"), Graphics.TEXT_JUSTIFY_CENTER); + dc.drawText(distX, distY + 8, Graphics.FONT_XTINY, "km", Graphics.TEXT_JUSTIFY_CENTER); } - } - function drawDistance(dc as Dc, width as Number, height as Number, info) as Void { - try { - var distX = (width * 3) / 4; - var distY = (height * 2) / 5; - var circleRadius = 42; - - dc.setColor(0x1D5E11, Graphics.COLOR_TRANSPARENT); - dc.fillCircle(distX, distY, circleRadius); - - if (info != null && info.elapsedDistance != null) { - var distanceKm = info.elapsedDistance / 100000.0; - dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); - dc.drawText(distX, distY - 25, Graphics.FONT_TINY, distanceKm.format("%.2f"), Graphics.TEXT_JUSTIFY_CENTER); - dc.drawText(distX, distY + 8, Graphics.FONT_XTINY, "km", Graphics.TEXT_JUSTIFY_CENTER); - } - } catch (e) { - Logger.logError("AdvancedView", "Error drawing distance", e); - } - } + //draw ideal cadence range + var idealMinCadence = app.getMinCadence(); + var idealMaxCadence = app.getMaxCadence(); + var idealCadenceY = height * 0.45; + - function drawIdealCadenceRange(dc as Dc, width as Number, height as Number, app as GarminApp) as Void { - try { - var idealMinCadence = app.getMinCadence(); - var idealMaxCadence = app.getMaxCadence(); - var idealCadenceY = height * 0.45; - - if(idealMinCadence != null && idealMaxCadence != null){ - var displayString = (idealMinCadence + " - " + idealMaxCadence).toString(); - dc.setColor(0xAAAAAA, Graphics.COLOR_TRANSPARENT); - dc.drawText(width / 2, idealCadenceY, Graphics.FONT_XTINY, displayString, Graphics.TEXT_JUSTIFY_CENTER); - } - } catch (e) { - Logger.logError("AdvancedView", "Error drawing ideal cadence range", e); + if(idealMinCadence != null && idealMaxCadence != null){ + var displayString = (idealMinCadence + " - " + idealMaxCadence).toString(); + dc.setColor(0xAAAAAA, Graphics.COLOR_TRANSPARENT); + dc.drawText(width / 2,idealCadenceY , Graphics.FONT_XTINY, displayString, Graphics.TEXT_JUSTIFY_CENTER); } - } - function drawCurrentCadence(dc as Dc, width as Number, height as Number, info, app as GarminApp) as Void { - try { - var cadenceY = height * 0.8; - var idealMinCadence = app.getMinCadence(); - var idealMaxCadence = app.getMaxCadence(); + var cadenceY = height * 0.8; - if (info != null && info.currentCadence != null) { - dc.setColor(0xAAAAAA, Graphics.COLOR_TRANSPARENT); - dc.drawText(width / 2, cadenceY, Graphics.FONT_XTINY, "CADENCE", Graphics.TEXT_JUSTIFY_CENTER); - - correctColor(info.currentCadence, idealMinCadence, idealMaxCadence, dc); - dc.drawText(width / 2, cadenceY + 20, Graphics.FONT_XTINY, info.currentCadence.toString() + " spm", Graphics.TEXT_JUSTIFY_CENTER); - } - } catch (e) { - Logger.logError("AdvancedView", "Error drawing current cadence", e); + if (info != null && info.currentCadence != null) { + // Draw "CADENCE" label (light gray RGB: 170,170,170 = 0xAAAAAA) + dc.setColor(0xAAAAAA, Graphics.COLOR_TRANSPARENT); + dc.drawText(width / 2, cadenceY, Graphics.FONT_XTINY, "CADENCE", Graphics.TEXT_JUSTIFY_CENTER); + + // Draw cadence value in green (RGB: 0,255,0 = 0x00FF00) + correctColor(info.currentCadence, idealMinCadence, idealMaxCadence, dc); + dc.drawText(width / 2, cadenceY + 20, Graphics.FONT_XTINY, info.currentCadence.toString() + " spm", Graphics.TEXT_JUSTIFY_CENTER); } + + drawChart(dc); } /** - Functions to continuously update the chart with live cadence data. - The chart is split into bars each representing a cadence reading, - Each bar data is retrieved from a cadence array which is updated every tick + Functions to continous update the chart with live cadence data. + The chart is split into bars each representing a candence reading, + Each bar data is retrieve from an cadencecadence array which is updated every tick Each update the watchUI redraws the chart with the latest data. + } **/ function drawChart(dc as Dc) as Void { - try { - var width = dc.getWidth(); - var height = dc.getHeight(); - - //margins value - var margin = width * 0.1; - var marginLeftRightMultiplier = 1.2; - var marginTopMultiplier = 0.5; - var marginBottomMultiplier = 2; - - //chart position - var chartLeft = margin * marginLeftRightMultiplier; - var chartRight = width - chartLeft; - var chartTop = height * 0.5 + margin * marginTopMultiplier; - var chartBottom = height - margin*marginBottomMultiplier; - var chartWidth = chartRight - chartLeft; - var chartHeight = chartBottom - chartTop; - - // Validate chart dimensions - if (chartWidth <= 0 || chartHeight <= 0) { - Logger.log(Logger.WARNING, "AdvancedView", "Invalid chart dimensions"); - return; - } - - // Draw white border around chart - dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); - dc.drawRectangle(chartLeft, chartTop, chartWidth, chartHeight); - - // Get data from app - var app = getApp(); - var idealMinCadence = app.getMinCadence(); - var idealMaxCadence = app.getMaxCadence(); - var cadenceHistory = app.getCadenceHistory(); - var cadenceIndex = app.getCadenceIndex(); - var cadenceCount = app.getCadenceCount(); - - //check array ?null - if(cadenceCount == 0) { - Logger.log(Logger.DEBUG, "AdvancedView", "No cadence data to display"); - return; - } - - // Calculate bar width - var numBars = cadenceCount; - if(numBars == 0) { return; } - var barWidth = chartWidth / MAX_BARS; - - var startIndex = (cadenceIndex - numBars + MAX_BARS) % MAX_BARS; - - // Draw bars - for (var i = 0; i < numBars; i++) { - try { - var index = (startIndex + i) % MAX_BARS; - var cadence = cadenceHistory[index]; - if(cadence == null) {cadence = 0;} - - //calculate bar height and position - var barHeight = (cadence / MAX_CADENCE_DISPLAY) * chartHeight; - var x = chartLeft + i * barWidth; - var y = chartBottom - barHeight; - - //separation between each bar - var barOffset = 1; - correctColor(cadence, idealMinCadence, idealMaxCadence, dc); - dc.fillRectangle(x, y, barWidth-barOffset, barHeight); - } catch (e) { - // Log but continue drawing other bars - if (i == 0) { // Only log first error to avoid spam - Logger.logError("AdvancedView", "Error drawing bar " + i, e); - } - } - } - } catch (e) { - Logger.logError("AdvancedView", "Error in drawChart", e); + var width = dc.getWidth(); + var height = dc.getHeight(); + + //margins value + var margin = width * 0.1; + var marginLeftRightMultiplier = 1.2; + var marginTopMultiplier = 0.5; + var marginBottomMultiplier = 2; + + //chart position + var chartLeft = margin * marginLeftRightMultiplier; + var chartRight = width - chartLeft; + var chartTop = height * 0.5 + margin * marginTopMultiplier; + var chartBottom = height - margin*marginBottomMultiplier; + var chartWidth = chartRight - chartLeft; + var chartHeight = chartBottom - chartTop; + + // Draw white border around chart (RGB: 255,255,255 = 0xFFFFFF) + dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); + dc.drawRectangle(chartLeft, chartTop, chartWidth, chartHeight); + + // Get data from app + var app = getApp(); + var idealMinCadence = app.getMinCadence(); + var idealMaxCadence = app.getMaxCadence(); + var cadenceHistory = app.getCadenceHistory(); + var cadenceIndex = app.getCadenceIndex(); + var cadenceCount = app.getCadenceCount(); + //check array ?null + if(cadenceCount == 0) {return;} + + // Calculate bar width + var numBars = cadenceCount; + if(numBars == 0) { return; } + var barWidth = chartWidth / MAX_BARS; + + var startIndex = (cadenceIndex - numBars + MAX_BARS) % MAX_BARS; + + // Draw bars + for (var i = 0; i < numBars; i++) { + var index = (startIndex + i) % MAX_BARS; // Start from oldest data + var cadence = cadenceHistory[index]; + if(cadence == null) {cadence = 0;} + + //calculate bar height and position + var barHeight = (cadence / MAX_CADENCE_DISPLAY) * chartHeight; + var x = chartLeft + i * barWidth; + var y = chartBottom - barHeight; + + //seperation between each bar + var barOffset = 1; + correctColor(cadence, idealMinCadence, idealMaxCadence, dc); + dc.fillRectangle(x, y, barWidth-barOffset, barHeight); } } } function correctColor(cadence as Number, idealMinCadence as Number, idealMaxCadence as Number, dc as Dc) as Void{ - try { - if(cadence <= idealMinCadence) - { - dc.setColor(0x0000FF, Graphics.COLOR_TRANSPARENT);//blue - } - else if (cadence >= idealMaxCadence) - { - dc.setColor(0xFF0000, Graphics.COLOR_TRANSPARENT);//red - } - else - { - dc.setColor(0x00FF00, Graphics.COLOR_TRANSPARENT);//green - } - } catch (e) { - Logger.logError("AdvancedView", "Error setting color", e); - // Default to white if color setting fails - dc.setColor(0xFFFFFF, Graphics.COLOR_TRANSPARENT); + if(cadence <= idealMinCadence) + { + dc.setColor(0x0000FF, Graphics.COLOR_TRANSPARENT);//blue + } + else if (cadence >= idealMaxCadence) + { + dc.setColor(0xFF0000, Graphics.COLOR_TRANSPARENT);//red + } + else + { + dc.setColor(0x00FF00, Graphics.COLOR_TRANSPARENT);//green } } From e821a3b293634b477fb1ed29006ebbb7ec83d3a5 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 3 Jan 2026 23:00:28 +1100 Subject: [PATCH 5/9] added in workout start/stop and save /discard. --- source/Delegates/SimpleViewDelegate.mc | 81 +++++++++++++++++++++++++- source/GarminApp.mc | 62 ++++++++++++++++++++ source/Views/SimpleView.mc | 26 +++++++++ 3 files changed, 168 insertions(+), 1 deletion(-) 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 e5db05e..24fbbf2 100644 --- a/source/GarminApp.mc +++ b/source/GarminApp.mc @@ -4,6 +4,7 @@ import Toybox.WatchUi; import Toybox.Timer; import Toybox.Activity; import Toybox.System; +import Toybox.ActivityRecording; class GarminApp extends Application.AppBase { const MAX_BARS = 60; @@ -15,6 +16,8 @@ class GarminApp extends Application.AppBase { private var _cadenceHistory as Array = new [MAX_BARS]; var globalTimer; + var session as ActivityRecording.Session?; + var isRecording as Boolean = false; enum { Beginner = 0.96, @@ -44,6 +47,11 @@ class GarminApp extends Application.AppBase { 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; @@ -53,6 +61,60 @@ class GarminApp extends Application.AppBase { 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(); 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(); From ce4f5d5a53e51c019d56abd944312d2e0d4023ed Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Jan 2026 11:18:30 +1100 Subject: [PATCH 6/9] Merge feature/system-logging into main --- source/GarminApp.mc | 119 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 107 insertions(+), 12 deletions(-) diff --git a/source/GarminApp.mc b/source/GarminApp.mc index 24fbbf2..26ff258 100644 --- a/source/GarminApp.mc +++ b/source/GarminApp.mc @@ -8,6 +8,24 @@ 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, + Intermediate = 1.04, + Advanced = 1.02 + } + + enum { + Male, + Female, + Other + } private var _idealMinCadence = 80; private var _idealMaxCadence = 100; @@ -15,20 +33,18 @@ class GarminApp extends Application.AppBase { private var _cadenceCount = 0; private var _cadenceHistory as Array = new [MAX_BARS]; - var globalTimer; - var session as ActivityRecording.Session?; - var isRecording as Boolean = false; + private var _userHeight = null;//>>cm + private var _userSpeed = null;//>>m/s + private var _experienceLvl = null; + private var _userGender = null; - enum { - Beginner = 0.96, - Intermediate = 1, - Advanced = 1.04 + function dummyValueTesting() as Void { + _userHeight = 170; + _userSpeed = 3.8; + _experienceLvl = Beginner; + _userGender = Female; } - private var _userHeight = 160; - private var _userSpeed = 0; - private var _trainingLvl = Beginner; - function initialize() { AppBase.initialize(); System.println("[INFO] App initialized"); @@ -42,6 +58,11 @@ class GarminApp extends Application.AppBase { globalTimer = new Timer.Timer(); globalTimer.start(method(:updateCadence), 1000, true); + dummyValueTesting(); + /* + remember to remove after testing + */ + idealCadenceCalculator(); } function onStop(state as Dictionary?) as Void { @@ -131,6 +152,38 @@ class GarminApp extends Application.AppBase { } } + 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 getMinCadence() as Number { return _idealMinCadence; } @@ -159,6 +212,48 @@ class GarminApp extends Application.AppBase { _idealMaxCadence = value; } + function getUserGender() as String { + return _userGender; + } + + function setUserGender(value as String) as Void { + _userGender = value; + } + + function getUserLegLength() as Float { + return _userHeight * 0.53; + } + + function setUserHeight(value as Number) as Void { + _userHeight = value; + } + + function getUserSpeed() as Float { + return _userSpeed; + } + + function setUserSpeed(value as Float) as Void { + _userSpeed = value; + } + + function getExperienceLvl() as Number { + return _experienceLvl; + } + + //double check ltr + function setExperienceLvl(value as Number) as Void { + _experienceLvl = value; + } + + function min(a,b){ + return (a < b) ? a : b; + } + + function max(a,b){ + return (a > b) ? a : b; + } + + // Return the initial view of your application here function getInitialView() as [Views] or [Views, InputDelegates] { return [ new SimpleView(), new SimpleViewDelegate() ]; } @@ -166,4 +261,4 @@ class GarminApp extends Application.AppBase { function getApp() as GarminApp { return Application.getApp() as GarminApp; -} +} \ No newline at end of file From 7ff963f66f71d232f4e7469beaac77b0da48c2ac Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Jan 2026 11:23:48 +1100 Subject: [PATCH 7/9] Resolve merge conflicts with main --- manifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manifest.xml b/manifest.xml index 594fc80..8d2c269 100644 --- a/manifest.xml +++ b/manifest.xml @@ -24,6 +24,8 @@ + + From e111bf7fce99ee51699541fc7335a548e875ebf0 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Jan 2026 11:31:01 +1100 Subject: [PATCH 8/9] Resolve merge conflicts with miann --- source/GarminApp.mc | 1 - 1 file changed, 1 deletion(-) diff --git a/source/GarminApp.mc b/source/GarminApp.mc index 26ff258..70ecca0 100644 --- a/source/GarminApp.mc +++ b/source/GarminApp.mc @@ -183,7 +183,6 @@ class GarminApp extends Application.AppBase { _idealMinCadence = finalCadence - 5; } - function getMinCadence() as Number { return _idealMinCadence; } From 3bc35c1e8884259550eaf76b258f224680a97b3b Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Jan 2026 11:36:15 +1100 Subject: [PATCH 9/9] Resolve merge conflicts with main --- source/GarminApp.mc | 3 --- 1 file changed, 3 deletions(-) diff --git a/source/GarminApp.mc b/source/GarminApp.mc index 70ecca0..a7967db 100644 --- a/source/GarminApp.mc +++ b/source/GarminApp.mc @@ -157,7 +157,6 @@ class GarminApp extends Application.AppBase { var finalCadence = 0; var userLegLength = _userHeight * 0.53; - //reference cadence switch (_userGender) { case Male: @@ -239,7 +238,6 @@ class GarminApp extends Application.AppBase { return _experienceLvl; } - //double check ltr function setExperienceLvl(value as Number) as Void { _experienceLvl = value; } @@ -252,7 +250,6 @@ class GarminApp extends Application.AppBase { return (a > b) ? a : b; } - // Return the initial view of your application here function getInitialView() as [Views] or [Views, InputDelegates] { return [ new SimpleView(), new SimpleViewDelegate() ]; }