BangleApps_old/apps/crsclock/crsclock.js

805 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// --- Circadian Wellness Clock for Bangle.js 2 ---
// File: crsclock.js
const STORE_KEY = "crswellness";
const STEPS_KEY = "w_steps";
const HR_KEY = "w_hr";
const LIGHT_KEY = "w_light";
const SLEEP_START_KEY = "w_sleepstart";
const VERSION = "1.2";
const storage = require("Storage");
const MAX_HISTORY_ENTRIES = 200;
const TREND_HOURS = 12;
const WRITE_DELAY = 5 * 60 * 1000; // delay before persisting history
let writeTimers = {};
function scheduleHistoryWrite(key, data) {
if (writeTimers[key]) clearTimeout(writeTimers[key]);
writeTimers[key] = setTimeout(() => {
storage.writeJSON(key, data);
writeTimers[key] = undefined;
}, WRITE_DELAY);
}
// 1. PERSISTENT SETTINGS & HISTORICAL DATA
let S = storage.readJSON(STORE_KEY, 1) || {
phaseOffset: 0,
hydrationInterval: 2 * 3600*1000,
lastHydration: Date.now(),
sleepStart: 23,
sleepEnd: 7,
colorTheme: 0,
notifications: {
hydration: true,
sleepDetection: true,
hrLogging: true,
hrPower: true
},
bioTimeRefType: "DLMO",
bioTimeRefHour: 21,
bioTimeRefMinute: 0
};
function pruneHistory(arr, maxHours) {
let cutoff = Date.now() - maxHours * 3600*1000;
while (arr.length && arr[0].t < cutoff) arr.shift();
while (arr.length > MAX_HISTORY_ENTRIES) arr.shift();
}
let stepsHist = storage.readJSON(STEPS_KEY, 1) || [];
let prevStepCount = 0;
let stepResetOffset = 0;
for (let e of stepsHist) {
if (e.s < prevStepCount) stepResetOffset += prevStepCount;
e.c = e.s + stepResetOffset;
prevStepCount = e.s;
}
let hrHist = storage.readJSON(HR_KEY, 1) || [];
let lightHist = storage.readJSON(LIGHT_KEY, 1) || [];
let sleepStartHist = storage.readJSON(SLEEP_START_KEY, 1) || [];
pruneHistory(stepsHist, 24);
pruneHistory(hrHist, 24);
pruneHistory(lightHist, 24);
pruneHistory(sleepStartHist, 24*7);
// 2. COLOR THEMES
const themes = [
{ bg:"#004080", fg:"#ffffff", accent:"#00e0e0" },
{ bg:"#105c30", fg:"#ffffff", accent:"#a0ff00" },
{ bg:"#20222a", fg:"#f5f5f5", accent:"#76d6ff" }
];
let t = themes[S.colorTheme];
// Default font for stats displayed on the clock
const STATS_FONT_NAME = "Vector";
const STATS_FONT_SIZE = 16;
// 3. HELPERS & CALCULATIONS
function getSleepWindowStr() {
let a = ("0"+S.sleepStart).substr(-2) + ":00";
let b = ("0"+S.sleepEnd).substr(-2) + ":00";
return a + "" + b;
}
function stability(arr, key, hours) {
let cutoff = Date.now() - hours * 3600*1000;
let vals = arr.filter(e => e.t > cutoff && typeof e[key] === "number")
.map(e => e[key]);
if (!vals.length) return 0;
let sum = vals.reduce((a,b)=>a+b, 0);
let mean = sum / vals.length;
let sd = Math.sqrt(vals.reduce((a,b)=>a + (b-mean)*(b-mean), 0) / vals.length);
let cv = mean ? sd/mean : 1;
let idx = 100 - cv*100;
return Math.max(0, Math.min(100, idx));
}
function computeCrs(offsetMs) {
let now = Date.now() - (offsetMs || 0);
let sArr = stepsHist.filter(e => e.t <= now && e.t > now - 24*3600*1000);
let hArr = hrHist.filter(e => e.t <= now && e.t > now - 24*3600*1000);
let parts = [];
if (sArr.length) parts.push(stability(sArr, "c", 24));
if (hArr.length) parts.push(stability(hArr, "bpm", 24));
if (sleepStartHist.length >= 2) parts.push(sleepTimingScore());
let lh = estimateLightHours();
if (typeof lh === "number" && !isNaN(lh)) {
let lhScore = Math.min(100, Math.max(0, (lh / 12) * 100));
parts.push(lhScore);
}
if (!parts.length) return 0;
let avg = parts.reduce((a,b)=>a+b,0) / parts.length;
return Math.round(avg);
}
let crsByHour = [];
let lastCrsUpdateTime = 0;
function updateCrsCache() {
if (Date.now() - lastCrsUpdateTime < 5*60*1000 && crsByHour.length === TREND_HOURS) return;
crsByHour = [];
for (let i=0; i<TREND_HOURS; i++) {
let off = i * 3600*1000;
crsByHour.unshift( computeCrs(off) );
}
lastCrsUpdateTime = Date.now();
}
function computeBioTime() {
let d = new Date();
let m = d.getHours()*60 + d.getMinutes() + S.phaseOffset*60;
m = ((m % 1440) + 1440) % 1440;
let hh = Math.floor(m/60), mm = m % 60;
return ("0"+hh).substr(-2) + ":" + ("0"+mm).substr(-2);
}
function detectEmotion() {
let cutoff = Date.now() - 60*60*1000;
let vals = hrHist.filter(e => e.t > cutoff).map(e => e.bpm);
if (!vals.length) return 3; // No Data
let mean = vals.reduce((a,b) => a+b, 0) / vals.length;
let sd = Math.sqrt(vals.reduce((a,b) => a + (b-mean)*(b-mean), 0) / vals.length);
if (sd < 2.5) return 0; // Calm
if (sd < 5) return 1; // Neutral
if (sd < 12) return 2; // Stressed
return 3; // No Data/undefined
}
// Estimate daylight exposure in hours based solely on step data
function estimateLightHours() {
let now = Date.now();
let start = now - 24*3600*1000;
function inSleepWindow(h) {
if (S.sleepStart < S.sleepEnd)
return h >= S.sleepStart && h < S.sleepEnd;
return h >= S.sleepStart || h < S.sleepEnd;
}
let hourMap = {};
let count = 0;
for (let e of stepsHist) {
if (e.t < start) continue;
let d = new Date(e.t);
let hr = d.getHours();
if (hr < 7 || hr > 19) continue;
if (inSleepWindow(hr)) continue;
let key = Math.floor(e.t / 3600000);
if (!hourMap[key]) {
hourMap[key] = true;
count++;
}
}
return count;
}
function getStepsLast24h() {
let cutoff = Date.now() - 24*3600*1000;
let arr = stepsHist.filter(e => e.t > cutoff).map(e=>e.c);
return arr.length>1 ? (arr[arr.length-1] - arr[0]) : 0;
}
function sleepTimingScore() {
return stability(sleepStartHist, "h", 7);
}
let saveTimeout = null;
function saveSettings() {
if (saveTimeout) return;
saveTimeout = setTimeout(() => {
saveTimeout = null;
storage.writeJSON(STORE_KEY, S);
}, 2000);
}
// 4. SENSORS & LOGGING (BATTERY-SAFE)
let hrmEnabled = false;
function onHrmSample(hrm) {
if (typeof hrm.bpm === "number" && hrm.bpm > 0 && S.notifications.hrLogging) {
hrHist.push({ t: Date.now(), bpm: hrm.bpm });
pruneHistory(hrHist, 24);
scheduleHistoryWrite(HR_KEY, hrHist);
}
}
function enableHrmIfNeeded() {
if (!hrmEnabled && S.notifications.hrPower && S.notifications.hrLogging) {
hrmEnabled = true;
Bangle.setHRMPower(1);
Bangle.on("HRM", onHrmSample);
}
}
function disableHrm() {
if (hrmEnabled) {
hrmEnabled = false;
Bangle.removeListener("HRM", onHrmSample);
Bangle.setHRMPower(0);
}
}
// Steps
Bangle.on("step",(s) => {
if (s < prevStepCount) stepResetOffset += prevStepCount;
prevStepCount = s;
let c = s + stepResetOffset;
stepsHist.push({ t: Date.now(), s, c });
pruneHistory(stepsHist, 24);
scheduleHistoryWrite(STEPS_KEY, stepsHist);
});
// Light sensor
function sampleLight() {
let env = Bangle.getHealthStatus().light;
if (env !== undefined) {
lightHist.push({ t: Date.now(), light: env });
pruneHistory(lightHist, 24);
scheduleHistoryWrite(LIGHT_KEY, lightHist);
}
}
setInterval(sampleLight, 300*1000);
if (Bangle.isLCDOn()) sampleLight();
// Sleep/Wake Detection
let isSleeping = false, pendingSleepAlert = false, pendingWakeAlert = false;
function checkSleepWake() {
if (!S.notifications.sleepDetection) return;
let now = Date.now();
let windowStart = now - 30*60*1000; // 30 min ago
let arr = stepsHist.filter(e => e.t >= windowStart).map(e => e.c);
let delta = arr.length>1 ? (arr[arr.length-1] - arr[0]) : 0;
let moving = Bangle.isMoving ? Bangle.isMoving() : true;
let asleep = (delta < 5) && !moving;
if (!isSleeping && asleep) {
isSleeping = true;
S.sleepStart = (new Date()).getHours();
sleepStartHist.push({ t: now, h: S.sleepStart });
pruneHistory(sleepStartHist, 24*7);
scheduleHistoryWrite(SLEEP_START_KEY, sleepStartHist);
pendingSleepAlert = true;
saveSettings();
} else if (isSleeping && !asleep) {
isSleeping = false;
S.sleepEnd = (new Date()).getHours();
pendingWakeAlert = true;
saveSettings();
}
}
setInterval(() => {
if (Bangle.isLCDOn()) checkSleepWake();
}, 15*60*1000);
// Hydration Reminder
let pendingHydrationAlert = false;
function remindHydration() {
if (!S.notifications.hydration) return;
Bangle.buzz();
pendingHydrationAlert = true;
S.lastHydration = Date.now();
saveSettings();
}
setInterval(() => {
if (Date.now() - S.lastHydration >= S.hydrationInterval) {
remindHydration();
}
}, 60*1000);
// LCD events
Bangle.on("lcdPower",(on) => {
if (on) {
enableHrmIfNeeded();
drawClock();
} else {
disableHrm();
}
});
if (Bangle.isLCDOn()) enableHrmIfNeeded();
// ----- DISPLAY HELPERS -----
function drawStat(text, y, g) {
const left = 5;
g.drawString(text, left, y);
}
// 5. DRAW CLOCK SCREEN
function drawClock() {
g.clear();
Bangle.drawWidgets();
let w = g.getWidth(), h = g.getHeight();
// Alerts
if (pendingHydrationAlert) {
pendingHydrationAlert = false;
E.showAlert("Time to hydrate!").then(() => {
drawClock(); Bangle.setUI(uiOpts);
}); return;
}
if (pendingSleepAlert) {
pendingSleepAlert = false;
E.showAlert("Sleep detected at " + computeBioTime()).then(() => {
drawClock(); Bangle.setUI(uiOpts);
}); return;
}
if (pendingWakeAlert) {
pendingWakeAlert = false;
E.showAlert("Wake detected at " + computeBioTime()).then(() => {
drawClock(); Bangle.setUI(uiOpts);
}); return;
}
// ----- HEADER STRIP -----
let headerH = 44;
g.setColor(t.bg);
g.fillRect(0, 24, w, 24 + headerH);
// ----- TIME -----
g.setFont("Vector", 32);
let bt = computeBioTime();
let tw = g.stringWidth(bt);
let xPos = (w - tw) / 2;
let yPos = 24 + (headerH - g.getFontHeight()) / 2;
// Shadow (black, offset)
g.setColor("#000");
g.drawString(bt, xPos + 2, yPos + 2);
// Main time
g.setColor(t.fg);
g.drawString(bt, xPos, yPos);
// ----- STATS -----
let statsY = 24 + headerH + 8;
let crVal = computeCrs(0);
let cr = (isNaN(crVal) || crVal === null || typeof crVal === "undefined") ? "N/A" : crVal;
let stVal = getStepsLast24h();
if (stVal <= 0 || isNaN(stVal)) stVal = null;
let emo = detectEmotion();
let lightHoursVal = estimateLightHours();
let lightHours = (isNaN(lightHoursVal) ? "N/A" : lightHoursVal + "h");
let moodText = ["Calm","Neutral","Stressed","N/A"];
g.setFont(STATS_FONT_NAME, STATS_FONT_SIZE);
g.setColor("#000");
let y = statsY;
drawStat("CRS: " + cr, y, g); y += 18;
if (stVal !== null) { drawStat("Steps: " + stVal, y, g); y += 18; }
drawStat("Mood: " + moodText[emo], y, g); y += 18;
drawStat("Light: " + lightHours, y, g);
// ----- Footer & menu hint -----
g.drawLine(0, h-20, w, h-20);
g.setFont("6x8", 1);
g.setColor("#000");
g.drawString("Menu: btn1/tap/swipe", 10, h-18);
}
// 6. VIEW TREND CHART
function showTrend() {
let w = g.getWidth(), h = g.getHeight();
g.setColor(t.bg); g.fillRect(0, 24, w, h);
g.setFont("Vector", 20);
let title = "CRS Trend (" + TREND_HOURS + "h)";
let tw = g.stringWidth(title);
g.setColor(t.fg);
g.drawString(title, (w - tw)/2, 28);
if (!stepsHist.length && !hrHist.length) {
g.setFont("6x8", 2);
let msg = "No data to show";
let mw = g.stringWidth(msg);
g.drawString(msg, (w - mw)/2, h/2);
g.setFont("6x8", 1);
g.drawString("Tap to return", 10, h-12);
Bangle.setUI(uiOpts);
Bangle.on('touch', function tapCB() {
Bangle.removeListener('touch', tapCB);
drawClock();
Bangle.setUI(uiOpts);
});
return;
}
updateCrsCache();
let i = 0;
function drawNextBar() {
if (i >= TREND_HOURS) {
g.setColor(t.fg);
g.setFont("6x8", 1);
g.drawString("Tap to return", 10, h-12);
Bangle.setUI(uiOpts);
Bangle.on('touch', function tapCB() {
Bangle.removeListener('touch', tapCB);
drawClock();
Bangle.setUI(uiOpts);
});
return;
}
let barW = Math.floor(w / TREND_HOURS);
let val = crsByHour[i];
let barH = Math.round((val/100) * (h - 60));
let x = i * barW;
let y = h - barH - 24;
g.setColor(t.accent);
g.fillRect(x, y, x + barW - 2, h - 24);
i++;
setTimeout(drawNextBar, 0);
}
drawNextBar();
}
// 7. MENU & SETTINGS (no change to your logic)
function showStats() {
let bt = computeBioTime(),
cr = computeCrs(0),
st = getStepsLast24h(),
emo = detectEmotion(),
lightH = estimateLightHours(),
moodText = ["Calm","Neutral","Stressed","N/A"];
E.showAlert(
"BioTime: " + bt + "\n" +
"CRS: " + cr + "\n" +
"Steps: " + st + "\n" +
"Mood: " + moodText[emo] + "\n" +
"Light: " + lightH + "h",
"Today's Stats"
).then(() => {
drawClock();
Bangle.setUI(uiOpts);
});
}
function exportData() {
let exportObj = {
timestamp: new Date().toISOString(),
steps: stepsHist,
hr: hrHist,
light: lightHist,
sleepStart: sleepStartHist,
sleepWindow: { start: S.sleepStart, end: S.sleepEnd },
phaseOffset: S.phaseOffset,
colorTheme: S.colorTheme,
notifications: S.notifications,
bioTimeRefType: S.bioTimeRefType,
bioTimeRefHour: S.bioTimeRefHour,
bioTimeRefMinute:S.bioTimeRefMinute
};
let json = JSON.stringify(exportObj, null, 2);
let filename = "crs_export.json";
storage.write(filename, json);
E.showAlert(`Exported to\n"${filename}"\n(${json.length} bytes)`).then(() => {
drawClock();
Bangle.setUI(uiOpts);
});
}
function confirmResetAllData() {
E.showPrompt("Reset all data?").then(res => {
if (!res) {
drawClock();
Bangle.setUI(uiOpts);
return;
}
storage.erase(STORE_KEY);
storage.erase(STEPS_KEY);
storage.erase(HR_KEY);
storage.erase(LIGHT_KEY);
storage.erase(SLEEP_START_KEY);
S = {
phaseOffset: 0,
hydrationInterval: 2 * 3600*1000,
lastHydration: Date.now(),
sleepStart: 23,
sleepEnd: 7,
colorTheme: 0,
notifications: {
hydration: true,
sleepDetection: true,
hrLogging: true,
hrPower: true
},
bioTimeRefType: "DLMO",
bioTimeRefHour: 21,
bioTimeRefMinute: 0
};
// Persist defaults so they survive a restart
saveSettings();
// Optionally bypass the delayed save
storage.writeJSON(STORE_KEY, S);
stepsHist = []; hrHist = []; lightHist = []; sleepStartHist = [];
prevStepCount = 0; stepResetOffset = 0;
t = themes[S.colorTheme];
E.showAlert("All app data cleared").then(() => {
drawClock();
Bangle.setUI(uiOpts);
});
});
}
// Menu logic: Sleep window, hydration, BT calibration, theme, notifications, bio ref, about — unchanged, use as before
function setSleepWindow() {
let menu = { "": { title: "Select Start Hour" } };
for (let hr = 0; hr < 24; hr++) {
let label = (hr<10?"0":"") + hr;
menu[label] = ((h) => () => {
S.sleepStart = h;
saveSettings();
showEndHourSelector();
})(hr);
}
menu.Back = () => {
drawClock();
Bangle.setUI(uiOpts);
};
E.showMenu(menu);
function showEndHourSelector() {
let em = { "": { title: "Select End Hour" } };
for (let eh = 0; eh < 24; eh++) {
let lab2 = (eh<10?"0":"") + eh;
em[lab2] = ((e) => () => {
S.sleepEnd = e;
saveSettings();
E.showAlert("Sleep window set: " + getSleepWindowStr()).then(() => {
drawClock();
Bangle.setUI(uiOpts);
});
})(eh);
}
em.Back = () => {
drawClock();
Bangle.setUI(uiOpts);
};
E.showMenu(em);
}
}
function setHydrationMenu() {
let m = { "": { title: "Hydration" } };
m["Remind Now"] = () => {
remindHydration();
drawClock();
Bangle.setUI(uiOpts);
};
m["Set Interval"] = setHydrationInterval;
m.Back = () => {
drawClock();
Bangle.setUI(uiOpts);
};
E.showMenu(m);
}
function setHydrationInterval() {
let m = { "": { title: "Set Hydration Interval" } };
for (let h = 1; h <= 12; h++) {
let label = h + "h";
m[label] = ((hours) => () => {
S.hydrationInterval = hours * 3600*1000;
S.lastHydration = Date.now();
saveSettings();
E.showAlert("Interval set: " + label).then(() => {
drawClock();
Bangle.setUI(uiOpts);
});
})(h);
}
m.Off = () => {
S.hydrationInterval = Infinity;
saveSettings();
E.showAlert("Hydration off").then(() => {
drawClock();
Bangle.setUI(uiOpts);
});
};
m.Back = () => {
drawClock();
Bangle.setUI(uiOpts);
};
E.showMenu(m);
}
function calibrateBT() {
let m = { "": { title: "Calibrate BT" } };
for (let d = 1; d <= 8; d++) {
m["+" + d + "h"] = (() => () => {
S.phaseOffset += d;
saveSettings();
E.showAlert("Offset now: " + (S.phaseOffset>=0? "+"+S.phaseOffset : S.phaseOffset) + "h").then(() => {
drawClock();
Bangle.setUI(uiOpts);
});
})(d);
m["" + d + "h"] = (() => () => {
S.phaseOffset -= d;
saveSettings();
E.showAlert("Offset now: " + (S.phaseOffset>=0? "+"+S.phaseOffset : S.phaseOffset) + "h").then(() => {
drawClock();
Bangle.setUI(uiOpts);
});
})(d);
}
m["Reset Offset"] = () => {
S.phaseOffset = 0;
saveSettings();
E.showAlert("Offset reset to 0h").then(() => {
drawClock();
Bangle.setUI(uiOpts);
});
};
m.Back = () => {
drawClock();
Bangle.setUI(uiOpts);
};
E.showMenu(m);
}
function setTheme() {
let m = { "": { title: "Select Theme" } };
m.Blue = () => { S.colorTheme = 0; applyTheme(); };
m.Green = () => { S.colorTheme = 1; applyTheme(); };
m.Dark = () => { S.colorTheme = 2; applyTheme(); };
m.Back = () => {
drawClock();
Bangle.setUI(uiOpts);
};
E.showMenu(m);
function applyTheme() {
t = themes[S.colorTheme];
saveSettings();
drawClock();
Bangle.setUI(uiOpts);
}
}
function showNotificationSettings() {
let m = { "": { title: "Notifications" } };
m["Hydration"] = {
value: S.notifications.hydration,
format: v => v ? "On" : "Off",
onchange: v => {
S.notifications.hydration = v;
saveSettings();
}
};
m["Sleep Detection"] = {
value: S.notifications.sleepDetection,
format: v => v ? "On" : "Off",
onchange: v => {
S.notifications.sleepDetection = v;
saveSettings();
}
};
m["HR Logging"] = {
value: S.notifications.hrLogging,
format: v => v ? "On" : "Off",
onchange: v => {
S.notifications.hrLogging = v;
if (!v) disableHrm();
saveSettings();
}
};
m["HR Monitoring"] = {
value: S.notifications.hrPower,
format: v => v ? "On" : "Off",
onchange: v => {
S.notifications.hrPower = v;
if (v) enableHrmIfNeeded();
else disableHrm();
saveSettings();
}
};
m.Back = () => {
showSettings();
};
E.showMenu(m);
}
function setBioTimeReference() {
let m = { "": { title: "BioTime Reference" } };
m.DLMO = () => {
S.bioTimeRefType = "DLMO";
saveSettings();
promptRefTime();
};
m.CBTmin = () => {
S.bioTimeRefType = "CBTmin";
saveSettings();
promptRefTime();
};
m.Back = () => {
showSettings();
};
E.showMenu(m);
function promptRefTime() {
E.showPrompt("Hour (023)?").then(h => {
if (h===undefined || h<0 || h>23) {
E.showAlert("Invalid hour").then(() => {
drawClock();
Bangle.setUI(uiOpts);
});
return;
}
S.bioTimeRefHour = h;
E.showPrompt("Minute (059)?").then(m => {
if (m===undefined || m<0 || m>59) {
E.showAlert("Invalid minute").then(() => {
drawClock();
Bangle.setUI(uiOpts);
});
return;
}
S.bioTimeRefMinute = m;
saveSettings();
E.showAlert(`Reference set: ${S.bioTimeRefType} @ ${("0"+h).substr(-2)}:${("0"+m).substr(-2)}`)
.then(() => {
drawClock();
Bangle.setUI(uiOpts);
});
});
});
}
}
function showAbout() {
E.showAlert(
"Circadian Wellness Clock v" + VERSION + "\n" +
"Displays your CRS and BioTime.\n" +
"© 2025"
).then(()=>{
drawClock();
Bangle.setUI(uiOpts);
});
}
function showSettings() {
const menu = {
"": { title: "Settings" },
"Sleep Window": setSleepWindow,
"Hydration": setHydrationMenu,
"Calibrate BT": calibrateBT,
"Theme": setTheme,
"Notifications": showNotificationSettings,
"BioTime Ref": setBioTimeReference,
"About / Version": showAbout,
"Back": () => {
drawClock();
Bangle.setUI(uiOpts);
}
};
E.showMenu(menu);
}
// MAIN MENU & UI HOOKUP
const menuOpts = {
"": { title: "Main Menu" },
"Show Stats": showStats,
"View Trend": showTrend,
"Sleep Window": setSleepWindow,
"Hydration": setHydrationMenu,
"Calibrate BT": calibrateBT,
"Export Data": exportData,
"Theme": setTheme,
"About": showAbout,
"Go to Launcher": () => Bangle.showLauncher(),
"Reset All Data": confirmResetAllData,
"Exit": () => Bangle.showClock()
};
const uiOpts = {
mode: "custom",
clock: false,
btn: () => E.showMenu(menuOpts),
touch: () => E.showMenu(menuOpts),
swipe: () => E.showMenu(menuOpts)
};
// 9. INITIALIZE
Bangle.setUI(uiOpts);
Bangle.loadWidgets();
drawClock();
setInterval(() => {
if (Bangle.isLCDOn()) drawClock();
}, 60*1000);
// END