BangleApps_old/apps/heatsuite/heatsuite.wid.js

819 lines
30 KiB
JavaScript

(() => {
const modHS = require('HSModule');
var settings = modHS.getSettings();
var cache = modHS.getCache();
var hrmInterval = 0;
var appName = "heatsuite";
var bleAdvertGen = 0xE9D0;
var lastBLEAdvert = [];
var recorders;
var activeRecorders = [];
var dataLog = [];
var lastGPSFix = 0;
var gpsLog = [];
var connectionLock = false;
var processQueue = [];
var processQueueTimeout = null;
let initHandlerTimeout = null;
let BTHRM_ConnectCheck = null;
//high Accelerometry data
var perSecAccHandler = null;
var highAccTimeout = null;
var highAccWriteTimeout = null;
//Fall Detection
var fallTime = 0;
var fallDetected = false;
Bangle.setOptions({
"hrmSportMode": -1,
});
//function for setting timeouts to the nearest second or minute
function timeoutAligned(periodMs, callback) {
var now = new Date();
var millisPassed = (now.getSeconds() * 1000) + now.getMilliseconds();
if (periodMs < 1000) periodMs = 1000; //nothing less than a second is allowed
var millisLeft = periodMs - (millisPassed % periodMs);
return setTimeout(() => { callback(); }, millisLeft);
}
function secondsSinceMidnight() {//valuable for compact storage of time
let d = new Date();
return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
}
function queueProcess(func, arg) {
processQueue.push((next) => func(next, arg));
if (!connectionLock) {
processNextInQueue();
} else {
if (!processQueueTimeout) {
processQueueTimeout = setTimeout(processNextInQueue, 1000);
}
}
}
function processNextInQueue() {
clearTimeout(processQueueTimeout); // Clear the timeout when processing starts
processQueueTimeout = null;
if (processQueue.length === 0) {
return;
}
if (!connectionLock && processQueue.length > 0) {
const task = processQueue.shift();
task(() => {
modHS.log("[ProcessQueue] Processing queued Task");
processNextInQueue();
});
} else {
if (!processQueueTimeout) {
processQueueTimeout = setTimeout(processNextInQueue, 1000);
}
}
}
function stringToBytes(str) {
const byteArray = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
byteArray[i] = str.charCodeAt(i);
}
return byteArray;
}
function xorEncryptWithSalt(payload, key, salt) {
const encrypted = [];
const keyLen = key.length;
const saltLen = salt.length;
for (let i = 0; i < payload.length; i++) {
encrypted[i] = payload[i] ^ key.charCodeAt(i % keyLen) ^ salt.charCodeAt(i % saltLen);
}
return encrypted;
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
let temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
function flattenArray(array) {
let flattened = [];
for (let i = 0; i < array.length; i++) {
if (Array.isArray(array[i])) {
// If the element is an array, concatenate it recursively
flattened = flattened.concat(flattenArray(array[i]));
} else {
// If it's not an array, add the element to the result
flattened.push(array[i]);
}
}
return flattened;
}
function createRandomizedPayload(studyid, battery, temperature, heartRate, hr_loc, movement, pingFlag) {
let textBytes = stringToBytes(studyid);
if (textBytes.length < 4) {
const paddedArray = new Uint8Array(4); // Create a new Uint8Array with 4 bytes (default 0x00)
paddedArray.set(textBytes); // Copy the textBytes into the paddedArray
textBytes = paddedArray; // Replace textBytes with the padded version
}
const dataBlocks = [];
dataBlocks.push([0x00, textBytes[0], textBytes[1], textBytes[2], textBytes[3]]); // Study Code
var alert = false;
if (cache.hasOwnProperty('alert') && Object.keys(cache.alert).length > 0) {
var HEALTH_EVENTS = {
fall: 1,
custom: 99
};
var HEALTH_EVENT_TYPE = HEALTH_EVENTS[cache.alert.type] || HEALTH_EVENTS.custom;
dataBlocks.push([0x07, HEALTH_EVENT_TYPE]);
}
if (studyid !== "####") {
if (battery != null) {
dataBlocks.push([0x01, battery]); // Battery level
}
if (temperature != null && !isNaN(temperature)) {
dataBlocks.push([0x02, temperature & 255, (temperature >> 8) & 255]); // Temperature
}
if (heartRate != null && !isNaN(heartRate)) {
dataBlocks.push([0x03, heartRate]);
}
if (hr_loc != null && !isNaN(hr_loc) && !alert) { //sub this for an alert flag if needed!
dataBlocks.push([0x04, hr_loc]);
}
if (movement != null && !isNaN(movement)) {
dataBlocks.push([
0x05,
movement & 255,
(movement >> 8) & 255,
(movement >> 16) & 255,
(movement >> 24) & 255
]);
}
}
if (!isNaN(pingFlag)) {
let statusByte = (+Bangle.isCharging() << 1) | pingFlag;
dataBlocks.push([0x06, statusByte]);
}
modHS.log(JSON.stringify(dataBlocks));
const randomizedDataBlocks = shuffleArray(dataBlocks);
const payload = flattenArray(randomizedDataBlocks);
return studyid !== "####" ? xorEncryptWithSalt(payload, "heatsuite", studyid) : payload;
}
//from: https://github.com/espruino/BangleApps/tree/master/apps/recorder
//adapted to minute average data
let getRecorders = function () {
var recorders = {
hrm: function () {
var bpm = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null }, bpmConfidence = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null }, src = "";
function onHRM(h) {
if (h.confidence !== 0) bpmConfidence = newValueHandler(bpmConfidence, h.confidence);
if (h.bpm !== 0) bpm = newValueHandler(bpm, h.bpm);
src = h.src;
}
return {
name: "HR",
fields: ["hr", "hr_conf", "hr_src"],
getValues: () => {
var r = [bpm.avg === null ? null : bpm.avg.toFixed(0), bpmConfidence.avg === null ? null : bpmConfidence.avg.toFixed(0), src];
bpm = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
bpmConfidence = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
src = "";
return r;
},
start: () => {
Bangle.on('HRM', onHRM);
Bangle.setHRMPower(1, appName);
},
stop: () => {
Bangle.removeListener('HRM', onHRM);
Bangle.setHRMPower(0, appName);
},
draw: (x, y) => g.setColor(Bangle.isHRMOn() ? "#f00" : "#f88").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"), x, y)
};
},
bthrm: function () {
var bt_bpm = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
var bt_bat = "";
var bt_energy = "";
var bt_contact = "";
var bt_rr = [];
function onBTHRM(h) {
//modHS.log(JSON.stringify(h));
if (h.bpm === 0) return;
bt_bpm = newValueHandler(bt_bpm, h.bpm);
bt_bat = h.bat;
bt_energy = h.energy;
bt_contact = h.contact;
if (h.rr) {
h.rr.forEach(val => bt_rr.push(val));
}
}
return {
name: "BT HR",
fields: ["bt_bpm", "bt_bat", "bt_energy", "bt_contact", "bt_rr"],
getValues: () => {
const result = [bt_bpm.avg === null ? null : bt_bpm.avg.toFixed(0), bt_bat, bt_energy, bt_contact, bt_rr.join(";")];
bt_bpm = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
bt_rr = [];
bt_bat = "";
bt_energy = "";
bt_contact = "";
return result;
},
start: () => {
Bangle.on('BTHRM', onBTHRM);
if (Bangle.setBTHRMPower) Bangle.setBTHRMPower(1, appName);
},
stop: () => {
Bangle.removeListener('BTHRM', onBTHRM);
if (Bangle.setBTHRMPower) Bangle.setBTHRMPower(0, appName);
}
}
},
CORESensor: function () {
var core = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
var skin = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
var core_hr = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
var heatflux = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
var hsi = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
var core_bat = null;
var unit = null;
function onCORE(h) {
core = newValueHandler(core, h.core);
skin = newValueHandler(skin, h.skin);
if (core_hr > 0) {
core_hr = newValueHandler(core_hr, h.hr);
}
heatflux = newValueHandler(heatflux, h.heatflux);
hsi = newValueHandler(hsi, h.hsi);
core_bat = h.battery;
unit = h.unit;
}
return {
name: "CORESensor",
fields: ["core", "skin", "unit", "core_hr", "hf","hsi", "core_bat"],
getValues: () => {
const result = [core.avg === null ? null : core.avg.toFixed(2), skin.avg === null ? null : skin.avg.toFixed(2), unit, core_hr.avg === null ? null : core_hr.avg.toFixed(0), heatflux.avg === null ? null : heatflux.avg.toFixed(2),hsi.avg === null ? null : hsi.avg.toFixed(1), core_bat];
core = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
skin = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
core_hr = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
heatflux = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
hsi = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
core_bat = null;
unit = null;
return result;
},
start: () => {
Bangle.on('CORESensor', onCORE);
if (Bangle.setCORESensorPower) Bangle.setCORESensorPower(1, appName);
},
stop: () => {
Bangle.removeListener('CORESensor', onCORE);
if (Bangle.setCORESensorPower) Bangle.setCORESensorPower(0, appName);
}
}
},
bat: function () {
return {
name: "BAT",
fields: ["batt_p", "batt_v", "charging"],
getValues: () => {
return [E.getBattery(), NRF.getBattery().toFixed(2), Bangle.isCharging()];
},
start: () => {
},
stop: () => {
},
draw: (x, y) => g.setColor(Bangle.isCharging() ? "#0f0" : "#ff0").drawImage(atob("DAwBAABgH4G4EYG4H4H4H4GIH4AA"), x, y)
};
},
steps: function () {
var lastSteps = 0;
return {
name: "steps",
fields: ["steps"],
getValues: () => {
var c = Bangle.getStepCount(), r = [c - lastSteps];
lastSteps = c;
return r;
},
start: () => { lastSteps = Bangle.getStepCount(); },
stop: () => { },
draw: (x, y) => g.reset().drawImage(atob("DAwBAAMMeeeeeeeecOMMAAMMMMAA"), x, y)
};
},
movement: function () {
return {
name: "movement",
fields: ["movement"],
getValues: () => {
return [Bangle.getHealthStatus().movement];
},
start: () => { },
stop: () => { },
draw: (x, y) => g.reset().drawImage(atob("DAwBAAMMeeeeeeeecOMMAAMMMMAA"), x, y)
};
},
acc: function () {
var accMagArray = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
function accelHandler(accel) {
// magnitude is computed as: sqrt(x*x + y*y + z*z)
// to compute Elucidean Norm Minus One, simply run: mag - 1
// (https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0061691)
accMagArray = newValueHandler(accMagArray, accel.mag);
}
return {
name: "Accelerometer",
fields: ["acc_min", "acc_max", "acc_avg", "acc_sum"],
getValues: () => {
var r = [accMagArray.min === null ? null : accMagArray.min.toFixed(4), accMagArray.max === null ? null : accMagArray.max.toFixed(4), accMagArray.avg === null ? null : accMagArray.avg.toFixed(4), accMagArray.sum === null ? null : accMagArray.sum.toFixed(4)];
accMagArray = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
return r;
},
start: () => {
//Bangle.setPollInterval(80); // This will allow it to be dynamic and save battery
Bangle.on('accel', accelHandler);
},
stop: () => {
Bangle.removeListener('accel', accelHandler);
},
draw: (x, y) => g.setColor(Bangle.isHRMOn() ? "#f00" : "#f88").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"), x, y)
};
},
};
if (Bangle.getPressure) {
recorders['baro'] = function () {
var temp = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
var press = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
var alt = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
function onPress(c) {
if (c.temperature !== 0) temp = newValueHandler(temp, c.temperature);
if (c.pressure !== 0) press = newValueHandler(press, c.pressure);
if (c.altitude !== 0) alt = newValueHandler(alt, c.altitude);
}
return {
name: "Baro",
fields: ["baro_temp", "baro_press", "baro_alt"],
getValues: () => {
var r = [temp.avg === null ? null : temp.avg.toFixed(2), press.avg === null ? null : press.avg.toFixed(2), alt.avg === null ? null : alt.avg.toFixed(2)];
var temp = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
var press = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
var alt = { "count": null, "avg": null, "min": null, "max": null, "sum": null, "last": null };
return r;
},
start: () => {
Bangle.setBarometerPower(1, appName);
Bangle.on('pressure', onPress);
},
stop: () => {
Bangle.setBarometerPower(0, appName);
Bangle.removeListener('pressure', onPress);
},
draw: (x, y) => g.setColor("#0f0").drawImage(atob("DAwBAAH4EIHIEIHIEIHIEIEIH4AA"), x, y)
};
}
}
return recorders;
}
function newValueHandler(arr, value) {//a way to keep resource use down for each sensor since this could grow large!
arr.count = (arr.count === null) ? 1 : arr.count + 1;
if (arr.count === 1) arr.min = value;
arr.last = value;
arr.avg = (value + (arr.avg * (arr.count - 1))) / arr.count;
arr.sum = arr.sum + value;
arr.min = (value < arr.min) ? value : arr.min;
arr.max = (value > arr.max) ? value : arr.max;
return arr;
}
//increased accelerometer data storage for higher resolution activity tracking
function perSecAcc(status) {
if(!status){
if (perSecAccHandler) Bangle.removeListener('accel', perSecAccHandler);
if (highAccTimeout) clearTimeout(highAccTimeout);
if (highAccWriteTimeout) clearTimeout(highAccWriteTimeout);
return;
}
function arrayAcc() {
return { count: 0, sum: 0 };
}
function updateArray(acc, value) {
acc.sum += value;
acc.count++;
}
function getAvg(acc) {
return acc.count ? acc.sum / acc.count : 0;
}
let mag = arrayAcc();
let accTemp = [];
perSecAccHandler = function(accel){
updateArray(mag, accel.mag);
};
Bangle.on('accel', perSecAccHandler);
function writeAccLog(buf) {
if (!buf || !buf.length) return;
let f = modHS.getRecordFile("accel", []);
if (!f) return;
let line = "";
function processArrayChunk() {
let chunkSize = 10;
for (let i = 0; i < chunkSize && buf.length; i++) {
let data = buf.shift();
if (!data || data.length !== 8) continue;
let dv = new DataView(data.buffer);
let t = dv.getUint32(0, true);
let mag = dv.getUint16(4, true);
let sum = dv.getUint16(6, true);
line += t + "," + mag + "," + sum + "\n";
}
if (buf.length) {
setTimeout(processArrayChunk, 10);
} else {
f.write(line);
f = null;
}
}
processArrayChunk();
}
function writeHSAccelSetTimeout() {
if (accTemp.length > 0) {
queueProcess((next, buf) => {
writeAccLog(buf);
next();
},accTemp);
accTemp = [];
}
highAccWriteTimeout = timeoutAligned(10000, writeHSAccelSetTimeout); //check every 10 seconds
}
function tempAccLog() {
let secondsSM = secondsSinceMidnight();
let scaledMagAvg = Math.round(getAvg(mag) * 8192);
let scaledMagSum = Math.round(mag.sum * 1024);
let b = new Uint8Array(8);
let dv = new DataView(b.buffer);
dv.setUint32(0, secondsSM, true);
dv.setUint16(4, scaledMagAvg, true);
dv.setUint16(6, scaledMagSum, true);
accTemp.push(b); // Push Uint8Array
mag = arrayAcc();
highAccTimeout = timeoutAligned(1000, tempAccLog);
}
let rawAccLogInt = (settings.AccLogInt ? settings.AccLogInt * 1000 : 1000);
let accLogInt = Math.max(1000, Math.round(rawAccLogInt / 1000) * 1000);
highAccTimeout = timeoutAligned(accLogInt, tempAccLog);
highAccWriteTimeout = timeoutAligned(30000, writeHSAccelSetTimeout);
}
function updateBLEAdvert(data) {
var unix = parseInt((new Date().getTime() / 1000).toFixed(0));
var batt = null,
rawTemp = null,
temperature = null,
heartRate = null,
hr_loc = null,
rawMovement = null,
movement = null;
if (data.length > 0) {
var headers = ['unix', 'tz'];
activeRecorders.forEach(recorder => headers.push.apply(headers, recorder.fields));
const safeGet = (field) => {
const index = headers.indexOf(field);
return index !== -1 ? data[index] : null;
};
unix = data[0]; // Unix timestamp is always first
batt = safeGet('batt_p', data, headers);
rawTemp = safeGet('baro_temp', data, headers);
temperature = (rawTemp != null) ? Math.round(rawTemp * 100) : null;
heartRate = safeGet('hr', data, headers);
hr_loc = 1;
if (headers.includes('bt_hrm')) {
hr_loc = 2;
heartRate = safeGet('bt_hrm', data, headers);
}
rawMovement = safeGet('acc_sum', data, headers);
movement = (rawMovement != null) ? Math.round(rawMovement * 10000) : null;
}
var studyid = settings.studyID || "####";
if (studyid.length > 4) {
studyid = studyid.substring(0, 4);
}
var lastNodePing = cache.lastNodePing || 0;
var nodePing = (Math.abs(unix - lastNodePing) > 360) ? 1 : 0;
let advert = createRandomizedPayload(studyid, batt, temperature, heartRate, hr_loc, movement, nodePing);
modHS.log(advert);
require("ble_advert").set(bleAdvertGen, advert);
}
function fallDetectFunc(acc) {
if (!fallDetected) {
let d = new Date().getTime();
if (fallTime != 0) {
modHS.log("acc", acc.mag);
}
if (fallTime != 0 && d - fallTime > 200) {
fallTime = 0; fallDetected = false;
} else if (acc.mag < 0.3 && fallTime === 0) {
fallTime = d;
modHS.log("FALLING", fallTime);
} else if (acc.mag > 2.1 && d - fallTime < 200) {
//IMPACT
Bangle.buzz(400);
E.showPrompt("Did you fall?", { title: "FALL DETECTION", img: atob("FBQBAfgAf+Af/4P//D+fx/n+f5/v+f//n//5//+f//n////3//5/n+P//D//wf/4B/4AH4A=") }).then((r) => {
if (r) {
fallDetected = true;
modHS.saveDataToFile('alert', 'marker', { 'event': 'fall' });
Bangle.showClock();
} else {
Bangle.showClock(); //no fall, so just return to clock
}
});
}
}
}
function storeTempLog(unix) {
var fields = [unix, ((new Date()).getTimezoneOffset() * -60)];
activeRecorders.forEach(recorder => fields.push.apply(fields, recorder.getValues()));
dataLog.push(fields);
lastBLEAdvert = fields;
updateBLEAdvert(lastBLEAdvert);
}
function writeLog() {
var headers = ["unix", "tz"];
activeRecorders.forEach(recorder => headers.push.apply(headers, recorder.fields));
var storageFile = modHS.getRecordFile('minData', headers);
try {
if (storageFile) {
while (dataLog.length > 0) {
let item = dataLog.shift();
storageFile.write(item.join(',') + '\n');
}
}
} catch (e) {
modHS.log(e);
}
modHS.checkStorageFree('minData');
return true;
}
function toggleHRM() {
var recHRM = recorders['hrm'];
var hrm = recHRM();
if (Bangle.isHRMOn()) {
hrm.stop();
} else {
hrm.start();
}
}
function writeGPS() {
var storageFile = modHS.getRecordFile('gps', ["unix", "tz", 'lat', "lon", "alt", "speed", "course", "fix", "satellites"]);
if (storageFile) {
while (gpsLog.length > 0) { //store it
let item = gpsLog.shift();
storageFile.write(item.join(',') + '\n');
cache["lastGPSSignal"] = item[0];
queueProcess((next, cache) => {
modHS.writeCache(cache);
next();
}, cache);
}
}
}
function gpsHandler() {
if (settings.GPS) {
function logGPS(f) {
if (!isNaN(f.lat)) {
const unix = parseInt((new Date().getTime() / 1000).toFixed(0));
if (unix > lastGPSFix + 60) {
var fields = [unix, ((new Date()).getTimezoneOffset() * -60), f.lat, f.lon, f.alt, f.speed, f.course, f.fix, f.satellites];
gpsLog.push(fields);
lastGPSFix = unix;
}
}
}
Bangle.on('GPS', logGPS);
Bangle.setGPSPower(1, appName);
var updateTime = settings.GPSInterval !== undefined ? settings.GPSInterval * 60 : 600;
var searchTime = settings.GPSScanTime !== undefined ? settings.GPSScanTime * 60 : 60;
var adaptiveTime = settings.GPSAdaptiveTime !== undefined ? settings.GPSAdaptiveTime * 60 : 120;
require("gpssetup").setPowerMode({ power_mode: "PSMOO", update: updateTime, search: searchTime, adaptive: adaptiveTime, appName: appName });
} else {
Bangle.setGPSPower(0, appName); //just make sure its off
}
}
function studyTaskCheck(timenow) {
modHS.log("[StudyTask] Func init at " + timenow);
let notifications = false;
const tasks = settings.StudyTasks;
tasks.forEach(task => {
let key = task.id;
modHS.log(`[StudyTask] Processing task: ${JSON.stringify(task)}`);
if (!cache[key]) {
cache[key] = {};
}
const d = new Date(timenow * 1000);
const hours = d.getHours();
const minutes = d.getMinutes().toString().padStart(2, "0");
const tod = parseInt(`${hours}${minutes}`);
const lastTaskTime = cache[key].unix || 0;
const debounceTime = (timenow - lastTaskTime) >= task.debounce;
if (task.tod !== undefined && Array.isArray(task.tod) && task.tod.includes(tod) && debounceTime) {
modHS.log(`[StudyTask] Time to notify: ${task}`);
const taskID = { id: key, time: timenow };
cache.taskQueue = cache.taskQueue || []; // Ensure taskQueue exists
cache.taskQueue.push(taskID);
var seen = {};
var newTaskQueue = [];
for (var i = 0; i < cache.taskQueue.length; i++) {
var obj = cache.taskQueue[i];
if (!seen[obj.id]) {
seen[obj.id] = true;
newTaskQueue.push(obj);
}
}
cache.taskQueue = newTaskQueue;
modHS.log(`[StudyTask] ${JSON.stringify(cache)}`);
if (task.notify) {
notifications = true;
}
modHS.writeCache(cache);
WIDGETS["heatsuite"].draw();
}
});
if (notifications) {
Bangle.buzz(200);
setTimeout(() => Bangle.buzz(200), 300);
}
if (notifications && Bangle.CLOCK && settings.notifications) {
const desc = `Tasks: ${cache.taskQueue.length}`;
E.showPrompt(desc, {
title: "NOTIFICATION",
img: atob("FBQBAfgAf+Af/4P//D+fx/n+f5/v+f//n//5//+f//n////3//5/n+P//D//wf/4B/4AH4A="),
buttons: {
" X ": false,
" >> ": true
}
}).then(v => {
if (v) {
Bangle.load("heatsuite.app.js");
} else {
Bangle.load();
}
});
}
modHS.log("[StudyTask] Func end");
}
//heart beat of the backend
function init() {
cache = modHS.getCache(); //update cache each minute
var unix = parseInt((new Date().getTime() / 1000).toFixed(0));
if (storeTempLog(unix)) {
modHS.log("Data Logged to RAM");
}
queueProcess((next, unix) => {
modHS.log("[HRM + StudyTask]");
try {
// HRM interval check
if (settings.HRMInterval > 0) {
if (hrmInterval >= settings.HRMInterval) {
toggleHRM();
hrmInterval = 0;
}
hrmInterval++;
}
if (settings.StudyTasks.length > 0) {
studyTaskCheck(unix); // This might also need to be queued if async
}
} catch (error) {
modHS.log("Error in StudyTaskCheck:", error);
} finally {
next(); // Ensure next() is called even if an error occurs
}
}, unix);
}
function initHandler() {
function callback() {
init(); initHandler();
}
initHandlerTimeout = timeoutAligned(60000, callback);
}
function startRecorder() {
settings = modHS.getSettings();
if (initHandlerTimeout) clearTimeout(initHandlerTimeout);
if (BTHRM_ConnectCheck) clearInterval(BTHRM_ConnectCheck);
activeRecorders = []; //clear active recorders
recorders = getRecorders();
settings.record.forEach(r => {
var recorder = recorders[r];
if (!recorder) {
return;
}
var activeRecorder = recorder();
activeRecorder.start();
activeRecorders.push(activeRecorder);
});
if (settings.hasOwnProperty('fallDetect') && settings.fallDetect) {
Bangle.on('accel', fallDetectFunc);
} else {
Bangle.removeListener('accel', fallDetectFunc);
}
//BTHRM Additions
if (settings.record.includes('bthrm') && Bangle.hasOwnProperty("isBTHRMConnected")) {
var BTHRMStatus = 0;
BTHRM_ConnectCheck = setInterval(function () {
if (Bangle.isBTHRMConnected() != BTHRMStatus) {
BTHRMStatus = Bangle.isBTHRMConnected();
WIDGETS["heatsuite"].draw();
}
}, 10000); //runs every 10 seconds
}
updateBLEAdvert(lastBLEAdvert);
initHandler();
if (settings.highAcc !== undefined && settings.highAcc) {
perSecAcc(settings.highAcc);
}
if(settings.swipeOpen !== undefined && settings.swipeOpen){
Bangle.on('swipe', function(dir) {
if (dir === 1) { // 1 = right swipe
Bangle.buzz();
require("widget_utils").hide();
Bangle.load('heatsuite.app.js');
}
});
}
}
startRecorder();
gpsHandler();
function writeSetTimeout() {
if (dataLog.length > 0) {
queueProcess((next, unix) => {
writeLog();
next();
},0);
}
if (gpsLog.length > 0) {
//writeGPS();
queueProcess((next, unix) => {
writeGPS();
next();
},0);
}
setTimeout(writeSetTimeout, 5000);
}
//log writer/checker
writeSetTimeout();
//widget stuff
var iconWidth = 44;
function draw() {
g.reset();
g.setColor(cache.taskQueue === undefined ? "#fff" : cache.taskQueue.length > 0 ? "#f00" : "#0f0");
g.setFontAlign(0, 0);
g.fillRect({ x: this.x, y: this.y, w: this.x + iconWidth - 1, h: this.y + 23, r: 8 });
g.setColor(-1);
g.setFont("Vector", 12);
g.drawImage(atob("FBfCAP//AADk+kPKAAAoAAAAAKoAAAAAKAAAAFQoFQAAVTxVAFQVVVQVRABVABFVEBQEVQBUABUAAFUAVQCoFVVUKogAVQAiqBVVVCoAVQBVAABUABUAVRAUBFVEAFUAEVQVVVQVAFU8VQAAVCgVAAAAKAAAAACqAAAAACgAAA=="), this.x + 1, this.y + 1);
g.setColor((Bangle.hasOwnProperty("isBTHRMConnected") && Bangle.isBTHRMConnected()) ? "#00F" : "#0f0");
g.drawImage(atob("EhCCAAKoAqgCqqiqqCqqqqqqqqqqqqqqqqqqqqqqqqqqqqqiqqqqqAqqqqoAKqqqgACqqqAAAqqoAAAKqgAAACqAAAAAoAAAAAoAAA=="), this.x + 22, this.y + 3);
}
WIDGETS.heatsuite = {
area: 'tr',
width: iconWidth,
draw: draw,
changed: function () {
startRecorder();
WIDGETS["heatsuite"].draw();
}
};
//Diagnosing BLUETOOTH Connection Issues
//for managing memory issues - keeping code here for testing purposes in the future
if (NRF.getSecurityStatus().connected) { //if widget starts while a bluetooth connection exits, force connection flag - but this is
//connectionLock = true;
}
NRF.on('error', function (msg) {
modHS.log("[NRF][ERROR] " + msg);
});
NRF.on('connect', function (addr) {
//connectionLock = true;
modHS.log("[NRF][CONNECTED] " + JSON.stringify(addr));
});
NRF.on('disconnect', function (reason) {
connectionLock = false;
cache = modHS.getCache(); //update cache each disconnect
if (lastBLEAdvert) {
updateBLEAdvert(lastBLEAdvert); //update this if its changed with cache update
}
processNextInQueue();
modHS.log("[NRF][DISCONNECT] " + JSON.stringify(reason));
});
})();