From bd077aa010e3e518ae1c347782ade379890a20e2 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Sun, 14 Sep 2025 20:34:45 -0400 Subject: [PATCH 01/40] Add sleep summary logging and notification functions --- apps/sleepsummary/boot.js | 92 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 apps/sleepsummary/boot.js diff --git a/apps/sleepsummary/boot.js b/apps/sleepsummary/boot.js new file mode 100644 index 0000000000..5e3f119a64 --- /dev/null +++ b/apps/sleepsummary/boot.js @@ -0,0 +1,92 @@ +{ + function formatTime(hours) { + let h = Math.floor(hours); // whole hours + let m = Math.round((hours - h) * 60); // leftover minutes + + // handle rounding like 1.9999 → 2h 0m + if (m === 60) { + h += 1; + m = 0; + } + + if (h > 0 && m > 0) return h + "h " + m + "m"; + if (h > 0) return h + "h"; + return m + "m"; + } + + + function logNow() { + let filename="sleepsummarylog.json"; + let storage = require("Storage"); + + // load existing log (or empty array if file doesn't exist) + let log = storage.readJSON(filename,1) || []; + + // get human-readable time + let d = new Date(); + let timeStr = d.getFullYear() + "-" + + ("0"+(d.getMonth()+1)).slice(-2) + "-" + + ("0"+d.getDate()).slice(-2) + " " + + ("0"+d.getHours()).slice(-2) + ":" + + ("0"+d.getMinutes()).slice(-2) + ":" + + ("0"+d.getSeconds()).slice(-2); + + // push new entry + log.push(timeStr); + + // keep file from growing forever + if (log.length > 200) log = log.slice(-200); + + // save back + storage.writeJSON(filename, log); + } + + let showSummary=function(){ + var sleepData=require("sleepsummary").getSleepData(); + var sleepScore=require("sleepsummary").getSleepScores().overallSleepScore; + //sleepData.consecSleep + var message="You slept for "+ formatTime(sleepData.consecSleep/60) +", with a sleep score of "+sleepScore; + + E.showPrompt(message, { + title: "Good Morning!", + buttons: { "Dismiss": 1, "Open App":2}, + + }).then(function (answer) { + if(answer==1){ + Bangle.load(); + }else{ + load("sleepsummary.app.js"); + } + }); + } + + function checkIfAwake(){ + let today = new Date().getDay(); + if(require("sleepsummary").getSummaryData().promptLastShownDay!=today){ + var settings=getSettings(); + var awakeSince=global.sleeplog.info.awakeSince; + if(awakeSince+settings.timeSinceAwake Date: Sun, 14 Sep 2025 20:35:02 -0400 Subject: [PATCH 02/40] Create module.js --- apps/sleepsummary/module.js | 188 ++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 apps/sleepsummary/module.js diff --git a/apps/sleepsummary/module.js b/apps/sleepsummary/module.js new file mode 100644 index 0000000000..8403b0ae77 --- /dev/null +++ b/apps/sleepsummary/module.js @@ -0,0 +1,188 @@ +{ + let getMsPastMidnight=function(unixTimestamp) { + const millisecondsSinceEpoch = unixTimestamp * 1000; + const dateObject = new Date(millisecondsSinceEpoch); + + const hours = dateObject.getHours(); + const minutes = dateObject.getMinutes(); + const seconds = dateObject.getSeconds(); + const milliseconds = dateObject.getMilliseconds(); + + const msPastMidnight = (hours * 3600000) + (minutes * 60000) + (seconds * 1000) + milliseconds; + return msPastMidnight; + }; + + let averageNumbers=function(runningAvg,totalCycles,newData){ + return ((runningAvg*totalCycles)+newData)/(totalCycles+1); + + }; + + let getData=function(){ + return Object.assign({ + + avgSleepTime: 0, + totalCycles:0, + avgWakeUpTime:0, + promptLastShownDay:"" + + }, require('Storage').readJSON("sleepsummarydata.json", true) || {}); + }; + + let getSettings=function() { + return Object.assign({ + useTrueSleep:true, + timeSinceAwake: 1800000, + showMessage:true, + deepSleepHours:5, + idealSleepHours:10, + + }, require('Storage').readJSON("sleepsummary.settings.json", true) || {}); + }; + + let writeData=function(data){ + require("Storage").writeJSON("sleepsummarydata.json", data); + + }; + + let deleteData=function(){ + require("Storage").erase("sleepsummarydata.json"); + }; + + let getSleepData=function(){ + var data=require("sleeplog").getStats(Date.now(), 24*60*60*1000); + var totalSleep=data.consecSleep; + if(getSettings().useTrueSleep) totalSleep=data.deepSleep+data.lightSleep; + + return { calculatedAt: data.calculatedAt, + deepSleep: data.deepSleep, + lightSleep: data.lightSleep, + awakeSleep: data.awakeSleep, + consecSleep: data.consecSleep, + awakeTime: data.awakeTime, + notWornTime: data.notWornTime, + unknownTime: data.unknownTime, + logDuration: data.logDuration, + firstDate: data.firstDate, + lastDate: data.lastDate, + totalSleep: totalSleep, + awakeSince:global.sleeplog.info.awakeSince + }; + + + } + + + + + let recordSleepStats=function(){ + var today = new Date().getDay(); + var sleepData=getSleepData(); + var data=getData(); + //Wakeup time + var wakeUpTime=getMsPastMidnight(data.awakeSince); + var avgWakeUpTime=averageNumbers(data.avgWakeUpTime,data.totalCycles,wakeUpTime); + data.avgWakeUpTime=avgWakeUpTime; + + //sleep time in minutes + var time=sleepData.totalSleep; + + + var avgSleepTime = averageNumbers(data.avgSleepTime, data.totalCycles, time); + data.avgSleepTime = avgSleepTime; + + data.promptLastShownDay=today; + + + data.totalCycles+=1; + writeData(data); + + }; + + // takes in an object with {score, weight} + let getWeightedScore=function(components) { + // sum of weights + let totalWeight = 0; + for (let key in components) totalWeight += components[key].weight; + + // avoid division by zero + if (totalWeight === 0) return 0; + + let score = 0; + for (let key in components) { + let s = components[key].score; + let w = components[key].weight; + score += (s * (w / totalWeight)); + } + + return Math.round(score); + } + let generateScore = function(value, target) { + if (value >= target) { + let extra = Math.min(1, (value - target) / target); + return Math.round(95 + extra * 5); // perfect = 95, max = 100 + } else { + return Math.round((value / target) * 95); + } + } + + + let getSleepScore=function(){ + + var sleepData=getSleepData(); + var settings=getSettings(); + var summaryData=getData(); + var deepSleepScore; + var totalSleepTimeScore; + //only if enabled in Health + //var hrmScore; + + + + + + + + return getWeightedScore({ + duration: + {score: generateScore(sleepData.consecSleep/60,settings.idealSleepHours), weight: 0.6}, + deepSleep: + {score: generateScore(sleepData.deepSleep/60,settings.deepSleepHours), weight: 0.3}, + averageSleep: + {score:generateScore(sleepData.totalSleep,summaryData.avgSleepTime),weight:0.15}, + averageWakeup: + {score:generateScore(getMsPastMidnight(sleepData.awakeSince),summaryData.avgWakeUpTime),weight:0.1}, + }); + } + + + + + + let getAllSleepScores=function(){ + var data=getData(); + var sleepData=getSleepData(); + var settings=getSettings(); + return { + durationScore:generateScore(sleepData.consecSleep/60,settings.idealSleepHours), + deepSleepScore:generateScore(sleepData.deepSleep/60,settings.deepSleepHours), + avgWakeUpScore: generateScore(getMsPastMidnight(sleepData.awakeSince),data.avgWakeUpTime), + avgSleepTimeScore:generateScore(sleepData.totalSleep,data.avgSleepTime), + overallScore:getSleepScore(), + } + }; + + + + + + + exports.deleteData = deleteData; + exports.getSummaryData=getData; + exports.recordData=recordSleepStats; + exports.getSleepScores=getAllSleepScores; + exports.getSleepData=getSleepData; + + + +} + From 4d331f52f3c4d9183947a36aa35368c128f014d2 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Sun, 14 Sep 2025 20:48:19 -0400 Subject: [PATCH 03/40] Implement sleep summary display with custom layout --- apps/sleepsummary/app.js | 79 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 apps/sleepsummary/app.js diff --git a/apps/sleepsummary/app.js b/apps/sleepsummary/app.js new file mode 100644 index 0000000000..766fd347c9 --- /dev/null +++ b/apps/sleepsummary/app.js @@ -0,0 +1,79 @@ +var Layout = require("Layout"); +var sleepScore = require("sleepsummary").getSleepScores().overallScore; + +// Convert unix timestamp (s or ms) → HH:MM +function unixToTime(ts) { + if (ts < 1e12) ts *= 1000; // seconds → ms + let d = new Date(ts); + return d.getHours() + ":" + ("0"+d.getMinutes()).slice(-2); +} + +// Custom renderer for the battery bar +function drawGraph(l) { + let w = 160; + g.setColor(g.theme.fg); + g.drawRect(l.x, l.y, l.x+w, l.y+10); // outline + g.setColor("#00F") + if(g.theme.dark)g.setColor("#0F0"); + g.fillRect(l.x, l.y, l.x+(sleepScore*1.65), l.y+10); // fill +} + +// Layout definition +var pageLayout = new Layout({ + type: "v", c: [ + {type:"txt",filly:0, label:"Sleep Summary", font:"Vector:17", halign:0, id:"title",height:17,pad:3}, + { + type:"v", c: [ + {type:"txt", label:"Sleep Score: --", font:"8%", pad:5, id:"sleepScore"}, + {type:"custom", render:drawGraph, height:15, width:165, id:"scoreBar"}, + + {type:undefined, height:4}, // spacer + {type:"txt", label:"Time Stats", font:"9%"}, + {type:"h", c:[ + { + type:"v", pad:3, c:[ + {type:"txt", label:"", font:"9%",halign:1,pad:4}, + {type:"txt", label:"Wake Up:", font:"8%",halign:1}, + {type:"txt", label:"Sleep:", font:"8%",halign:1}, + ] + }, + { + type:"v", pad:3, c:[ + {type:"txt", label:"Today", font:"9%",pad:4}, + {type:"txt", label:"3:40a", font:"8%", id:"todayWakeupTime"}, + {type:"txt", label:"7:40", font:"8%", id:"todaySleepTime"}, + ] + }, + { + type:"v", pad:3, c:[ + {type:"txt", label:"Avg", font:"9%",pad:4}, + {type:"txt", label:"6:33a", font:"8%", id:"avgWakeupTime"}, + {type:"txt", label:"7:54", font:"8%", id:"avgSleepTime"}, + ] + } + ]} + ] + } + + ] +}, {lazy:true}); + +// Update function +function draw() { + battery = E.getBattery(); + pageLayout.sleepScore.label = "Sleep score: "+sleepScore; + pageLayout.render(); +} + +// Setup custom back handler +Bangle.setUI({ + mode: "custom", + back: ()=>load() +}); + +// Initial draw +g.clear(); +draw(); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); From a183ad21d22fc8cedaee31e52bcb845c710ce820 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Sun, 14 Sep 2025 21:29:10 -0400 Subject: [PATCH 04/40] Add metadata.json for Sleep Summary app This file contains metadata for the Sleep Summary app, including its ID, name, version, description, and supported platforms. --- apps/sleepsummary/metadata.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 apps/sleepsummary/metadata.json diff --git a/apps/sleepsummary/metadata.json b/apps/sleepsummary/metadata.json new file mode 100644 index 0000000000..1863791fdd --- /dev/null +++ b/apps/sleepsummary/metadata.json @@ -0,0 +1,25 @@ + { + "id": "sleepsummary", + "name": "Sleep Summary", + "shortName": "Sleep Summ.", + "version": "0.01", + "description": "Adds a module that learns sleeping habits over time and provides a sleep score based on how good of a sleep you got", + "icon": "app.png", + "type": "app", + "tags": "health", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"sleepsummary.app.js","url":"app.js"}, + {"name":"sleepsummary.boot.js","url":"boot.js"}, + {"name":"sleepsummary","url":"module.js"}, + {"name":"sleepsummary.settings.js","url":"settings.js"}, + ], + "data":[ + {"name":"sleepsummarydata.json"}, + {"name":"sleepsummary.settings.json"}, + ], + "screenshots":[ + {"url":"screenshot.png"} + ] +} From 52ba8dfc8833dcd7283f325fc6b2c61d766f3ca3 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Sun, 14 Sep 2025 21:30:07 -0400 Subject: [PATCH 05/40] Add files via upload --- apps/sleepsummary/Screenshot.png | Bin 0 -> 21402 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/sleepsummary/Screenshot.png diff --git a/apps/sleepsummary/Screenshot.png b/apps/sleepsummary/Screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..7d38079a86e07aea5678ed9331d39807d71b8ac7 GIT binary patch literal 21402 zcmd41cT`i)7Y9g_rcyC#?|!m`7q((-O{j=lhaa>ljG2GcX?~)_y!M8IU>=3#89{AZib1@Jt9(hS=9}7 zs(2b%RZ{+=7>-Bx$f#7K$)0t@JT6|EQ6TDzEY`U*OWyrT&dlqL_D*2wx9q~G?Nv$mk39;gv&SrIJTUzn@c3_2t%H zNL<-SUi$R(>D?N%x?SLx^!UC$qwt^EoHRx&)j|rCiHhIrJMA-g-)aoSZDH+y9xEun z>t(B??yeQI2_{3`_jP7*36G^aAZ+Cj4HcLB#LNhMt^4^Nh0aXNMHU@Az+AfBk7_b} zCB#imwCj!VtFZOPqJFFhY(D&nGUhnQE0Zkx_l()3npD32r z6Q+ERi`v_TDKgCv{wG^db8WT?j!pgsJ0^ z1d24TPBJ(;3z(})0lwwNMoL(_Z!YxnJ|l{QTKZk|DLWg=imrvh-AeEH%0DOt+v>_e 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 kO6lHp$3E#&- Date: Sun, 14 Sep 2025 21:30:40 -0400 Subject: [PATCH 06/40] Update metadata.json --- apps/sleepsummary/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sleepsummary/metadata.json b/apps/sleepsummary/metadata.json index 1863791fdd..54bc668f09 100644 --- a/apps/sleepsummary/metadata.json +++ b/apps/sleepsummary/metadata.json @@ -20,6 +20,6 @@ {"name":"sleepsummary.settings.json"}, ], "screenshots":[ - {"url":"screenshot.png"} + {"url":"Screenshot.png"} ] } From 133ceddeac264892bd60067cee3b7460db406c49 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 14:57:38 -0400 Subject: [PATCH 07/40] Add README for sleepsummary module --- apps/sleepsummary/README.md | 60 +++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 apps/sleepsummary/README.md diff --git a/apps/sleepsummary/README.md b/apps/sleepsummary/README.md new file mode 100644 index 0000000000..4d7c80a00b --- /dev/null +++ b/apps/sleepsummary/README.md @@ -0,0 +1,60 @@ +# Sleep Summary +This provides the module `sleepsummary`, which collects sleep data from `Sleep Log`, and generates a sleep score for you, based on average wakeup times, duration and more. The scores will generally trend higher in the first week that you use the module, however, the most accurate scores come the longer you use the module. + +The module also comes with an app to see detailed statistics of your sleep compared to your averages prior. +All data is stored only on your device. + +It is highly reccomended to use HRM with `sleeplog` for increased accuracy in sleep tracking, leading to a more accurate sleep score here. To enable this, turn on HRM intervals in the `Health` app. +## Formula + +The module takes in several data points: +- How long you slept compared to your average +- Duration compared to ideal duration set in settings +- Deep sleep length compared to ideal deep sleep set in settings +- When you woke up compared to your average + +The module then averages those individual scores with a weight added to get a score out of 100. +## Settings +- Show Message - Whether or not to show a good morning message / prompt when you wake up (this may not be exactly when you wake up, depending on how accurate your settings for Sleeplog are) +- Ideal Deep Sleep - How much deep sleep per night should qualify as a deep sleep score of 100 (the highest) +- Ideal duration - How much sleep per night should qualify as a sleep duration score of 100 (the highest) +- Use True Sleep - Whether or not to use True Sleep from sleeplog. If not checked, uses consecutive sleep instead. + +## Development +To use the module, do `require("sleepsummary")` followed by any function from the list below. + +- `require("sleepsummary").recordData();` - Records current sleep data for the averages. It is best to only do this once a day, and is already automatically handled by the module. + +- `require("sleepsummary").getSummaryData();` - Returns the following: + - `avgSleepTime` - The average time you sleep for, returned in minutes + - `totalCycles` - How many times that the average was calculated / recorded + - `avgWakeUpTime` - The average time you wake up at every day, returned in ms (milliseconds) past midnight + - `promptLastShownDay` - The day of the week that the good morning prompt was last shown (0-6). This is only used by the boot.js file as a way to know if it needs to show the prompt today + +- `require("sleepsummary").getSleepData();` - Returns the following about this night's sleep (Most of the data comes directly from `require("sleeplog").getStats(Date.now(), 24*60*60*1000)`: + - `calculatedAt` - When the data was calculated + - `deepSleep` - How long in minutes you spent in deep sleep + - `lightSleep` - How long in minutes you spent in light sleep + - `awakeSleep` - How long you spent awake during sleep periods + - `consecSleep` - How long in minutes your consecutive sleep is + - `awakeTime` - How long you are awake for + - `notWornTime` - How long the watch was detected as not worn + - `unknownTime` - Time spent unknown + - `logDuration` - Time spent logging + - `firstDate` - Unix timestamp of the first log entry in the stats + - `lastDate`: Unix timestamp of the last log entry in the stats + - `totalSleep`: Total minutes of sleep for this night using consecutive sleep or true sleep, depending on what's selected in settings + - `awakeSince` - Unix timestamp of the time you woke up at. + + - `require("sleepsummary").getSleepScores();` - Returns the following sleep scores: + - `durationScore` - Sleep duration compared to ideal duration set in settings. + - `deepSleepScore` - Deep sleep length compared to ideal deep sleep set in settings + - `avgWakeUpScore` - When you woke up compared to your average + - `avgSleepTimeScore` - How long you slept compared to your average + - `overallScore`:The overall sleep score, calculated as a weighted average of all the other scores. + + - `require("sleepsummary").deleteData();` - Deletes learned data, automatically relearns the next time `recordData()` is called. + + +## Creator +RKBoss6 From 274ec8b8e9c1312bee454c359c9d8c7b0b99d8fd Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 16:11:53 -0400 Subject: [PATCH 08/40] Add settings management for Sleep Summary app --- apps/sleepsummary/settings.js | 71 +++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 apps/sleepsummary/settings.js diff --git a/apps/sleepsummary/settings.js b/apps/sleepsummary/settings.js new file mode 100644 index 0000000000..f91e1e2bec --- /dev/null +++ b/apps/sleepsummary/settings.js @@ -0,0 +1,71 @@ +(function(back) { + var FILE = "sleepsummary.settings.json"; + // Load settings + var settings = Object.assign({ + useTrueSleep:true, + showMessage:true, + deepSleepHours:5, + idealSleepHours:10, + + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "Sleep Summary" }, + "< Back" : () => back(), + 'Use True Sleep': { + value: !!settings.useTrueSleep, + onchange: v => { + settings.useTrueSleep = v; + writeSettings(); + } + }, + 'Show Message': { + value: !!settings.showMessage, + onchange: v => { + settings.showMessage = v; + writeSettings(); + } + + }, + 'Ideal Deep Sleep': { + value: 0|settings.deepSleepHours*60, + min: 60, max: 600, + step:15, + onchange: v => { + settings.deepSleepHours = v/60; //Convert minutes to hours + writeSettings(); + }, + format : v => { + let h = Math.floor(v/60); + let m = v % 60; + let str = ""; + if (h) str += h+"h"; + if (m) str += " "+m+"m"; + return str || "0m"; + } + }, + 'Ideal Sleep Time': { + value: 0|settings.idealSleepHours*60, + min: 120, max: 60*14 , + step:15, + onchange: v => { + settings.idealSleepHours = v/60, + writeSettings(); + }, + format : v => { + let h = Math.floor(v/60); + let m = v % 60; + let str = ""; + if (h) str += h+"h"; + if (m) str += " "+m+"m"; + return str || "0m"; + } + }, + }); +}) + From 91111dbb72929bd585998682e7372c7dda0fa1d0 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 16:13:10 -0400 Subject: [PATCH 09/40] Add author field to metadata.json --- apps/sleepsummary/metadata.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sleepsummary/metadata.json b/apps/sleepsummary/metadata.json index 54bc668f09..939999ac95 100644 --- a/apps/sleepsummary/metadata.json +++ b/apps/sleepsummary/metadata.json @@ -2,6 +2,7 @@ "id": "sleepsummary", "name": "Sleep Summary", "shortName": "Sleep Summ.", + "author":"RKBoss6", "version": "0.01", "description": "Adds a module that learns sleeping habits over time and provides a sleep score based on how good of a sleep you got", "icon": "app.png", From b8fc56f356390da79957f36b9f75aa7ef669e305 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 16:15:00 -0400 Subject: [PATCH 10/40] Format README settings section with bold text --- apps/sleepsummary/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/sleepsummary/README.md b/apps/sleepsummary/README.md index 4d7c80a00b..a2c9f26b1c 100644 --- a/apps/sleepsummary/README.md +++ b/apps/sleepsummary/README.md @@ -15,10 +15,10 @@ The module takes in several data points: The module then averages those individual scores with a weight added to get a score out of 100. ## Settings -- Show Message - Whether or not to show a good morning message / prompt when you wake up (this may not be exactly when you wake up, depending on how accurate your settings for Sleeplog are) -- Ideal Deep Sleep - How much deep sleep per night should qualify as a deep sleep score of 100 (the highest) -- Ideal duration - How much sleep per night should qualify as a sleep duration score of 100 (the highest) -- Use True Sleep - Whether or not to use True Sleep from sleeplog. If not checked, uses consecutive sleep instead. +- Use True Sleep - Whether or not to use True Sleep from sleeplog. If not checked, uses consecutive sleep instead +- Show Message - Whether or not to show a good morning message / prompt when you wake up (this may not be exactly when you wake up, depending on how accurate your settings for Sleeplog are) +- Ideal Deep Sleep - How much deep sleep per night should qualify as a deep sleep score of 100 (the highest) +- Ideal Sleep Time - How much sleep per night should qualify as a sleep duration score of 100 (the highest) ## Development To use the module, do `require("sleepsummary")` followed by any function from the list below. From d915f163cd3deff8cdda21e91113302865c13c2b Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 16:32:12 -0400 Subject: [PATCH 11/40] Add timeSinceAwake setting to sleep summary Added timeSinceAwake setting and updated Message Time configuration. --- apps/sleepsummary/settings.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/sleepsummary/settings.js b/apps/sleepsummary/settings.js index f91e1e2bec..0ad94a69b6 100644 --- a/apps/sleepsummary/settings.js +++ b/apps/sleepsummary/settings.js @@ -6,6 +6,7 @@ showMessage:true, deepSleepHours:5, idealSleepHours:10, + timeSinceAwake: 1800000, }, require('Storage').readJSON(FILE, true) || {}); @@ -32,6 +33,23 @@ } }, + 'Message Time': { + value: 0|settings.timeAfterAwake, + min: 0, max: 7200000, + step:15, + onchange: v => { + settings.timeAfterAwake = v; //Convert minutes to hours + writeSettings(); + }, + format : v => { + let h = Math.floor(v/60000); + let m = v % 60; + let str = ""; + if (h) str += h+"h"; + if (m) str += " "+m+"m"; + return str || "0m"; + } + }, 'Ideal Deep Sleep': { value: 0|settings.deepSleepHours*60, min: 60, max: 600, From e9b659f05eb7ffc6ce99cce413385223454ccf33 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 21:23:14 -0400 Subject: [PATCH 12/40] Add files via upload --- apps/sleepsummary/app.png | Bin 0 -> 5140 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/sleepsummary/app.png diff --git a/apps/sleepsummary/app.png b/apps/sleepsummary/app.png new file mode 100644 index 0000000000000000000000000000000000000000..f6742eb2ad195572e8b66b6f6507b9faef6181cd GIT binary patch literal 5140 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 rJ~VPNFUaarZ=X@ne{rY2w( Date: Mon, 15 Sep 2025 21:24:56 -0400 Subject: [PATCH 13/40] Add app icon entry to metadata.json --- apps/sleepsummary/metadata.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/sleepsummary/metadata.json b/apps/sleepsummary/metadata.json index 939999ac95..74daa79c3f 100644 --- a/apps/sleepsummary/metadata.json +++ b/apps/sleepsummary/metadata.json @@ -15,10 +15,11 @@ {"name":"sleepsummary.boot.js","url":"boot.js"}, {"name":"sleepsummary","url":"module.js"}, {"name":"sleepsummary.settings.js","url":"settings.js"}, + {"name":"sleepsummary.img","url":"appicon.js","evaluate":true}, ], "data":[ {"name":"sleepsummarydata.json"}, - {"name":"sleepsummary.settings.json"}, + {"name":"sleepsummary.settings.json"} ], "screenshots":[ {"url":"Screenshot.png"} From 9cce58ef3c472c8912a32e69065142daf13eb28c Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 21:25:46 -0400 Subject: [PATCH 14/40] Add appicon.js for sleep summary app --- apps/sleepsummary/appicon.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/sleepsummary/appicon.js diff --git a/apps/sleepsummary/appicon.js b/apps/sleepsummary/appicon.js new file mode 100644 index 0000000000..93a1fa8c32 --- /dev/null +++ b/apps/sleepsummary/appicon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4kA///9PTxEF+PQ9td1P991lmN+mOG99tymerXXjPH/nl1U1DoJj/AH4AYgUiAAgXQCwoYQFwwABkAuPl10kVJzOeGBwuCz//4W///6C6PPpn2o1s+xIOC4W/lAsBl+yC6Ov34XB1+CC6Mq5X5GYKQPC4XkxfyJITAPB4N//dilf+C6AwBknia5sRiMQGAoAFFJAXGMIQWMC44AQC7dVqoXUgoXBqAXTCwIABgczABMwC60zC+x3DC6anDC6Ud7oAC6YX/C4IARC/4Xla4IAMgIX/C5oA/AH4ANA=")) From 3215939cd69a73be11932b1fd420c478fadc23e1 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 21:27:03 -0400 Subject: [PATCH 15/40] Fix formatting in metadata.json --- apps/sleepsummary/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sleepsummary/metadata.json b/apps/sleepsummary/metadata.json index 74daa79c3f..7916f66596 100644 --- a/apps/sleepsummary/metadata.json +++ b/apps/sleepsummary/metadata.json @@ -15,7 +15,7 @@ {"name":"sleepsummary.boot.js","url":"boot.js"}, {"name":"sleepsummary","url":"module.js"}, {"name":"sleepsummary.settings.js","url":"settings.js"}, - {"name":"sleepsummary.img","url":"appicon.js","evaluate":true}, + {"name":"sleepsummary.img","url":"appicon.js","evaluate":true} ], "data":[ {"name":"sleepsummarydata.json"}, From 7a1e0f00e033379fcc51cec2b499dbf6fb5882bf Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 21:44:31 -0400 Subject: [PATCH 16/40] Implement time conversion functions in app.js --- apps/sleepsummary/app.js | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/sleepsummary/app.js b/apps/sleepsummary/app.js index 766fd347c9..e4319e9da5 100644 --- a/apps/sleepsummary/app.js +++ b/apps/sleepsummary/app.js @@ -2,12 +2,32 @@ var Layout = require("Layout"); var sleepScore = require("sleepsummary").getSleepScores().overallScore; // Convert unix timestamp (s or ms) → HH:MM -function unixToTime(ts) { - if (ts < 1e12) ts *= 1000; // seconds → ms - let d = new Date(ts); - return d.getHours() + ":" + ("0"+d.getMinutes()).slice(-2); +function msToTimeStr(ms) { + // convert ms → minutes + let totalMins = Math.floor(ms / 60000); + let h = Math.floor(totalMins / 60) % 24; // hours in 0–23 + let m = totalMins % 60; // minutes + let ampm = h >= 12 ? "p" : "a"; + + // convert to 12-hour clock, where 0 → 12 + let hour12 = h % 12; + if (hour12 === 0) hour12 = 12; + + // pad minutes + let mm = m.toString().padStart(2, "0"); + + return `${hour12}:${mm}${ampm}`; } +function minsToTimeStr(mins) { + let h = Math.floor(mins / 60) % 24; // hours 0–23 + let m = mins % 60; // minutes 0–59 + let mm = m.toString().padStart(2,"0"); + return `${h}:${mm}`; +} + + + // Custom renderer for the battery bar function drawGraph(l) { let w = 160; @@ -21,6 +41,7 @@ function drawGraph(l) { // Layout definition var pageLayout = new Layout({ type: "v", c: [ + {type:undefined, height:7}, // spacer {type:"txt",filly:0, label:"Sleep Summary", font:"Vector:17", halign:0, id:"title",height:17,pad:3}, { type:"v", c: [ @@ -60,8 +81,13 @@ var pageLayout = new Layout({ // Update function function draw() { - battery = E.getBattery(); + var sleepData=require("sleepsummary").getSleepData(); + var data=require("sleepsummary").getSummaryData(); pageLayout.sleepScore.label = "Sleep score: "+sleepScore; + pageLayout.todayWakeupTime.label = msToTimeStr(sleepData.awakeSince); + pageLayout.avgWakeupTime.label = msToTimeStr(data.avgWakeUpTime); + pageLayout.todaySleepTime.label = minsToTimeStr(sleepData.totalSleep); + pageLayout.avgSleepTime.label = minsToTimeStr(data.avgSleepTime); pageLayout.render(); } From 3c17a7046312de30ea52b478576c1649a7ad6729 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 21:44:48 -0400 Subject: [PATCH 17/40] Refactor getMsPastMidnight and update sleep calculations --- apps/sleepsummary/module.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/apps/sleepsummary/module.js b/apps/sleepsummary/module.js index 8403b0ae77..8dc4d9e738 100644 --- a/apps/sleepsummary/module.js +++ b/apps/sleepsummary/module.js @@ -1,7 +1,7 @@ { let getMsPastMidnight=function(unixTimestamp) { - const millisecondsSinceEpoch = unixTimestamp * 1000; - const dateObject = new Date(millisecondsSinceEpoch); + + const dateObject = new Date(unixTimestamp); const hours = dateObject.getHours(); const minutes = dateObject.getMinutes(); @@ -23,7 +23,8 @@ avgSleepTime: 0, totalCycles:0, avgWakeUpTime:0, - promptLastShownDay:"" + promptLastShownDay:"", + timeSinceAwake: 1800000, }, require('Storage').readJSON("sleepsummarydata.json", true) || {}); }; @@ -31,7 +32,6 @@ let getSettings=function() { return Object.assign({ useTrueSleep:true, - timeSinceAwake: 1800000, showMessage:true, deepSleepHours:5, idealSleepHours:10, @@ -65,7 +65,7 @@ firstDate: data.firstDate, lastDate: data.lastDate, totalSleep: totalSleep, - awakeSince:global.sleeplog.info.awakeSince + awakeSince:getMsPastMidnight(global.sleeplog.info.awakeSince) }; @@ -79,7 +79,7 @@ var sleepData=getSleepData(); var data=getData(); //Wakeup time - var wakeUpTime=getMsPastMidnight(data.awakeSince); + var wakeUpTime=sleepData.awakeSince; var avgWakeUpTime=averageNumbers(data.avgWakeUpTime,data.totalCycles,wakeUpTime); data.avgWakeUpTime=avgWakeUpTime; @@ -131,16 +131,9 @@ var sleepData=getSleepData(); var settings=getSettings(); var summaryData=getData(); - var deepSleepScore; - var totalSleepTimeScore; + //only if enabled in Health //var hrmScore; - - - - - - return getWeightedScore({ duration: From 2d5543abb7d5f5cee76d7814a9f84ccf165a9f7f Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 21:45:09 -0400 Subject: [PATCH 18/40] Add condition to check awakeSince is not zero --- apps/sleepsummary/boot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sleepsummary/boot.js b/apps/sleepsummary/boot.js index 5e3f119a64..66ad41fa9e 100644 --- a/apps/sleepsummary/boot.js +++ b/apps/sleepsummary/boot.js @@ -65,7 +65,7 @@ if(require("sleepsummary").getSummaryData().promptLastShownDay!=today){ var settings=getSettings(); var awakeSince=global.sleeplog.info.awakeSince; - if(awakeSince+settings.timeSinceAwake Date: Mon, 15 Sep 2025 21:48:07 -0400 Subject: [PATCH 19/40] Revise ideal deep sleep and sleep time thresholds --- apps/sleepsummary/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/sleepsummary/README.md b/apps/sleepsummary/README.md index a2c9f26b1c..13ea704261 100644 --- a/apps/sleepsummary/README.md +++ b/apps/sleepsummary/README.md @@ -17,8 +17,8 @@ The module then averages those individual scores with a weight added to get a sc ## Settings - Use True Sleep - Whether or not to use True Sleep from sleeplog. If not checked, uses consecutive sleep instead - Show Message - Whether or not to show a good morning message / prompt when you wake up (this may not be exactly when you wake up, depending on how accurate your settings for Sleeplog are) -- Ideal Deep Sleep - How much deep sleep per night should qualify as a deep sleep score of 100 (the highest) -- Ideal Sleep Time - How much sleep per night should qualify as a sleep duration score of 100 (the highest) +- Ideal Deep Sleep - How much deep sleep per night should qualify as a deep sleep score of 95 (more than this gives you 100) +- Ideal Sleep Time - How much sleep per night should qualify as a sleep duration score of 95 (more than this gives you 100) ## Development To use the module, do `require("sleepsummary")` followed by any function from the list below. @@ -44,14 +44,14 @@ To use the module, do `require("sleepsummary")` followed by any function from th - `firstDate` - Unix timestamp of the first log entry in the stats - `lastDate`: Unix timestamp of the last log entry in the stats - `totalSleep`: Total minutes of sleep for this night using consecutive sleep or true sleep, depending on what's selected in settings - - `awakeSince` - Unix timestamp of the time you woke up at. + - `awakeSince` - Time you woke up at, in ms past midnight - `require("sleepsummary").getSleepScores();` - Returns the following sleep scores: - `durationScore` - Sleep duration compared to ideal duration set in settings. - `deepSleepScore` - Deep sleep length compared to ideal deep sleep set in settings - `avgWakeUpScore` - When you woke up compared to your average - `avgSleepTimeScore` - How long you slept compared to your average - - `overallScore`:The overall sleep score, calculated as a weighted average of all the other scores. + - `overallScore` - The overall sleep score, calculated as a weighted average of all the other scores - `require("sleepsummary").deleteData();` - Deletes learned data, automatically relearns the next time `recordData()` is called. From f818d22fa6b95b7bf80d97841792b768f9f1a1c9 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 21:53:09 -0400 Subject: [PATCH 20/40] Refactor UI setup for sleep summary app --- apps/sleepsummary/app.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/sleepsummary/app.js b/apps/sleepsummary/app.js index e4319e9da5..68e1d8ca71 100644 --- a/apps/sleepsummary/app.js +++ b/apps/sleepsummary/app.js @@ -91,15 +91,18 @@ function draw() { pageLayout.render(); } -// Setup custom back handler -Bangle.setUI({ - mode: "custom", - back: ()=>load() -}); // Initial draw g.clear(); draw(); + +// We want this app to behave like a clock: +// i.e. show launcher when middle button pressed +Bangle.setUI("clock"); +// But the app is not actually a clock +// This matters for widgets that hide themselves for clocks, like widclk or widclose +delete Bangle.CLOCK; + Bangle.loadWidgets(); Bangle.drawWidgets(); From 6bb869a19a76ba8570c32cdbadcdb7243dbdf2a5 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Mon, 15 Sep 2025 21:58:59 -0400 Subject: [PATCH 21/40] Add dependencies and provides_modules to metadata --- apps/sleepsummary/metadata.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sleepsummary/metadata.json b/apps/sleepsummary/metadata.json index 7916f66596..2e3e0cb6f8 100644 --- a/apps/sleepsummary/metadata.json +++ b/apps/sleepsummary/metadata.json @@ -10,6 +10,8 @@ "tags": "health", "supports": ["BANGLEJS2"], "readme": "README.md", + "dependencies" : { "sleeplog":"app" }, + "provides_modules" : ["sleepsummary"], "storage": [ {"name":"sleepsummary.app.js","url":"app.js"}, {"name":"sleepsummary.boot.js","url":"boot.js"}, From bcd5f02fdd6e67be15fe061898b0d230c9706313 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Thu, 9 Oct 2025 17:33:32 -0400 Subject: [PATCH 22/40] Update settings.js --- apps/sleepsummary/settings.js | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/apps/sleepsummary/settings.js b/apps/sleepsummary/settings.js index 0ad94a69b6..97ffe921b6 100644 --- a/apps/sleepsummary/settings.js +++ b/apps/sleepsummary/settings.js @@ -1,21 +1,13 @@ (function(back) { var FILE = "sleepsummary.settings.json"; // Load settings - var settings = Object.assign({ - useTrueSleep:true, - showMessage:true, - deepSleepHours:5, - idealSleepHours:10, - timeSinceAwake: 1800000, - - }, require('Storage').readJSON(FILE, true) || {}); + var settings = require("sleepsummary").getSettings(); function writeSettings() { require('Storage').writeJSON(FILE, settings); } - // Show the menu - E.showMenu({ + E.showMenu({ "" : { "title" : "Sleep Summary" }, "< Back" : () => back(), 'Use True Sleep': { @@ -33,12 +25,12 @@ } }, - 'Message Time': { - value: 0|settings.timeAfterAwake, + 'Message Delay': { + value: 0|settings.messageDelay, min: 0, max: 7200000, - step:15, + step:5, onchange: v => { - settings.timeAfterAwake = v; //Convert minutes to hours + settings.messageDelay = v; writeSettings(); }, format : v => { @@ -55,7 +47,7 @@ min: 60, max: 600, step:15, onchange: v => { - settings.deepSleepHours = v/60; //Convert minutes to hours + settings.deepSleepHours = v/60; writeSettings(); }, format : v => { @@ -84,6 +76,19 @@ return str || "0m"; } }, + 'Clear Data': function () { + E.showPrompt("Are you sure you want to delete all averaged data?", {title:"Confirmation"}) + .then(function(v) { + if (v) { + require("sleepsummary").deleteData(); + E.showAlert("Cleared data!",{title:"Cleared!"}).then(function(v) {eval(require("Storage").read("sleepsummary.settings.js"))(()=>load())}); + } else { + eval(require("Storage").read("sleepsummary.settings.js"))(()=>load()); + + } + }); + }, }); + }) From 427a19499e5a5cf9dc42548a3ea436a6fa3987bc Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Thu, 9 Oct 2025 17:33:52 -0400 Subject: [PATCH 23/40] Refactor sleep summary logging and awake checks --- apps/sleepsummary/boot.js | 69 +++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/apps/sleepsummary/boot.js b/apps/sleepsummary/boot.js index 66ad41fa9e..2b3e133a82 100644 --- a/apps/sleepsummary/boot.js +++ b/apps/sleepsummary/boot.js @@ -1,4 +1,16 @@ { + let getMsPastMidnight=function(unixTimestamp) { + + const dateObject = new Date(unixTimestamp); + + const hours = dateObject.getHours(); + const minutes = dateObject.getMinutes(); + const seconds = dateObject.getSeconds(); + const milliseconds = dateObject.getMilliseconds(); + + const msPastMidnight = (hours * 3600000) + (minutes * 60000) + (seconds * 1000) + milliseconds; + return msPastMidnight; + }; function formatTime(hours) { let h = Math.floor(hours); // whole hours let m = Math.round((hours - h) * 60); // leftover minutes @@ -15,7 +27,7 @@ } - function logNow() { + function logNow(msg) { let filename="sleepsummarylog.json"; let storage = require("Storage"); @@ -29,7 +41,7 @@ ("0"+d.getDate()).slice(-2) + " " + ("0"+d.getHours()).slice(-2) + ":" + ("0"+d.getMinutes()).slice(-2) + ":" + - ("0"+d.getSeconds()).slice(-2); + ("0"+d.getSeconds()).slice(-2)+", MSG: "+msg; // push new entry log.push(timeStr); @@ -42,6 +54,7 @@ } let showSummary=function(){ + logNow("shown") var sleepData=require("sleepsummary").getSleepData(); var sleepScore=require("sleepsummary").getSleepScores().overallSleepScore; //sleepData.consecSleep @@ -60,33 +73,47 @@ }); } - function checkIfAwake(){ + + function checkIfAwake(data,thisTriggerEntry){ + + logNow("checked, prev status: "+data.prevStatus+", current status: "+data.status+", promptLastShownDay: "+require("sleepsummary").getSummaryData().promptLastShownDay); + let today = new Date().getDay(); if(require("sleepsummary").getSummaryData().promptLastShownDay!=today){ - var settings=getSettings(); - var awakeSince=global.sleeplog.info.awakeSince; - if(awakeSince+settings.timeSinceAwake Date: Thu, 9 Oct 2025 17:34:39 -0400 Subject: [PATCH 24/40] Refactor sleep summary module for settings and scores --- apps/sleepsummary/module.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/sleepsummary/module.js b/apps/sleepsummary/module.js index 8dc4d9e738..0f0e4e1b67 100644 --- a/apps/sleepsummary/module.js +++ b/apps/sleepsummary/module.js @@ -24,7 +24,6 @@ totalCycles:0, avgWakeUpTime:0, promptLastShownDay:"", - timeSinceAwake: 1800000, }, require('Storage').readJSON("sleepsummarydata.json", true) || {}); }; @@ -32,6 +31,7 @@ let getSettings=function() { return Object.assign({ useTrueSleep:true, + messageDelay: 1800000, showMessage:true, deepSleepHours:5, idealSleepHours:10, @@ -160,7 +160,7 @@ deepSleepScore:generateScore(sleepData.deepSleep/60,settings.deepSleepHours), avgWakeUpScore: generateScore(getMsPastMidnight(sleepData.awakeSince),data.avgWakeUpTime), avgSleepTimeScore:generateScore(sleepData.totalSleep,data.avgSleepTime), - overallScore:getSleepScore(), + overallScore:getSleepScore() } }; @@ -173,7 +173,8 @@ exports.getSummaryData=getData; exports.recordData=recordSleepStats; exports.getSleepScores=getAllSleepScores; - exports.getSleepData=getSleepData; + exports.getSleepData=getSleepData; + exports.getSettings=getSettings; From 2beb5c2ab956a2628a7aedd45b28d24374b110e4 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 17:25:58 -0500 Subject: [PATCH 25/40] Refactor sleep data handling and caching Refactor sleep summary module to use average data and cache results. --- apps/sleepsummary/module.js | 92 +++++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 14 deletions(-) diff --git a/apps/sleepsummary/module.js b/apps/sleepsummary/module.js index 0f0e4e1b67..41e58996ac 100644 --- a/apps/sleepsummary/module.js +++ b/apps/sleepsummary/module.js @@ -1,4 +1,8 @@ + { + //Creator: RKBoss6 + //The calculations used are very resource-heavy, so we calculate once, and offload to a cache for the day. + let getMsPastMidnight=function(unixTimestamp) { const dateObject = new Date(unixTimestamp); @@ -17,7 +21,7 @@ }; - let getData=function(){ + let getAvgData=function(){ return Object.assign({ avgSleepTime: 0, @@ -26,19 +30,19 @@ promptLastShownDay:"", }, require('Storage').readJSON("sleepsummarydata.json", true) || {}); - }; + } let getSettings=function() { return Object.assign({ useTrueSleep:true, - messageDelay: 1800000, + timeSinceAwake: 1800000, showMessage:true, deepSleepHours:5, idealSleepHours:10, }, require('Storage').readJSON("sleepsummary.settings.json", true) || {}); }; - + //l let writeData=function(data){ require("Storage").writeJSON("sleepsummarydata.json", data); @@ -65,6 +69,7 @@ firstDate: data.firstDate, lastDate: data.lastDate, totalSleep: totalSleep, + trueSleep:data.deepSleep+data.lightSleep, awakeSince:getMsPastMidnight(global.sleeplog.info.awakeSince) }; @@ -74,10 +79,10 @@ - let recordSleepStats=function(){ + function recordSleepStats(){ var today = new Date().getDay(); var sleepData=getSleepData(); - var data=getData(); + var data=getAvgData(); //Wakeup time var wakeUpTime=sleepData.awakeSince; var avgWakeUpTime=averageNumbers(data.avgWakeUpTime,data.totalCycles,wakeUpTime); @@ -99,7 +104,7 @@ }; // takes in an object with {score, weight} - let getWeightedScore=function(components) { + function getWeightedScore(components) { // sum of weights let totalWeight = 0; for (let key in components) totalWeight += components[key].weight; @@ -130,7 +135,7 @@ var sleepData=getSleepData(); var settings=getSettings(); - var summaryData=getData(); + var summaryData=getAvgData(); //only if enabled in Health //var hrmScore; @@ -152,7 +157,7 @@ let getAllSleepScores=function(){ - var data=getData(); + var data=getAvgData(); var sleepData=getSleepData(); var settings=getSettings(); return { @@ -160,21 +165,80 @@ deepSleepScore:generateScore(sleepData.deepSleep/60,settings.deepSleepHours), avgWakeUpScore: generateScore(getMsPastMidnight(sleepData.awakeSince),data.avgWakeUpTime), avgSleepTimeScore:generateScore(sleepData.totalSleep,data.avgSleepTime), - overallScore:getSleepScore() + overallScore:getSleepScore(), } }; + let writeCachedData=function(data){ + require("Storage").writeJSON("sleepsummarydatacache.json",data); + } + let getCachedData=function(){ + var data=Object.assign({ + + wkUpTime:0, + overallSleepScore:0, + deepSleepScore:0, + wkUpSleepScore:0, + durationSleepScore:0, + consecSleep:0, + trueSleep:0, + dayLastUpdated:100, + + }, require('Storage').readJSON("sleepsummarydatacache.json", true) || {}); + data.sleepDuration=getSettings().useTrueSleep?data.trueSleep:data.consecSleep; + return data; + } + let calcAndCache=function(){ + let today=new Date().getDay(); + let scores=getAllSleepScores(); + let slpData=getSleepData(); + let cachedData=getCachedData(); + //cache data + cachedData.overallSleepScore=scores.overallScore; + cachedData.deepSleepScore=scores.deepSleepScore; + cachedData.wkUpSleepScore=scores.avgWakeUpScore; + cachedData.durationSleepScore=scores.avgSleepTimeScore; + cachedData.consecSleep=slpData.consecSleep; + cachedData.trueSleep=slpData.trueSleep; + cachedData.dayLastUpdated=today; + + writeCachedData(cachedData); + + + + } + let getSummaryData=function(){ + + + let avgData=getAvgData(); + let cachedData=getCachedData() + let today = new Date().getDay(); + // Check if data is up to date for today + if(cachedData.dayLastUpdated!=today){ + //has not cached for today, do it now + calcAndCache(); + //re-run this function to get the new data + cachedData= getCachedData(); + + } + + // we now have up-to-date cache data, return merged cachedData and avgData to user + + return Object.assign({}, avgData, cachedData); + + + } exports.deleteData = deleteData; - exports.getSummaryData=getData; - exports.recordData=recordSleepStats; - exports.getSleepScores=getAllSleepScores; - exports.getSleepData=getSleepData; + exports.recalculate=calcAndCache; + exports.getSummaryData=getSummaryData; + exports.recordAvgData=recordSleepStats; exports.getSettings=getSettings; + From 1ac451cbbf1c32be49a06e3fb0d3d0b514a78612 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 17:26:19 -0500 Subject: [PATCH 26/40] Refactor boot.js for improved readability and structure --- apps/sleepsummary/boot.js | 211 +++++++++++++++++++------------------- 1 file changed, 105 insertions(+), 106 deletions(-) diff --git a/apps/sleepsummary/boot.js b/apps/sleepsummary/boot.js index 2b3e133a82..961a50e326 100644 --- a/apps/sleepsummary/boot.js +++ b/apps/sleepsummary/boot.js @@ -1,119 +1,118 @@ -{ - let getMsPastMidnight=function(unixTimestamp) { - - const dateObject = new Date(unixTimestamp); - - const hours = dateObject.getHours(); - const minutes = dateObject.getMinutes(); - const seconds = dateObject.getSeconds(); - const milliseconds = dateObject.getMilliseconds(); - - const msPastMidnight = (hours * 3600000) + (minutes * 60000) + (seconds * 1000) + milliseconds; - return msPastMidnight; - }; - function formatTime(hours) { - let h = Math.floor(hours); // whole hours - let m = Math.round((hours - h) * 60); // leftover minutes - - // handle rounding like 1.9999 → 2h 0m - if (m === 60) { - h += 1; - m = 0; + { + let getMsPastMidnight=function(unixTimestamp) { + + const dateObject = new Date(unixTimestamp); + + const hours = dateObject.getHours(); + const minutes = dateObject.getMinutes(); + const seconds = dateObject.getSeconds(); + const milliseconds = dateObject.getMilliseconds(); + + const msPastMidnight = (hours * 3600000) + (minutes * 60000) + (seconds * 1000) + milliseconds; + return msPastMidnight; + }; + function formatTime(hours) { + let h = Math.floor(hours); // whole hours + let m = Math.round((hours - h) * 60); // leftover minutes + + // handle rounding like 1.9999 → 2h 0m + if (m === 60) { + h += 1; + m = 0; + } + + if (h > 0 && m > 0) return h + "h " + m + "m"; + if (h > 0) return h + "h"; + return m + "m"; } - if (h > 0 && m > 0) return h + "h " + m + "m"; - if (h > 0) return h + "h"; - return m + "m"; - } + function logNow(msg) { + let filename="sleepsummarylog.json"; + let storage = require("Storage"); - function logNow(msg) { - let filename="sleepsummarylog.json"; - let storage = require("Storage"); + // load existing log (or empty array if file doesn't exist) + let log = storage.readJSON(filename,1) || []; - // load existing log (or empty array if file doesn't exist) - let log = storage.readJSON(filename,1) || []; + // get human-readable time + let d = new Date(); + let timeStr = d.getFullYear() + "-" + + ("0"+(d.getMonth()+1)).slice(-2) + "-" + + ("0"+d.getDate()).slice(-2) + " " + + ("0"+d.getHours()).slice(-2) + ":" + + ("0"+d.getMinutes()).slice(-2) + ":" + + ("0"+d.getSeconds()).slice(-2)+", MSG: "+msg; - // get human-readable time - let d = new Date(); - let timeStr = d.getFullYear() + "-" + - ("0"+(d.getMonth()+1)).slice(-2) + "-" + - ("0"+d.getDate()).slice(-2) + " " + - ("0"+d.getHours()).slice(-2) + ":" + - ("0"+d.getMinutes()).slice(-2) + ":" + - ("0"+d.getSeconds()).slice(-2)+", MSG: "+msg; + // push new entry + log.push(timeStr); - // push new entry - log.push(timeStr); + // keep file from growing forever + if (log.length > 200) log = log.slice(-200); - // keep file from growing forever - if (log.length > 200) log = log.slice(-200); + // save back + storage.writeJSON(filename, log); + } - // save back - storage.writeJSON(filename, log); - } - - let showSummary=function(){ - logNow("shown") - var sleepData=require("sleepsummary").getSleepData(); - var sleepScore=require("sleepsummary").getSleepScores().overallSleepScore; - //sleepData.consecSleep - var message="You slept for "+ formatTime(sleepData.consecSleep/60) +", with a sleep score of "+sleepScore; - - E.showPrompt(message, { - title: "Good Morning!", - buttons: { "Dismiss": 1, "Open App":2}, - - }).then(function (answer) { - if(answer==1){ - Bangle.load(); - }else{ - load("sleepsummary.app.js"); - } - }); - } - - - function checkIfAwake(data,thisTriggerEntry){ - - logNow("checked, prev status: "+data.prevStatus+", current status: "+data.status+", promptLastShownDay: "+require("sleepsummary").getSummaryData().promptLastShownDay); - - let today = new Date().getDay(); - if(require("sleepsummary").getSummaryData().promptLastShownDay!=today){ - //if coming from sleep - if (data.status==2&&(data.previousStatus==3||data.previousStatus==4)) { - var settings=require("sleepsummary").getSettings(); - - //woke up - if(settings.showMessage){ - setTimeout(showSummary,settings.messageDelay) + let showSummary=function(){ + logNow("shown") + let summaryData=require("sleepsummary").getSummaryData() + + var message="You slept for "+ formatTime(summaryData.sleepDuration/60) +", with a sleep score of "+summaryData.overallSleepScore+"!"; + + E.showPrompt(message, { + title: "Good Morning!", + buttons: { "Dismiss": 1, "Open App":2}, + + }).then(function (answer) { + if(answer==1){ + Bangle.load(); + }else{ + Bangle.load("sleepsummary.app.js"); + } + }); + } + + + function checkIfAwake(data,thisTriggerEntry){ + + logNow("checked, prev status: "+data.prevStatus+", current status: "+data.status+", promptLastShownDay: "+require("sleepsummary").getSummaryData().promptLastShownDay); + + let today = new Date().getDay(); + if(require("sleepsummary").getSummaryData().promptLastShownDay!=today){ + //if coming from sleep + if (data.status==2&&(data.previousStatus==3||data.previousStatus==4)) { + var settings=require("sleepsummary").getSettings(); + + //woke up + if(settings.showMessage){ + setTimeout(showSummary,settings.messageDelay) + } + + require("sleepsummary").recordData(); } - - require("sleepsummary").recordData(); + } - } + + //Force-load module + require("sleeplog"); + + // first ensure that the sleeplog trigger object is available (sleeplog is enabled) + if (typeof (global.sleeplog || {}).trigger === "object") { + // then add your parameters with the function to call as object into the trigger object + sleeplog.trigger["sleepsummary"] = { + onChange: true, // false as default, if true call fn only on a status change + from: 0, // 0 as default, in ms, first time fn will be called + to: 24*60*60*1000, // 24h as default, in ms, last time fn will be called + // reference time to from & to is rounded to full minutes + fn: function(data, thisTriggerEntry) { + + checkIfAwake(data,thisTriggerEntry); + + + + } // function to be executed + }; + } + showSummary(); } - - //Force-load module - require("sleeplog"); - - // first ensure that the sleeplog trigger object is available (sleeplog is enabled) - if (typeof (global.sleeplog || {}).trigger === "object") { - // then add your parameters with the function to call as object into the trigger object - sleeplog.trigger["sleepsummary"] = { - onChange: true, // false as default, if true call fn only on a status change - from: 0, // 0 as default, in ms, first time fn will be called - to: 24*60*60*1000, // 24h as default, in ms, last time fn will be called - // reference time to from & to is rounded to full minutes - fn: function(data, thisTriggerEntry) { - - checkIfAwake(data,thisTriggerEntry); - - - - } // function to be executed - }; - } - -} From 3c1c8af9df0a8e53ac19fe5b1b7241cf17c05383 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 17:38:06 -0500 Subject: [PATCH 27/40] Refactor draw function to use new summary data --- apps/sleepsummary/app.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/sleepsummary/app.js b/apps/sleepsummary/app.js index 68e1d8ca71..a643f2b3fb 100644 --- a/apps/sleepsummary/app.js +++ b/apps/sleepsummary/app.js @@ -81,12 +81,11 @@ var pageLayout = new Layout({ // Update function function draw() { - var sleepData=require("sleepsummary").getSleepData(); var data=require("sleepsummary").getSummaryData(); - pageLayout.sleepScore.label = "Sleep score: "+sleepScore; - pageLayout.todayWakeupTime.label = msToTimeStr(sleepData.awakeSince); + pageLayout.sleepScore.label = "Sleep score: "+data.overallSleepScore; + pageLayout.todayWakeupTime.label = msToTimeStr(data.wkUpTime); pageLayout.avgWakeupTime.label = msToTimeStr(data.avgWakeUpTime); - pageLayout.todaySleepTime.label = minsToTimeStr(sleepData.totalSleep); + pageLayout.todaySleepTime.label = minsToTimeStr(data.sleepDuration); pageLayout.avgSleepTime.label = minsToTimeStr(data.avgSleepTime); pageLayout.render(); } From be6a7121fe5d913755397cdc581abe4206933e85 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 17:38:29 -0500 Subject: [PATCH 28/40] Add getSleepData export and update cached wake time --- apps/sleepsummary/module.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/sleepsummary/module.js b/apps/sleepsummary/module.js index 41e58996ac..20acecd319 100644 --- a/apps/sleepsummary/module.js +++ b/apps/sleepsummary/module.js @@ -201,7 +201,7 @@ cachedData.consecSleep=slpData.consecSleep; cachedData.trueSleep=slpData.trueSleep; cachedData.dayLastUpdated=today; - + cachedData.wkUpTime=slpData.awakeSince; writeCachedData(cachedData); @@ -238,6 +238,7 @@ exports.getSummaryData=getSummaryData; exports.recordAvgData=recordSleepStats; exports.getSettings=getSettings; + exports.getSleepData=getSleepData; From 9e19e44a3d62637855aecdd2b484f3af968c3d0b Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 17:39:16 -0500 Subject: [PATCH 29/40] Create ChangeLog --- apps/sleepsummary/ChangeLog | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/sleepsummary/ChangeLog diff --git a/apps/sleepsummary/ChangeLog b/apps/sleepsummary/ChangeLog new file mode 100644 index 0000000000..1a3bc17573 --- /dev/null +++ b/apps/sleepsummary/ChangeLog @@ -0,0 +1 @@ +0.01: New app! From 418e57c2943eac8d62a4d2b38145f70deb46a732 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 20:39:46 -0500 Subject: [PATCH 30/40] Add nicer-looking progress bar and dynamic colors --- apps/sleepsummary/app.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/sleepsummary/app.js b/apps/sleepsummary/app.js index a643f2b3fb..75d4c6a94d 100644 --- a/apps/sleepsummary/app.js +++ b/apps/sleepsummary/app.js @@ -1,6 +1,6 @@ var Layout = require("Layout"); -var sleepScore = require("sleepsummary").getSleepScores().overallScore; - +var data=require("sleepsummary").getSummaryData(); +var score=data.overallSleepScore; // Convert unix timestamp (s or ms) → HH:MM function msToTimeStr(ms) { // convert ms → minutes @@ -28,14 +28,19 @@ function minsToTimeStr(mins) { -// Custom renderer for the battery bar +// Custom renderer for the score bar function drawGraph(l) { let w = 160; + let pad=3; + g.setColor(g.theme.fg); - g.drawRect(l.x, l.y, l.x+w, l.y+10); // outline - g.setColor("#00F") - if(g.theme.dark)g.setColor("#0F0"); - g.fillRect(l.x, l.y, l.x+(sleepScore*1.65), l.y+10); // fill + g.fillRect({x:l.x, y:l.y, w:w, h:12,r:1000}); //bg + g.setColor("#808080") + g.fillRect({x:l.x+pad, y:l.y+pad, w:(w-(2*pad)), h:12-(pad*2),r:10000}); + g.setColor("#0F0"); + if(score<75)g.setColor("#FF8000") + if(score<40)g.setColor("#F00") + g.fillRect({x:l.x+pad, y:l.y+pad, w:score*((w-(2*pad))/100), h:12-(pad*2),r:10000}); } // Layout definition @@ -54,7 +59,7 @@ var pageLayout = new Layout({ { type:"v", pad:3, c:[ {type:"txt", label:"", font:"9%",halign:1,pad:4}, - {type:"txt", label:"Wake Up:", font:"8%",halign:1}, + {type:"txt", label:"Wk Up:", font:"8%",halign:1}, {type:"txt", label:"Sleep:", font:"8%",halign:1}, ] }, @@ -81,8 +86,7 @@ var pageLayout = new Layout({ // Update function function draw() { - var data=require("sleepsummary").getSummaryData(); - pageLayout.sleepScore.label = "Sleep score: "+data.overallSleepScore; + pageLayout.sleepScore.label = "Sleep score: "+score; pageLayout.todayWakeupTime.label = msToTimeStr(data.wkUpTime); pageLayout.avgWakeupTime.label = msToTimeStr(data.avgWakeUpTime); pageLayout.todaySleepTime.label = minsToTimeStr(data.sleepDuration); @@ -93,7 +97,7 @@ function draw() { // Initial draw g.clear(); -draw(); +setTimeout(draw,200); // We want this app to behave like a clock: From 73b006ea4d3608fafb5759cbcd2c864d53255e2e Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 20:57:23 -0500 Subject: [PATCH 31/40] Refactor showSummary function and remove formatTime --- apps/sleepsummary/boot.js | 40 ++++++++------------------------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/apps/sleepsummary/boot.js b/apps/sleepsummary/boot.js index 961a50e326..b23222c01c 100644 --- a/apps/sleepsummary/boot.js +++ b/apps/sleepsummary/boot.js @@ -11,20 +11,7 @@ const msPastMidnight = (hours * 3600000) + (minutes * 60000) + (seconds * 1000) + milliseconds; return msPastMidnight; }; - function formatTime(hours) { - let h = Math.floor(hours); // whole hours - let m = Math.round((hours - h) * 60); // leftover minutes - - // handle rounding like 1.9999 → 2h 0m - if (m === 60) { - h += 1; - m = 0; - } - - if (h > 0 && m > 0) return h + "h " + m + "m"; - if (h > 0) return h + "h"; - return m + "m"; - } + function logNow(msg) { @@ -54,22 +41,9 @@ } let showSummary=function(){ - logNow("shown") - let summaryData=require("sleepsummary").getSummaryData() - - var message="You slept for "+ formatTime(summaryData.sleepDuration/60) +", with a sleep score of "+summaryData.overallSleepScore+"!"; - - E.showPrompt(message, { - title: "Good Morning!", - buttons: { "Dismiss": 1, "Open App":2}, - - }).then(function (answer) { - if(answer==1){ - Bangle.load(); - }else{ - Bangle.load("sleepsummary.app.js"); - } - }); + logNow("shown"); + Bangle.load("sleepsummarymsg.app.js"); + } @@ -83,12 +57,14 @@ if (data.status==2&&(data.previousStatus==3||data.previousStatus==4)) { var settings=require("sleepsummary").getSettings(); - //woke up + //woke up for the first time + require("sleepsummary").recordData(); + if(settings.showMessage){ setTimeout(showSummary,settings.messageDelay) } - require("sleepsummary").recordData(); + } } From b40f94cebb56fbcf7844a5048ddafeebb6fc01c2 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 20:57:42 -0500 Subject: [PATCH 32/40] Implement sleep summary message display --- apps/sleepsummary/msgapp.js | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 apps/sleepsummary/msgapp.js diff --git a/apps/sleepsummary/msgapp.js b/apps/sleepsummary/msgapp.js new file mode 100644 index 0000000000..2c4b5e6671 --- /dev/null +++ b/apps/sleepsummary/msgapp.js @@ -0,0 +1,43 @@ +//If the user has a timeout to return to the clock, this allows it to be ignored +Bangle.setUI("clock"); + +function formatTime(hours) { + let h = Math.floor(hours); // whole hours + let m = Math.round((hours - h) * 60); // leftover minutes + + // handle rounding like 1.9999 → 2h 0m + if (m === 60) { + h += 1; + m = 0; + } + + if (h > 0 && m > 0) return h + "h " + m + "m"; + if (h > 0) return h + "h"; + return m + "m"; +} + +let summaryData=require("sleepsummary").getSummaryData() +let score=summaryData.overallSleepScore; +var message=""; //"You slept for "+ formatTime(summaryData.sleepDuration/60) +if(summaryData.avgWakeUpTime-summaryData.wkUpTime>20){ + message+="You woke up earlier than usual today"; +}else if(summaryData.avgWakeUpTime-summaryData.wkUpTime<-20){ + message+="You woke up later than usual today"; +}else{ + message+="You woke up around the same time as usual today"; +} +message+=", with a sleep score of "+score+"."; + + +E.showPrompt(message, { + title: "Good Morning!", + buttons: { "Dismiss": 1, "Open App":2}, + +}).then(function (answer) { + if(answer==1){ + Bangle.load(); + }else{ + Bangle.load("sleepsummary.app.js"); + } +}); + From 180061441a0cbd5c7d73321aaf2f430213f336bb Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 20:59:03 -0500 Subject: [PATCH 33/40] Add sleepsummarydatacache.json to metadata --- apps/sleepsummary/metadata.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/sleepsummary/metadata.json b/apps/sleepsummary/metadata.json index 2e3e0cb6f8..2f0c44eab3 100644 --- a/apps/sleepsummary/metadata.json +++ b/apps/sleepsummary/metadata.json @@ -17,11 +17,13 @@ {"name":"sleepsummary.boot.js","url":"boot.js"}, {"name":"sleepsummary","url":"module.js"}, {"name":"sleepsummary.settings.js","url":"settings.js"}, + {"name":"sleepsummarymsg.app.js","url":"msgapp.js"}, {"name":"sleepsummary.img","url":"appicon.js","evaluate":true} ], "data":[ {"name":"sleepsummarydata.json"}, - {"name":"sleepsummary.settings.json"} + {"name":"sleepsummary.settings.json"}, + {"name":"sleepsummarydatacache.json"} ], "screenshots":[ {"url":"Screenshot.png"} From 44c5f372247c2c9cb825eec40fcfecf27a567c1c Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 21:00:24 -0500 Subject: [PATCH 34/40] Fix variable name for wakeUpTime in message logic --- apps/sleepsummary/msgapp.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sleepsummary/msgapp.js b/apps/sleepsummary/msgapp.js index 2c4b5e6671..f8047c5f06 100644 --- a/apps/sleepsummary/msgapp.js +++ b/apps/sleepsummary/msgapp.js @@ -19,9 +19,9 @@ function formatTime(hours) { let summaryData=require("sleepsummary").getSummaryData() let score=summaryData.overallSleepScore; var message=""; //"You slept for "+ formatTime(summaryData.sleepDuration/60) -if(summaryData.avgWakeUpTime-summaryData.wkUpTime>20){ +if(summaryData.avgWakeUpTime-summaryData.wakeUpTime>20){ message+="You woke up earlier than usual today"; -}else if(summaryData.avgWakeUpTime-summaryData.wkUpTime<-20){ +}else if(summaryData.avgWakeUpTime-summaryData.wakeUpTime<-20){ message+="You woke up later than usual today"; }else{ message+="You woke up around the same time as usual today"; From 476feddde3c360435c0b7c1aca317e6347c0a2f3 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 21:00:47 -0500 Subject: [PATCH 35/40] Rename wkUpTime to wakeUpTime in cached data --- apps/sleepsummary/module.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/sleepsummary/module.js b/apps/sleepsummary/module.js index 20acecd319..942dd1bff3 100644 --- a/apps/sleepsummary/module.js +++ b/apps/sleepsummary/module.js @@ -1,4 +1,3 @@ - { //Creator: RKBoss6 //The calculations used are very resource-heavy, so we calculate once, and offload to a cache for the day. @@ -174,7 +173,7 @@ let getCachedData=function(){ var data=Object.assign({ - wkUpTime:0, + wakeUpTime:0, overallSleepScore:0, deepSleepScore:0, wkUpSleepScore:0, @@ -201,7 +200,7 @@ cachedData.consecSleep=slpData.consecSleep; cachedData.trueSleep=slpData.trueSleep; cachedData.dayLastUpdated=today; - cachedData.wkUpTime=slpData.awakeSince; + cachedData.wakeUpTime=slpData.awakeSince; writeCachedData(cachedData); From d20130ebf7052e7c08907ad6edea38080b02fc76 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 21:05:25 -0500 Subject: [PATCH 36/40] Fix color codes and update wakeup time label --- apps/sleepsummary/app.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/sleepsummary/app.js b/apps/sleepsummary/app.js index 75d4c6a94d..82afff1941 100644 --- a/apps/sleepsummary/app.js +++ b/apps/sleepsummary/app.js @@ -38,7 +38,8 @@ function drawGraph(l) { g.setColor("#808080") g.fillRect({x:l.x+pad, y:l.y+pad, w:(w-(2*pad)), h:12-(pad*2),r:10000}); g.setColor("#0F0"); - if(score<75)g.setColor("#FF8000") + if(score<75)g.setColor("#FF0"); + if(score<60)g.setColor("#FF8000"); if(score<40)g.setColor("#F00") g.fillRect({x:l.x+pad, y:l.y+pad, w:score*((w-(2*pad))/100), h:12-(pad*2),r:10000}); } @@ -47,7 +48,7 @@ function drawGraph(l) { var pageLayout = new Layout({ type: "v", c: [ {type:undefined, height:7}, // spacer - {type:"txt",filly:0, label:"Sleep Summary", font:"Vector:17", halign:0, id:"title",height:17,pad:3}, + {type:"txt" ,filly:0, label:"Sleep Summary", font:"Vector:17", halign:0, id:"title",height:17,pad:3}, { type:"v", c: [ {type:"txt", label:"Sleep Score: --", font:"8%", pad:5, id:"sleepScore"}, @@ -87,7 +88,7 @@ var pageLayout = new Layout({ // Update function function draw() { pageLayout.sleepScore.label = "Sleep score: "+score; - pageLayout.todayWakeupTime.label = msToTimeStr(data.wkUpTime); + pageLayout.todayWakeupTime.label = msToTimeStr(data.wakeUpTime); pageLayout.avgWakeupTime.label = msToTimeStr(data.avgWakeUpTime); pageLayout.todaySleepTime.label = minsToTimeStr(data.sleepDuration); pageLayout.avgSleepTime.label = minsToTimeStr(data.avgSleepTime); From bf9c7510183f6d50f7083f2192b7fbbc9d73d506 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 21:15:30 -0500 Subject: [PATCH 37/40] Update module.js --- apps/sleepsummary/module.js | 50 +++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/apps/sleepsummary/module.js b/apps/sleepsummary/module.js index 942dd1bff3..5e2e185cf8 100644 --- a/apps/sleepsummary/module.js +++ b/apps/sleepsummary/module.js @@ -1,3 +1,4 @@ + { //Creator: RKBoss6 //The calculations used are very resource-heavy, so we calculate once, and offload to a cache for the day. @@ -78,29 +79,7 @@ - function recordSleepStats(){ - var today = new Date().getDay(); - var sleepData=getSleepData(); - var data=getAvgData(); - //Wakeup time - var wakeUpTime=sleepData.awakeSince; - var avgWakeUpTime=averageNumbers(data.avgWakeUpTime,data.totalCycles,wakeUpTime); - data.avgWakeUpTime=avgWakeUpTime; - - //sleep time in minutes - var time=sleepData.totalSleep; - - - var avgSleepTime = averageNumbers(data.avgSleepTime, data.totalCycles, time); - data.avgSleepTime = avgSleepTime; - - data.promptLastShownDay=today; - - - data.totalCycles+=1; - writeData(data); - - }; + // takes in an object with {score, weight} function getWeightedScore(components) { @@ -230,7 +209,30 @@ } - + function recordSleepStats(){ + var today = new Date().getDay(); + var sleepData=getSleepData(); + var data=getAvgData(); + //Wakeup time + var wakeUpTime=sleepData.awakeSince; + var avgWakeUpTime=averageNumbers(data.avgWakeUpTime,data.totalCycles,wakeUpTime); + data.avgWakeUpTime=avgWakeUpTime; + + //sleep time in minutes + var time=sleepData.totalSleep; + + + var avgSleepTime = averageNumbers(data.avgSleepTime, data.totalCycles, time); + data.avgSleepTime = avgSleepTime; + + data.promptLastShownDay=today; + + + data.totalCycles+=1; + writeData(data); + // recalculate new data for cache + calcAndCache(); + }; exports.deleteData = deleteData; exports.recalculate=calcAndCache; From 88f2cec40e2ab95da084ae30e5d7eb78c9bad880 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Tue, 11 Nov 2025 21:23:23 -0500 Subject: [PATCH 38/40] Remove call to showSummary function --- apps/sleepsummary/boot.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/sleepsummary/boot.js b/apps/sleepsummary/boot.js index b23222c01c..b7f6a574af 100644 --- a/apps/sleepsummary/boot.js +++ b/apps/sleepsummary/boot.js @@ -90,5 +90,4 @@ } // function to be executed }; } - showSummary(); } From c35dbcc28fe6565f984f4ceba42eab03386bcc74 Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Wed, 12 Nov 2025 08:55:09 -0500 Subject: [PATCH 39/40] Update module.js --- apps/sleepsummary/module.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sleepsummary/module.js b/apps/sleepsummary/module.js index 5e2e185cf8..ab3930382a 100644 --- a/apps/sleepsummary/module.js +++ b/apps/sleepsummary/module.js @@ -237,7 +237,7 @@ exports.deleteData = deleteData; exports.recalculate=calcAndCache; exports.getSummaryData=getSummaryData; - exports.recordAvgData=recordSleepStats; + exports.recordData=recordSleepStats; exports.getSettings=getSettings; exports.getSleepData=getSleepData; From 151a1ab6a23fc8f8b4aa961789a737a79349c1ed Mon Sep 17 00:00:00 2001 From: RKBoss6 Date: Wed, 12 Nov 2025 08:55:32 -0500 Subject: [PATCH 40/40] Fix missing newline at end of boot.js