Merge pull request #3827 from HeatSuiteLabs/master
Updates to 2 apps, including 1 new one (HeatSuite) - merged commitsmaster
commit
0e2c8d1814
|
|
@ -15,3 +15,4 @@ _site
|
|||
Desktop.ini
|
||||
.sync_*.db*
|
||||
*.swp
|
||||
apps/heatsuite/heatsuite.5sts.js
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@
|
|||
0.03: Move code for recording to this app
|
||||
0.04: Use default Bangle formatter for booleans
|
||||
0.05: Minor code improvements
|
||||
0.06: Added advanced settings, adapted code to mirror bthrm funcitonality, and will work with latest firmware CORE Sensor Firmware (V0.87)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,43 @@
|
|||
# CoreTemp display
|
||||
|
||||
Basic example of connecting to a bluetooth [CoreTemp](https://corebodytemp.com/) device and displaying the current skin and body core temperature readings.
|
||||
Application to connect to the [CORE](https://corebodytemp.com/) or [calera](https://info.greenteg.com/calera-research) devices from greenteg and display the current skin and body core temperature readings.
|
||||
|
||||
This also includes a module (heavily influenced by the BTHRM app) so you can integrate the core sensor into your own apps/widgets. You can also pair an ANT+ heart rate strap to the CORE/calera sensor as well in the App Settings so that you can leverage the exertional algorthim for estimating core temperature.
|
||||
|
||||
## Usage
|
||||
|
||||
Background task connects to any CoreTemp device (2100/2101) and emits a CoreTemp signal value for each reading.
|
||||
Application contains three components, one is a background task that monitors the sensor and emits a 'CoreTemp' signal on activity if activated in settings.
|
||||
The widget shows when the sensor is enabled with a mini value and blinks on use.
|
||||
The app listens for 'CoreTemp' signals and shows the current skin and core temperatures in large numbers.
|
||||
Background task connects to a paired and emits a CORESensor signal value for each reading.
|
||||
Application contains three components, one is a background task that monitors the sensor and emits a 'CORESensor' signal on activity if activated in settings.
|
||||
The widget shows when the sensor is enabled and connected (green) or disconnected (grey).
|
||||
The app listens for 'CORESensor' signals and shows the current data.
|
||||
|
||||
## CORESensor Module
|
||||
|
||||
With the module, you can add the CORE Sensor to your own app. Simply power on the module and listen to CORESensor:
|
||||
|
||||
```
|
||||
Bangle.setCORESensorPower(1,appName);
|
||||
Bangle.on('CORESensor', (x) =>{ ... });
|
||||
```
|
||||
|
||||
The CORESensor emits an object with following keys:
|
||||
|
||||
* **core**: Estimated/Predicted core temperature
|
||||
* **skin**: Measured skin temperature
|
||||
* **unit**: "F" or "C"
|
||||
* **hr**: Heart Rate (only when ANT+ heart rate monitor is paired)
|
||||
* **heatflux**: (calera device only - needs encryption level b released by greenteg)
|
||||
* **hsi**: Heat Strain Index ([read more here](https://help.corebodytemp.com/en/articles/10447107-heat-strain-index), exertional algorithm only)
|
||||
* **battery**: battery level
|
||||
* **quality**: Used to indicate the quality or trust level of the current measurement values
|
||||
|
||||
## TODO
|
||||
|
||||
* Integrate with other tracking/sports apps to log data.
|
||||
* Add specific device selection
|
||||
* Emit Bangle.js heart rate to device as a heart rate for internal algorthim
|
||||
|
||||
## Creator
|
||||
## Creators/Contributors
|
||||
|
||||
Ivor Hewitt
|
||||
|
||||
[Nicholas Ravanelli](https://github.com/nravanelli)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"enabled":false
|
||||
"enabled":false,
|
||||
"debuglog": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,114 +1 @@
|
|||
//
|
||||
// If enabled in settings run constantly in background
|
||||
//
|
||||
(function() {
|
||||
var log = function() {};//print
|
||||
var settings = {};
|
||||
var device;
|
||||
var gatt;
|
||||
var service;
|
||||
var characteristic;
|
||||
|
||||
class CoreSensor {
|
||||
constructor() {
|
||||
this.unit = "";
|
||||
this.core = -1;
|
||||
this.skin = -1;
|
||||
this.battery = 0;
|
||||
}
|
||||
|
||||
updateSensor(event) {
|
||||
if (event.target.uuid == "00002101-5b1e-4347-b07c-97b514dae121") {
|
||||
var dv = event.target.value;
|
||||
var flags = dv.buffer[0];
|
||||
|
||||
if (flags & 8) {
|
||||
this.unit = "F";
|
||||
} else {
|
||||
this.unit = "C";
|
||||
}
|
||||
|
||||
if (flags & 1) {
|
||||
this.skin = (dv.buffer[4] * 256 + dv.buffer[3]) / 100;
|
||||
} else {
|
||||
this.skin = 0;
|
||||
}
|
||||
if (flags & 2) {
|
||||
this.core = (dv.buffer[2] * 256 + dv.buffer[1]) / 100;
|
||||
} else {
|
||||
this.core = 0;
|
||||
}
|
||||
|
||||
Bangle.emit('CoreTemp',
|
||||
{core : this.core, skin : this.skin, unit : this.unit});
|
||||
}
|
||||
}
|
||||
|
||||
updateBatteryLevel(event) {
|
||||
if (event.target.uuid == "0x2a19")
|
||||
this.battery = event.target.value.getUint8(0);
|
||||
}
|
||||
}
|
||||
|
||||
var mySensor = new CoreSensor();
|
||||
|
||||
function getSensorBatteryLevel(gatt) {
|
||||
gatt.getPrimaryService("180f")
|
||||
.then(function(s) { return s.getCharacteristic("2a19"); })
|
||||
.then(function(c) {
|
||||
c.on('characteristicvaluechanged',
|
||||
(event) => mySensor.updateBatteryLevel(event));
|
||||
return c.startNotifications();
|
||||
});
|
||||
}
|
||||
|
||||
function connection_setup() {
|
||||
log("Scanning for CoreTemp sensor...");
|
||||
NRF.requestDevice({active:true,timeout : 20000, filters : [ {namePrefix : 'CORE'} ]})
|
||||
.then(function(d) {
|
||||
device = d;
|
||||
log("Found device");
|
||||
return device.gatt.connect();
|
||||
})
|
||||
.then(function(g) {
|
||||
gatt = g;
|
||||
return gatt.getPrimaryService('00002100-5b1e-4347-b07c-97b514dae121');
|
||||
})
|
||||
.then(function(s) {
|
||||
service = s;
|
||||
return service.getCharacteristic(
|
||||
'00002101-5b1e-4347-b07c-97b514dae121');
|
||||
})
|
||||
.then(function(c) {
|
||||
characteristic = c;
|
||||
characteristic.on('characteristicvaluechanged',
|
||||
(event) => mySensor.updateSensor(event));
|
||||
return characteristic.startNotifications();
|
||||
})
|
||||
.then(function() {
|
||||
log("Done!");
|
||||
// getSensorBatteryLevel(gatt);
|
||||
})
|
||||
.catch(function(e) {
|
||||
log(e.toString(), "ERROR");
|
||||
log(e);
|
||||
});
|
||||
}
|
||||
|
||||
function connection_end() {
|
||||
if (gatt != undefined)
|
||||
gatt.disconnect();
|
||||
}
|
||||
|
||||
settings = require("Storage").readJSON("coretemp.json", 1) || {};
|
||||
log("Settings:");
|
||||
log(settings);
|
||||
|
||||
if (settings.enabled) {
|
||||
connection_setup();
|
||||
NRF.on('disconnect', connection_setup);
|
||||
}
|
||||
|
||||
E.on('kill', () => { connection_end(); });
|
||||
|
||||
})();
|
||||
if ((require('Storage').readJSON("coretemp.json", true) || {}).enabled != false) require("CORESensor").enable();
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
var settings = require("Storage").readJSON("coretemp.json", 1) || {};
|
||||
// Simply listen for core events and show data
|
||||
|
||||
//var btm = g.getHeight() - 1;
|
||||
var px = g.getWidth() / 2;
|
||||
|
||||
|
|
@ -21,25 +21,14 @@ var corelogo = {
|
|||
function onCore(c) {
|
||||
// Large or small font
|
||||
var sz = ((process.env.HWVERSION == 1) ? 3 : 2);
|
||||
|
||||
g.setFontAlign(0, 0);
|
||||
g.clearRect(0, 32 + 48, g.getWidth(), 32 + 48 + 24 * 4);
|
||||
g.setColor(g.theme.dark ? "#CCC" : "#333"); // gray
|
||||
g.setFont("6x8", sz).drawString(
|
||||
"Core: " + ((c.core < 327) ? (c.core + c.unit) : 'n/a'), px, 48 + 48);
|
||||
g.setFont("6x8", sz).drawString("Skin: " + c.skin + c.unit, px, 48 + 48 + 24);
|
||||
}
|
||||
|
||||
// Background task will activate once settings are enabled.
|
||||
function enableSensor() {
|
||||
settings = require("Storage").readJSON("coretemp.json", 1) || {};
|
||||
|
||||
if (!settings.enabled) {
|
||||
settings.enabled = true;
|
||||
require("Storage").write("coretemp.json", settings);
|
||||
|
||||
drawBackground("Waiting for\ndata...");
|
||||
}
|
||||
g.setFont("6x8", sz).drawString("Core: " + ((c.core < 327) ? (c.core + c.unit) : 'n/a'), px, 48 + 48);
|
||||
g.setFont("6x8", sz).drawString("Skin: " + c.skin + c.unit, px, 48 + 48 + 14);
|
||||
g.setFont("6x8", sz).drawString("HR: " + c.hr + " BPM", px, 48 + 48 + 28);
|
||||
g.setFont("6x8", sz).drawString("HSI: " + c.hsi+ "/10", px, 48 + 48 + 42);
|
||||
g.setFont("6x8", sz).drawString("BATT: " + c.battery+ "%", px, 48 + 48 + 56);
|
||||
}
|
||||
|
||||
function drawBackground(message) {
|
||||
|
|
@ -51,16 +40,11 @@ function drawBackground(message) {
|
|||
g.drawString(message, g.getWidth() / 2, g.getHeight() / 2 + 16);
|
||||
}
|
||||
|
||||
Bangle.on('CoreTemp', onCore);
|
||||
|
||||
settings = require("Storage").readJSON("coretemp.json", 1) || {};
|
||||
|
||||
if (!settings.enabled) {
|
||||
drawBackground("Sensor off\nBTN" +
|
||||
((process.env.HWVERSION == 1) ? '2' : '1') + " to enable");
|
||||
drawBackground("Sensor off\nEnable in Settings");
|
||||
} else {
|
||||
Bangle.setCORESensorPower(1,"COREAPP");
|
||||
Bangle.on('CORESensor', onCore);
|
||||
drawBackground("Waiting for\ndata...");
|
||||
}
|
||||
|
||||
setWatch(() => { enableSensor(); }, (process.env.HWVERSION == 1) ? BTN2 : BTN1,
|
||||
{repeat : false});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,279 @@
|
|||
exports.enable = () => {
|
||||
var settings = require("Storage").readJSON("coretemp.json", 1) || {};
|
||||
let log = function () { };//print
|
||||
Bangle.enableCORESensorLog = function () {
|
||||
log = function (text, param) {
|
||||
let logline = new Date().toISOString() + " - " + text;
|
||||
if (param) logline += ": " + JSON.stringify(param);
|
||||
print(logline);
|
||||
};
|
||||
};
|
||||
let gatt;
|
||||
let device;
|
||||
let characteristics;
|
||||
let blockInit = false;
|
||||
let waitingPromise = function (timeout) {
|
||||
return new Promise(function (resolve) {
|
||||
log("Start waiting for " + timeout);
|
||||
setTimeout(() => {
|
||||
log("Done waiting for " + timeout);
|
||||
resolve();
|
||||
}, timeout);
|
||||
});
|
||||
};
|
||||
|
||||
if (settings.enabled && settings.cache) {
|
||||
let addNotificationHandler = function (characteristic) {
|
||||
log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/);
|
||||
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
|
||||
};
|
||||
let characteristicsFromCache = function (device) {
|
||||
let service = { device: device }; // fake a BluetoothRemoteGATTService
|
||||
log("Read cached characteristics");
|
||||
let cache = settings.cache;
|
||||
if (!cache.characteristics) return [];
|
||||
let restored = [];
|
||||
for (let c in cache.characteristics) {
|
||||
let cached = cache.characteristics[c];
|
||||
let r = new BluetoothRemoteGATTCharacteristic();
|
||||
log("Restoring characteristic ", cached);
|
||||
r.handle_value = cached.handle;
|
||||
r.uuid = cached.uuid;
|
||||
r.properties = {};
|
||||
r.properties.notify = cached.notify;
|
||||
r.properties.read = cached.read;
|
||||
r.service = service;
|
||||
addNotificationHandler(r);
|
||||
log("Restored characteristic: ", r);
|
||||
restored.push(r);
|
||||
}
|
||||
return restored;
|
||||
};
|
||||
let supportedCharacteristics = {
|
||||
"00002101-5b1e-4347-b07c-97b514dae121": {
|
||||
handler: function (dv) {
|
||||
log(dv);
|
||||
let index = 0;
|
||||
let flags = dv.getUint8(index++);
|
||||
let coreTemp = dv.getInt16(index, true) / 100.0;
|
||||
index += 2;
|
||||
let skinTemp = dv.getInt16(index, true) / 100.0;
|
||||
index += 2;
|
||||
let coreReserved = dv.getInt16(index, true); //caleraGT only with firmware decryption provided by Greenteg
|
||||
index += 2;
|
||||
let qualityAndState = dv.getUint8(index++);
|
||||
let heartRate = dv.getUint8(index++);
|
||||
let heatStrainIndex = dv.getUint8(index) / 10.0;
|
||||
let dataQuality = qualityAndState & 0x07;
|
||||
let hrState = (qualityAndState >> 4) & 0x03;
|
||||
let data = {
|
||||
core: coreTemp,
|
||||
skin: skinTemp,
|
||||
unit: (flags & 0b00001000) ? "F" : "C",
|
||||
hr: heartRate,
|
||||
heatflux: coreReserved,
|
||||
hsi: heatStrainIndex,
|
||||
battery: 0,
|
||||
dataQuality: dataQuality,
|
||||
hrState: hrState
|
||||
};
|
||||
if (lastReceivedData.hasOwnProperty("0x180f")) {
|
||||
data.battery = lastReceivedData["0x180f"]["0x2a19"];
|
||||
}
|
||||
log("data", data);
|
||||
Bangle.emit("CORESensor", data);
|
||||
}
|
||||
},
|
||||
"00002102-5b1e-4347-b07c-97b514dae121": {
|
||||
handler: function (dv) {
|
||||
log(dv);//just log the response, handle write and responses in another Promise Function (Bangle.CORESensorSendOpCode)
|
||||
}
|
||||
},
|
||||
"0x2a19": {
|
||||
//Battery
|
||||
handler: function (dv) {
|
||||
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
|
||||
log("Got battery", dv);
|
||||
lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
let lastReceivedData = {
|
||||
};
|
||||
|
||||
Bangle.isCORESensorOn = function () {
|
||||
return (Bangle._PWR && Bangle._PWR.CORESensor && Bangle._PWR.CORESensor.length > 0);
|
||||
};
|
||||
|
||||
Bangle.isCORESensorConnected = function () {
|
||||
return gatt && gatt.connected;
|
||||
};
|
||||
|
||||
let onDisconnect = function (reason) {
|
||||
blockInit = false;
|
||||
log("Disconnect: " + reason);
|
||||
if (Bangle.isCORESensorOn()) {
|
||||
setTimeout(initCORESensor, 5000);
|
||||
}
|
||||
};
|
||||
let createCharacteristicPromise = function (newCharacteristic) {
|
||||
log("Create characteristic promise", newCharacteristic);
|
||||
let result = Promise.resolve();
|
||||
if (newCharacteristic.readValue) {
|
||||
result = result.then(() => {
|
||||
log("Reading data", newCharacteristic);
|
||||
return newCharacteristic.readValue().then((data) => {
|
||||
if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) {
|
||||
supportedCharacteristics[newCharacteristic.uuid].handler(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (newCharacteristic.properties.notify) {
|
||||
result = result.then(() => {
|
||||
log("Starting notifications", newCharacteristic);
|
||||
let startPromise = newCharacteristic.startNotifications().then(() => log("Notifications started", newCharacteristic));
|
||||
startPromise = startPromise.then(() => {
|
||||
return waitingPromise(3000);
|
||||
});
|
||||
return startPromise;
|
||||
});
|
||||
}
|
||||
return result.then(() => log("Handled characteristic", newCharacteristic));
|
||||
};
|
||||
|
||||
let attachCharacteristicPromise = function (promise, characteristic) {
|
||||
return promise.then(() => {
|
||||
log("Handling characteristic:", characteristic);
|
||||
return createCharacteristicPromise(characteristic);
|
||||
});
|
||||
};
|
||||
let initCORESensor = function () {
|
||||
if (!settings.btname) {
|
||||
log("CORESensor not paired, quitting");
|
||||
return;
|
||||
}
|
||||
if (blockInit) {
|
||||
log("CORESensor already turned on by another app, quitting");
|
||||
return;
|
||||
}
|
||||
blockInit = true;
|
||||
NRF.setScan();
|
||||
let promise;
|
||||
let filters;
|
||||
|
||||
if (!device) {
|
||||
if (settings.btname) {
|
||||
log("Configured device name ", settings.btname);
|
||||
filters = [{ name: settings.btname }];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
log("Requesting device with filters", filters);
|
||||
try {
|
||||
promise = NRF.requestDevice({ filters: filters, active: true });
|
||||
} catch (e) {
|
||||
log("Error during initial request:", e);
|
||||
onDisconnect(e);
|
||||
return;
|
||||
}
|
||||
promise = promise.then((d) => {
|
||||
log("Wait after request");
|
||||
return waitingPromise(2000).then(() => Promise.resolve(d));
|
||||
});
|
||||
|
||||
promise = promise.then((d) => {
|
||||
log("Got device", d);
|
||||
d.on('gattserverdisconnected', onDisconnect);
|
||||
device = d;
|
||||
});
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
log("Reuse device", device);
|
||||
}
|
||||
|
||||
promise = promise.then(() => {
|
||||
gatt = device.gatt;
|
||||
return Promise.resolve(gatt);
|
||||
});
|
||||
|
||||
promise = promise.then((gatt) => {
|
||||
if (!gatt.connected) {
|
||||
log("Connecting...");
|
||||
let connectPromise = gatt.connect().then(function () {
|
||||
log("Connected.");
|
||||
});
|
||||
connectPromise = connectPromise.then(() => {
|
||||
log("Wait after connect");
|
||||
return waitingPromise(2000);
|
||||
});
|
||||
return connectPromise;
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
promise = promise.then(() => {
|
||||
if (!characteristics || characteristics.length == 0) {
|
||||
characteristics = characteristicsFromCache(device);
|
||||
}
|
||||
let characteristicsPromise = Promise.resolve();
|
||||
for (let characteristic of characteristics) {
|
||||
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
|
||||
}
|
||||
|
||||
return characteristicsPromise;
|
||||
});
|
||||
|
||||
return promise.then(() => {
|
||||
log("Connection established, waiting for notifications");
|
||||
}).catch((e) => {
|
||||
characteristics = [];
|
||||
log("Error:", e);
|
||||
onDisconnect(e);
|
||||
});
|
||||
};
|
||||
Bangle.setCORESensorPower = function (isOn, app) {
|
||||
// Do app power handling
|
||||
if (!app) app = "?";
|
||||
log("setCORESensorPower ->", isOn, app);
|
||||
if (Bangle._PWR === undefined) Bangle._PWR = {};
|
||||
if (Bangle._PWR.CORESensor === undefined) Bangle._PWR.CORESensor = [];
|
||||
if (isOn && !Bangle._PWR.CORESensor.includes(app)) Bangle._PWR.CORESensor.push(app);
|
||||
if (!isOn && Bangle._PWR.CORESensor.includes(app)) Bangle._PWR.CORESensor = Bangle._PWR.CORESensor.filter(a => a != app);
|
||||
isOn = Bangle._PWR.CORESensor.length;
|
||||
// so now we know if we're really on
|
||||
if (isOn) {
|
||||
log("setCORESensorPower on" + app);
|
||||
if (!Bangle.isCORESensorConnected()) initCORESensor();
|
||||
} else { // being turned off!
|
||||
log("setCORESensorPower turning off ", app);
|
||||
if (gatt) {
|
||||
if (gatt.connected) {
|
||||
log("CORESensor: Disconnect with gatt", gatt);
|
||||
try {
|
||||
gatt.disconnect().then(() => {
|
||||
log("CORESensor: Successful disconnect");
|
||||
}).catch((e) => {
|
||||
log("CORESensor: Error during disconnect promise", e);
|
||||
});
|
||||
} catch (e) {
|
||||
log("CORESensor: Error during disconnect attempt", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// disconnect when swapping apps
|
||||
E.on("kill", function () {
|
||||
if (gatt) {
|
||||
log("CORESensor connected - disconnecting");
|
||||
try { gatt.disconnect(); } catch (e) {
|
||||
log("CORESensor disconnect error", e);
|
||||
}
|
||||
gatt = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "coretemp",
|
||||
"name": "CoreTemp",
|
||||
"version": "0.05",
|
||||
"version": "0.06",
|
||||
"description": "Display CoreTemp device sensor data",
|
||||
"icon": "coretemp.png",
|
||||
"type": "app",
|
||||
|
|
@ -11,10 +11,11 @@
|
|||
"storage": [
|
||||
{"name":"coretemp.wid.js","url":"widget.js"},
|
||||
{"name":"coretemp.app.js","url":"coretemp.js"},
|
||||
{"name":"CORESensor","url":"lib.js"},
|
||||
{"name":"coretemp.recorder.js","url":"recorder.js"},
|
||||
{"name":"coretemp.settings.js","url":"settings.js"},
|
||||
{"name":"coretemp.img","url":"coretemp-icon.js","evaluate":true},
|
||||
{"name":"coretemp.boot.js","url":"boot.js"}
|
||||
{"name":"coretemp.0.boot.js","url":"boot.js"}
|
||||
],
|
||||
"data": [{"name":"coretemp.json","url":"app-settings.json"}],
|
||||
"screenshots": [{"url":"screenshot.png"}]
|
||||
|
|
|
|||
|
|
@ -1,28 +1,40 @@
|
|||
(function(recorders) {
|
||||
recorders.coretemp = function() {
|
||||
var core = "", skin = "";
|
||||
var core = "", skin = "", unit="", hr="", heatflux="", hsi="", battery="", quality="";
|
||||
var hasCore = false;
|
||||
function onCore(c) {
|
||||
core=c.core;
|
||||
skin=c.skin;
|
||||
hasCore = true;
|
||||
unit = c.unit;
|
||||
hr = c.hr;
|
||||
heatflux = c.heatflux;
|
||||
hsi = c.hsi;
|
||||
battery= c.battery;
|
||||
quality = c.dataQuality;
|
||||
}
|
||||
return {
|
||||
name : "Core",
|
||||
fields : ["Core","Skin"],
|
||||
fields : ["Core","Skin","Unit","HeartRate","HeatFlux","HeatStrainIndex","Battery","Quality"],
|
||||
getValues : () => {
|
||||
var r = [core,skin];
|
||||
var r = [core,skin,unit,hr,heatflux,hsi,battery,quality];
|
||||
core = "";
|
||||
skin = "";
|
||||
unit="";
|
||||
hr="";
|
||||
heatflux="";
|
||||
hsi="";
|
||||
battery="";
|
||||
quality="";
|
||||
return r;
|
||||
},
|
||||
start : () => {
|
||||
hasCore = false;
|
||||
Bangle.on('CoreTemp', onCore);
|
||||
Bangle.on('CORESensor', onCore);
|
||||
},
|
||||
stop : () => {
|
||||
hasCore = false;
|
||||
Bangle.removeListener('CoreTemp', onCore);
|
||||
Bangle.removeListener('CORESensor', onCore);
|
||||
},
|
||||
draw : (x,y) => g.setColor(hasCore?"#0f0":"#8f8").drawImage(atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),x,y)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,44 +3,531 @@
|
|||
* @param {function} back Use back() to return to settings menu
|
||||
*/
|
||||
(function (back) {
|
||||
var settings = {};
|
||||
const SETTINGS_FILE = 'coretemp.json'
|
||||
// initialize with default settings...
|
||||
let s = {
|
||||
'enabled': true,
|
||||
}
|
||||
// ...and overwrite them with any saved values
|
||||
// This way saved values are preserved if a new version adds more settings
|
||||
const storage = require('Storage')
|
||||
const saved = storage.readJSON(SETTINGS_FILE, 1) || {}
|
||||
for (const key in saved) {
|
||||
s[key] = saved[key];
|
||||
}
|
||||
var CORECONNECTED = false;
|
||||
// creates a function to safe a specific setting, e.g. save('color')(1)
|
||||
function save(key) {
|
||||
return function (value) {
|
||||
function writeSettings(key, value) {
|
||||
let s = require('Storage').readJSON(SETTINGS_FILE, true) || {};
|
||||
s[key] = value;
|
||||
storage.write(SETTINGS_FILE, s);
|
||||
}
|
||||
require('Storage').writeJSON(SETTINGS_FILE, s);
|
||||
readSettings();
|
||||
}
|
||||
|
||||
function updateSettings() {
|
||||
require("Storage").write("coretemp.json", s);
|
||||
if (WIDGETS["coretemp"])
|
||||
WIDGETS["coretemp"].reload();
|
||||
function readSettings() {
|
||||
settings = Object.assign(
|
||||
require('Storage').readJSON(SETTINGS_FILE, true) || {}
|
||||
);
|
||||
}
|
||||
|
||||
readSettings();
|
||||
let log = () => { };
|
||||
if (settings.debuglog)
|
||||
log = print;
|
||||
|
||||
let supportedServices = [
|
||||
"00002100-5b1e-4347-b07c-97b514dae121", // Core Body Temperature Service
|
||||
"0x180f", // Battery
|
||||
"0x1809", // Health Thermometer Service
|
||||
];
|
||||
|
||||
let supportedCharacteristics = [
|
||||
"00002101-5b1e-4347-b07c-97b514dae121", // Core Body Temperature Characteristic
|
||||
"00002102-5b1e-4347-b07c-97b514dae121", //Core Temp Control Point (opCode for extra function)
|
||||
//"0x2a1c", //Thermometer
|
||||
//"0x2a1d", //Sensor Location (CORE)
|
||||
"0x2a19", // Battery
|
||||
];
|
||||
|
||||
var characteristicsToCache = function (characteristics) {
|
||||
log("Cache characteristics");
|
||||
let cache = {};
|
||||
if (!cache.characteristics) cache.characteristics = {};
|
||||
for (var c of characteristics) {
|
||||
log("Saving handle " + c.handle_value + " for characteristic: ", c.uuid);
|
||||
cache.characteristics[c.uuid] = {
|
||||
"handle": c.handle_value,
|
||||
"uuid": c.uuid,
|
||||
"notify": c.properties.notify,
|
||||
"read": c.properties.read,
|
||||
"write": c.properties.write
|
||||
};
|
||||
}
|
||||
writeSettings("cache", cache);
|
||||
};
|
||||
|
||||
var controlPointChar;
|
||||
|
||||
let createCharacteristicPromise = function (newCharacteristic) {
|
||||
log("Create characteristic promise", newCharacteristic.uuid);
|
||||
if (newCharacteristic.uuid === "00002102-5b1e-4347-b07c-97b514dae121") {
|
||||
log("Subscribing to CoreTemp Control Point Indications.");
|
||||
controlPointChar = newCharacteristic;
|
||||
return controlPointChar.writeValue(new Uint8Array([0x02]), {
|
||||
type: "command",
|
||||
handle: true
|
||||
})
|
||||
.then(() => {
|
||||
log("Indications enabled! Listening for responses...");
|
||||
return controlPointChar.startNotifications(); //now we can send opCodes
|
||||
})
|
||||
.then(() => log("Finished handling CoreTemp Control Point."))
|
||||
.catch(error => {
|
||||
log("Error enabling indications:", error);
|
||||
});
|
||||
}
|
||||
return Promise.resolve().then(() => log("Handled characteristic", newCharacteristic.uuid));
|
||||
};
|
||||
|
||||
let attachCharacteristicPromise = function (promise, characteristic) {
|
||||
return promise.then(() => {
|
||||
log("Handling characteristic:", characteristic.uuid);
|
||||
return createCharacteristicPromise(characteristic);
|
||||
});
|
||||
};
|
||||
|
||||
let characteristics;
|
||||
let createCharacteristicsPromise = function (newCharacteristics) {
|
||||
log("Create characteristics promise ", newCharacteristics.length);
|
||||
let result = Promise.resolve();
|
||||
for (let c of newCharacteristics) {
|
||||
if (!supportedCharacteristics.includes(c.uuid)) continue;
|
||||
log("Supporting characteristic", c.uuid);
|
||||
characteristics.push(c);
|
||||
|
||||
result = attachCharacteristicPromise(result, c);
|
||||
}
|
||||
return result.then(() => log("Handled characteristics"));
|
||||
};
|
||||
|
||||
let createServicePromise = function (service) {
|
||||
log("Create service promise", service.uuid);
|
||||
let result = Promise.resolve();
|
||||
result = result.then(() => {
|
||||
log("Handling service", service.uuid);
|
||||
return service.getCharacteristics().then((c) => createCharacteristicsPromise(c));
|
||||
});
|
||||
return result.then(() => log("Handled service", service.uuid));
|
||||
};
|
||||
|
||||
let attachServicePromise = function (promise, service) {
|
||||
return promise.then(() => createServicePromise(service));
|
||||
};
|
||||
|
||||
function writeToControlPoint(opCode, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = new Uint8Array([opCode].concat(params));
|
||||
|
||||
if (!controlPointChar) {
|
||||
log("Control Point characteristic not found! Reconnecting...");
|
||||
return;
|
||||
}
|
||||
|
||||
const menu = {
|
||||
'' : {'title' : 'CoreTemp sensor'},
|
||||
'< Back' : back,
|
||||
'Enabled' : {
|
||||
value : !!s.enabled,
|
||||
onchange : v => {
|
||||
s.enabled = v;
|
||||
updateSettings();
|
||||
// Temporary handler to capture the response
|
||||
function handleResponse(event) {
|
||||
let response = new Uint8Array(event.target.value.buffer);
|
||||
//let responseOpCode = response[0];
|
||||
let requestOpCode = response[1]; // Matches the sent OpCode
|
||||
let resultCode = response[2]; // 0x01 = Success
|
||||
controlPointChar.removeListener("characteristicvaluechanged", handleResponse);
|
||||
if (requestOpCode === opCode) {
|
||||
if (resultCode === 0x01) { //successful
|
||||
resolve(response);
|
||||
} else {
|
||||
reject("Error Code: " + resultCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
E.showMenu(menu);
|
||||
controlPointChar.on("characteristicvaluechanged", handleResponse);
|
||||
controlPointChar.writeValue(data)
|
||||
.then(() => log("Sent OpCode:", opCode.toString(16), "Params:", data))
|
||||
.catch(error => {
|
||||
log("Write error:", error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
let gatt;
|
||||
function cacheDevice(deviceName) {
|
||||
let promise;
|
||||
let filters;
|
||||
characteristics = [];
|
||||
filters = [{ name: deviceName }];
|
||||
log("Requesting device with filters", filters);
|
||||
promise = NRF.requestDevice({ filters: filters, active: settings.active });
|
||||
promise = promise.then((d) => {
|
||||
E.showMessage("Found!!\n" + deviceName + "\nConnecting...");
|
||||
log("Got device", d);
|
||||
gatt = d.gatt;
|
||||
log("Connecting...");
|
||||
d.on('gattserverdisconnected', function () {
|
||||
CORECONNECTED = false;
|
||||
log("Disconnected! ");
|
||||
gatt = null;
|
||||
//setTimeout(() => cacheDevice(deviceName), 5000); // Retry in 5 seconds
|
||||
});
|
||||
return gatt.connect().then(function () {
|
||||
log("Connected.");
|
||||
});
|
||||
});
|
||||
promise = promise.then(() => {
|
||||
log(JSON.stringify(gatt.getSecurityStatus()));
|
||||
if (gatt.getSecurityStatus().bonded) {
|
||||
log("Already bonded");
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
log("Start bonding");
|
||||
return gatt.startBonding()
|
||||
.then(() => log("Security status after bonding" + gatt.getSecurityStatus()));
|
||||
}
|
||||
});
|
||||
promise = promise.then(() => {
|
||||
log("Getting services");
|
||||
return gatt.getPrimaryServices();
|
||||
});
|
||||
|
||||
promise = promise.then((services) => {
|
||||
log("Got services", services.length);
|
||||
let result = Promise.resolve();
|
||||
for (let service of services) {
|
||||
if (!(supportedServices.includes(service.uuid))) continue;
|
||||
log("Supporting service", service.uuid);
|
||||
result = attachServicePromise(result, service);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
return promise.then(() => {
|
||||
log("Connection established, saving cache");
|
||||
E.showMessage("Found " + deviceName + "\nConnected!");
|
||||
CORECONNECTED = true;
|
||||
characteristicsToCache(characteristics);
|
||||
});
|
||||
}
|
||||
|
||||
function ConnectToDevice(d) {
|
||||
E.showMessage("Connecting...");
|
||||
let count = 0;
|
||||
const successHandler = () => {
|
||||
E.showMenu(buildMainMenu());
|
||||
};
|
||||
const errorHandler = (e) => {
|
||||
count++;
|
||||
log("ERROR", e);
|
||||
if (count <= 10) {
|
||||
E.showMessage("Error during caching\nRetry " + count + "/10", e);
|
||||
return cacheDevice(d).then(successHandler).catch(errorHandler);
|
||||
} else {
|
||||
E.showAlert("Error during caching", e).then(() => {
|
||||
E.showMenu(buildMainMenu());
|
||||
});
|
||||
}
|
||||
};
|
||||
return cacheDevice(d).then(successHandler).catch(errorHandler);
|
||||
}
|
||||
/*
|
||||
function getPairedAntHRM() {
|
||||
writeToControlPoint(0x04) // Get paired HRMs
|
||||
.then(response => {
|
||||
let totalHRMs = response[3]; // HRM count at index 3
|
||||
log("📡 PAIRED ANT+:", totalHRMs);
|
||||
let promises = [];
|
||||
let hrmFound = [];
|
||||
for (let i = 0; i < totalHRMs; i++) {
|
||||
promises.push(
|
||||
writeToControlPoint(0x05, [i]) // Get HRM ID from paired list
|
||||
.then(hrmResponse => {
|
||||
log("🔍 Response 0x05:", hrmResponse);
|
||||
|
||||
let byte1 = hrmResponse[3]; // LSB
|
||||
let byte2 = hrmResponse[4]; // Middle Byte
|
||||
let byte3 = hrmResponse[5]; // MSB
|
||||
let txType = hrmResponse[5]; // Transmission Type
|
||||
let hrmState = hrmResponse[6]; // Connection State
|
||||
let pairedAntId = (byte1) | (byte2 << 8) | (byte3 << 16); // ✅ Corrected parsing
|
||||
let stateText = ["Closed", "Searching", "Synchronized", "Reserved"][hrmState & 0x03];
|
||||
log(`🔗 HRM ${i}: ANT ID = ${pairedAntId}, Tx-Type = ${txType}, State = ${stateText}`);
|
||||
hrmFound.push({ index: i, antId: pairedAntId, txType: txType, stateText: stateText });
|
||||
})
|
||||
.catch(e => log(`❌ Error fetching HRM ${i} ID:`, e))
|
||||
);
|
||||
}
|
||||
return Promise.all(promises).then(() => hrmFound);
|
||||
})
|
||||
.then(allHRMs => {
|
||||
log("Retrieved all paired HRMs:", allHRMs);
|
||||
return // Modified start scanning command
|
||||
})
|
||||
}
|
||||
*/
|
||||
function clearPairedHRM_ANT() {
|
||||
return writeToControlPoint(0x01) // Send OpCode 0x01 to clear list
|
||||
.then(response => {
|
||||
let resultCode = response[2]; // Check the success flag
|
||||
if (resultCode === 0x01) {
|
||||
log("ANT+ HRM list cleared successfully.");
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
log("Failed to clear ANT+ HRM list. Error code:", resultCode);
|
||||
return Promise.reject(new Error(`Error code: ${resultCode}`));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
log("Error clearing ANT+ HRM list:", error);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
function scanUntilSynchronized(maxRetries, delay) {
|
||||
let attempts = 0;
|
||||
function checkHRMState() {
|
||||
if (attempts >= maxRetries) {
|
||||
log("Max scan attempts reached. HRM did not synchronize.");
|
||||
E.showAlert("Max scan attempts reached. HRM did not synchronize.").then(() => E.showMenu(HRM_MENU()));
|
||||
return;
|
||||
}
|
||||
log(`Attempt ${attempts + 1}/${maxRetries}: Checking HRM state...`);
|
||||
writeToControlPoint(0x05, [0]) // Check paired HRM state
|
||||
.then(hrmResponse => {
|
||||
log("Sent OpCode: 0x05, response: ", hrmResponse);
|
||||
let byte1 = hrmResponse[3]; // LSB of ANT ID
|
||||
let byte2 = hrmResponse[4]; // MSB of ANT ID
|
||||
let txType = hrmResponse[5]; // Transmission Type
|
||||
let hrmState = hrmResponse[6]; // HRM State
|
||||
let retrievedAntId = (byte1) | (byte2 << 8) | (txType << 16);
|
||||
let stateText = ["Closed", "Searching", "Synchronized", "Reserved"][hrmState & 0x03];
|
||||
log(`HRM Status: ANT ID = ${retrievedAntId}, Tx-Type = ${txType}, State = ${stateText}`);
|
||||
E.showAlert(`HRM Status\nANT ID = ${retrievedAntId}\nState = ${stateText}`).then(() => E.showMenu(HRM_MENU()));
|
||||
if (stateText === "Synchronized") {
|
||||
return;
|
||||
} else {
|
||||
log(`HRM ${retrievedAntId} is not yet synchronized. Scanning again...`);
|
||||
// Start scan again
|
||||
writeToControlPoint(0x0D)
|
||||
.then(() => writeToControlPoint(0x0A, [0xFF]))
|
||||
.then(() => {
|
||||
attempts++;
|
||||
setTimeout(checkHRMState, delay); // Wait and retry
|
||||
})
|
||||
.catch(error => {
|
||||
log("Error restarting scan:", error);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
log("Error checking HRM state:", error);
|
||||
});
|
||||
}
|
||||
log("Starting scan to synchronize HRM...");
|
||||
writeToControlPoint(0x0A, [0xFF]) // Start initial scan
|
||||
.then(() => {
|
||||
setTimeout(checkHRMState, delay); // Wait and check state
|
||||
})
|
||||
.catch(error => {
|
||||
log("Error starting initial scan:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function scanHRM_ANT() {
|
||||
E.showMenu();
|
||||
E.showMessage("Scanning for 10 seconds"); // Increased scan time
|
||||
writeToControlPoint(0x0A, [0xFF])
|
||||
.then(response => {
|
||||
log("Received Response for 0x0A:", response);
|
||||
return new Promise(resolve => setTimeout(resolve, 10000)); // Extended scan time to 10 seconds
|
||||
})
|
||||
.then(() => {
|
||||
return writeToControlPoint(0x0B); // Get HRM count
|
||||
})
|
||||
.then(response => {
|
||||
let HRMCount = response[3];
|
||||
log("HRM Count Response:", HRMCount);
|
||||
let hrmFound = [];
|
||||
let promises = [];
|
||||
for (let i = 0; i < HRMCount; i++) {
|
||||
promises.push(
|
||||
writeToControlPoint(0x0C, [i]) // Get Scanned HRM IDs
|
||||
.then(hrmResponse => {
|
||||
log("Response 0x0C:", hrmResponse);
|
||||
let byte1 = hrmResponse[3]; // LSB
|
||||
let byte2 = hrmResponse[4]; // MSB
|
||||
let txType = hrmResponse[5]; // Transmission Type
|
||||
let scannedAntId = (byte1) | (byte2 << 8) | (txType << 16); //3 byte ANT+ ID
|
||||
log(`HRM ${i} ID Response: ${scannedAntId}`);
|
||||
hrmFound.push({ antId: scannedAntId });
|
||||
})
|
||||
.catch(e => log(`Error fetching HRM ${i} ID:`, e))
|
||||
);
|
||||
}
|
||||
return Promise.all(promises).then(() => {
|
||||
if (hrmFound > 0) {
|
||||
let submenu_scan = {
|
||||
'< Back': function () { E.showMenu(buildMainMenu()); }
|
||||
};
|
||||
hrmFound.forEach((hrm) => {
|
||||
let id = hrm.antId;
|
||||
submenu_scan[id] = function () {
|
||||
E.showPrompt("Connect to\n" + id + "?", { title: "ANT+ Pairing" }).then((r) => {
|
||||
if (r) {
|
||||
E.showMessage("Connecting...");
|
||||
let byte1 = id & 0xFF; // LSB
|
||||
let byte2 = (id >> 8) & 0xFF; // Middle byte
|
||||
let byte3 = (id >> 16) & 0xFF; // Transmission Type
|
||||
return clearPairedHRM_ANT(). //FIRST CLEAR ALL ANT+ HRM
|
||||
then(() => { writeToControlPoint(0x02, [byte1, byte2, byte3]) }) // Pair the HRM
|
||||
.then(() => {
|
||||
log(`HRM ${id} added to paired list.`);
|
||||
writeSettings("ANT_HRM", hrm);
|
||||
E.showMenu(HRM_MENU());
|
||||
})
|
||||
.catch(e => log(`Error adding HRM ${id} to paired list:`, e));
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
E.showMenu(submenu_scan);
|
||||
} else {
|
||||
E.showAlert("No ANT+ HRM found.").then(() => E.showMenu(HRM_MENU()));
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(e => log("ERROR:", e));
|
||||
}
|
||||
|
||||
function buildMainMenu() {
|
||||
let mainmenu = {
|
||||
'': { 'title': 'CORE Sensor' },
|
||||
'< Back': back,
|
||||
'Enable': {
|
||||
value: !!settings.enabled,
|
||||
onchange: v => {
|
||||
writeSettings("enabled", v);
|
||||
},
|
||||
},
|
||||
'Widget': {
|
||||
value: !!settings.widget,
|
||||
onchange: v => {
|
||||
writeSettings("widget", v);
|
||||
},
|
||||
}
|
||||
};
|
||||
if (settings.btname || settings.btid) {
|
||||
let name = "Clear " + (settings.btname || settings.btid);
|
||||
mainmenu[name] = function () {
|
||||
E.showPrompt("Clear current device?").then((r) => {
|
||||
if (r) {
|
||||
writeSettings("btname", undefined);
|
||||
writeSettings("btid", undefined);
|
||||
writeSettings("cache", undefined);
|
||||
if(gatt) gatt.disconnect();
|
||||
}
|
||||
E.showMenu(buildMainMenu());
|
||||
});
|
||||
};
|
||||
if(!CORECONNECTED){
|
||||
let connect = "Connect " + (settings.btname || settings.btid);
|
||||
mainmenu[connect] = function () {ConnectToDevice(settings.btname)};
|
||||
}else{
|
||||
mainmenu['HRM Settings'] = function () { E.showMenu(HRM_MENU()); };
|
||||
}
|
||||
} else {
|
||||
mainmenu['Scan for CORE'] = function () { ScanForCORESensor(); };
|
||||
}
|
||||
mainmenu['Debug'] = function () { E.showMenu(submenu_debug); };
|
||||
return mainmenu;
|
||||
}
|
||||
let submenu_debug = {
|
||||
'': { title: "Debug" },
|
||||
'< Back': function () { E.showMenu(buildMainMenu()); },
|
||||
'Alert on disconnect': {
|
||||
value: !!settings.warnDisconnect,
|
||||
onchange: v => {
|
||||
writeSettings("warnDisconnect", v);
|
||||
}
|
||||
},
|
||||
'Debug log': {
|
||||
value: !!settings.debuglog,
|
||||
onchange: v => {
|
||||
writeSettings("debuglog", v);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function HRM_MENU() {
|
||||
let menu = {
|
||||
'': { 'title': 'CORE: HR' },
|
||||
'< Back': function () { E.showMenu(buildMainMenu()); },
|
||||
'Scan for ANT+': function () { scanHRM_ANT(); }
|
||||
}
|
||||
if (settings.btname) {
|
||||
menu['ANT+ Status'] = function () { scanUntilSynchronized(10, 3000); },
|
||||
menu['Clear ANT+'] = function () {
|
||||
E.showPrompt("Clear ANT+ HRs?", { title: "CLear ANT+" }).then((r) => {
|
||||
if (r) {
|
||||
clearPairedHRM_ANT();
|
||||
}
|
||||
E.showMenu(HRM_MENU());
|
||||
});
|
||||
}
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
|
||||
function ScanForCORESensor() {
|
||||
E.showMenu();
|
||||
E.showMessage("Scanning for 5 seconds");
|
||||
let submenu_scan = {
|
||||
'< Back': function () { E.showMenu(buildMainMenu()); }
|
||||
};
|
||||
NRF.findDevices(function (devices) {
|
||||
submenu_scan[''] = { title: `Scan (${devices.length} found)` };
|
||||
if (devices.length === 0) {
|
||||
E.showAlert("No devices found")
|
||||
.then(() => E.showMenu(buildMainMenu()));
|
||||
return;
|
||||
} else {
|
||||
devices.forEach((d) => {
|
||||
log("Found device", d);
|
||||
let shown = (d.name || d.id.substr(0, 17));
|
||||
submenu_scan[shown] = function () {
|
||||
E.showPrompt("Connect to\n" + shown + "?", { title: "Pairing" }).then((r) => {
|
||||
if (r) {
|
||||
E.showMessage("Connecting...");
|
||||
let count = 0;
|
||||
const successHandler = () => {
|
||||
E.showPrompt("Success!", {
|
||||
buttons: { "OK": true }
|
||||
}).then(() => {
|
||||
writeSettings("btid", d.id);
|
||||
writeSettings("btname", d.name); //Seems to only like to connect by name
|
||||
E.showMenu(HRM_MENU());
|
||||
});
|
||||
};
|
||||
const errorHandler = (e) => {
|
||||
count++;
|
||||
log("ERROR", e);
|
||||
if (count <= 10) {
|
||||
E.showMessage("Error during caching\nRetry " + count + "/10", e);
|
||||
return cacheDevice(d.name).then(successHandler).catch(errorHandler);
|
||||
} else {
|
||||
E.showAlert("Error during caching", e).then(() => {
|
||||
E.showMenu(buildMainMenu());
|
||||
});
|
||||
}
|
||||
};
|
||||
return cacheDevice(d.name).then(successHandler).catch(errorHandler);
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
E.showMenu(submenu_scan);
|
||||
}, { timeout: 5000, active: true, filters: [{ services: ["00002100-5b1e-4347-b07c-97b514dae121"] }] });
|
||||
}
|
||||
|
||||
function init() {
|
||||
E.showMenu();
|
||||
E.showMenu(buildMainMenu());
|
||||
}
|
||||
init();
|
||||
})
|
||||
|
|
@ -1,56 +1,48 @@
|
|||
// TODO Change to a generic multiple sensor widget?
|
||||
|
||||
(() => {
|
||||
var settings = {};
|
||||
var count = 0;
|
||||
var core = 0;
|
||||
|
||||
var CORESensorStatus = false;
|
||||
// draw your widget
|
||||
function draw() {
|
||||
if (!settings.enabled)
|
||||
if (!settings.widget)
|
||||
return;
|
||||
g.reset();
|
||||
g.setFont("6x8", 1).setFontAlign(0, 0);
|
||||
g.setFontAlign(0, 0);
|
||||
g.clearRect(this.x, this.y, this.x + 23, this.y + 23);
|
||||
|
||||
if (count & 1) {
|
||||
if (CORESensorStatus) {
|
||||
g.setColor("#0f0"); // green
|
||||
} else {
|
||||
g.setColor(g.theme.dark ? "#333" : "#CCC"); // off = grey
|
||||
}
|
||||
|
||||
g.drawImage(
|
||||
atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),
|
||||
atob("FBSCAAAAADwAAAPw/8AAP/PD8AP/wwDwD//PAPAP/APA8D/AA//wP8AA/8A/AAAAPP8AAAD8/wAAAPz/AAAA/D8AAAAAP8AAA/A/8AAP8A/8AD/wD///z8AD///PAAA///AAAAP/wAA="),
|
||||
this.x + (24 - 12) / 2, this.y + 1);
|
||||
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawString(parseInt(core)+"\n."+parseInt((core*100)%100), this.x + 24 / 2, this.y + 18);
|
||||
|
||||
g.setColor(-1);
|
||||
}
|
||||
|
||||
// Set a listener to 'blink'
|
||||
function onTemp(temp) {
|
||||
count = count + 1;
|
||||
core = temp.core;
|
||||
WIDGETS["coretemp"].draw();
|
||||
}
|
||||
|
||||
// Called by sensor app to update status
|
||||
function reload() {
|
||||
settings = require("Storage").readJSON("coretemp.json", 1) || {};
|
||||
|
||||
Bangle.removeListener('CoreTemp', onTemp);
|
||||
|
||||
if (!settings.widget) {
|
||||
delete WIDGETS["coretemp"];
|
||||
return;
|
||||
}
|
||||
if (settings.enabled) {
|
||||
WIDGETS["coretemp"].width = 24;
|
||||
Bangle.on('CoreTemp', onTemp);
|
||||
} else {
|
||||
WIDGETS["coretemp"].width = 0;
|
||||
count = 0;
|
||||
WIDGETS["CORESensor"].width = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (Bangle.hasOwnProperty("isCORESensorConnected")) {
|
||||
setInterval(function () {
|
||||
if (Bangle.isCORESensorConnected() != CORESensorStatus) {
|
||||
CORESensorStatus = Bangle.isCORESensorConnected();
|
||||
WIDGETS["coretemp"].draw();
|
||||
}
|
||||
}, 10000); //runs every 10 seconds
|
||||
}
|
||||
// add the widget
|
||||
WIDGETS["coretemp"] = {
|
||||
area: "tl",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
0.01: First version of GPS Setup app
|
||||
0.02: Created gppsetup module
|
||||
0.03: Added support for Bangle.js2
|
||||
0.04: Added adaptive option for PSMOO with Bangle.js2
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ used. These settings will remain for all apps that use the GPS.
|
|||
|
||||
- fix_req (Bangle.js2 only) - the number of fixes required before the GPS turns off until next search for GPS signal. default is 1.
|
||||
|
||||
- adaptive (Bangle.js2 only) - When a GPS signal is acquired, this can reduce the time in seconds until next scan to generate higher temporal resolution of gps fixes. Off if set to 0.
|
||||
## Module
|
||||
|
||||
A module is provided that'll allow you to set GPS configuration from your own
|
||||
|
|
@ -69,11 +70,13 @@ app. To use it:
|
|||
// needed unless the watch's battery has run down
|
||||
require("gpssetup").setPowerMode();
|
||||
|
||||
// This sets up the PSMOO mode. update/search are optional in seconds
|
||||
// This sets up the PSMOO mode. update/search/adaptive are optional in seconds
|
||||
require("gpssetup").setPowerMode({
|
||||
power_mode:"PSMOO",
|
||||
update:optional (default 120),
|
||||
search:optional (default 5)})
|
||||
search:optional (default 5),
|
||||
adaptive: optional (default 0)
|
||||
});
|
||||
|
||||
// This sets up SuperE
|
||||
require("gpssetup").setPowerMode({power_mode:"SuperE"})
|
||||
|
|
|
|||
|
|
@ -165,9 +165,11 @@ function setupPSMOO(settings) {
|
|||
var gpsTimeout = null;
|
||||
var gpsActive = false;
|
||||
var fix = 0;
|
||||
var lastFix = false;
|
||||
function cb(f){
|
||||
if(parseInt(f.fix) === 1){
|
||||
fix++;
|
||||
lastFix = true;
|
||||
if(fix >= settings.fix_req){
|
||||
fix = 0;
|
||||
turnOffGPS();
|
||||
|
|
@ -180,9 +182,14 @@ function setupPSMOO(settings) {
|
|||
clearTimeout(gpsTimeout);
|
||||
Bangle.setGPSPower(0,settings.appName);
|
||||
Bangle.removeListener('GPS', cb); // cleaning it up
|
||||
var timeout = settings.update * 1000;
|
||||
if(lastFix && settings.adaptive > 0){
|
||||
timeout = settings.adaptive * 1000;
|
||||
}
|
||||
lastFix = false;
|
||||
gpsTimeout = setTimeout(() => {
|
||||
turnOnGPS();
|
||||
}, settings.update * 1000);
|
||||
}, timeout);
|
||||
}
|
||||
function turnOnGPS(){
|
||||
if (gpsActive) return;
|
||||
|
|
@ -213,12 +220,14 @@ exports.setPowerMode = function(options) {
|
|||
if (options) {
|
||||
if (options.update) settings.update = options.update;
|
||||
if (options.search) settings.search = options.search;
|
||||
if (options.adaptive) settings.adaptive = options.adaptive;
|
||||
if (options.fix_req) settings.fix_req = options.fix_req;
|
||||
if (options.power_mode) settings.power_mode = options.power_mode;
|
||||
if (options.appName) settings.appName = options.appName;
|
||||
}
|
||||
settings.update = settings.update||120;
|
||||
settings.search = settings.search||5;
|
||||
settings.adaptive = settings.adaptive||0;
|
||||
settings.fix_req = settings.fix_req||1; //default to just one fix and will turn off
|
||||
settings.power_mode = settings.power_mode||"SuperE";
|
||||
settings.appName = settings.appName||"gpssetup";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "gpssetup",
|
||||
"name": "GPS Setup",
|
||||
"shortName": "GPS Setup",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "Configure the GPS power options and store them in the GPS nvram",
|
||||
"icon": "gpssetup.png",
|
||||
"tags": "gps,tools,outdoors",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
0.01: New App!
|
||||
0.05: Added functionality for incorporating BTHRM
|
||||
0.06: Modified BLE Broadcasting; Includes Optical HR and Wrist Temperature
|
||||
0.07: Fixing storage overloaded issue
|
||||
0.08: Added scrolling to Surveys. CORESensor app added.
|
||||
0.09: Added High Temporal Accelerometry logging (x,y,z per second)
|
||||
0.10: Fixes to settings.js to account for optimizations in studyTasks.json
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
# HeatSuite Watch Application
|
||||
|
||||
This is the HeatSuite Watch Application which allows for seemless integration into the HeatSuite platform ([read the docs](https://heatsuitelabs.github.io/HeatSuiteDocs/) and our [research](#research-using-heatsuite)). You may use this watch application independent of the full(er) HeatSuite platform.
|
||||
|
||||
## What is HeatSuite?
|
||||
|
||||
HeatSuite is a comprehensive all in one solution for researchers to monitor the physiological, behavioural, and perceptual responses of individuals and their personal environmental exposure. Learn more on the details of the HeatSuite platform by [reading the docs](https://heatsuitelabs.github.io/HeatSuiteDocs/).
|
||||
|
||||
## Why do we need this?
|
||||
|
||||
Consumer and research-based wearables have largely determined what researchers can measure in the field, and/or have proprietary postprocessing embedded into the hardware/software stack which limits transparency and transferrability of data collected. HeatSuite challenges this one-sided relationship, offering a solution for researchers who desire access and awareness of how and what data they are collecting from *their* participants.
|
||||
|
||||
## Watch Specific Features
|
||||
|
||||
This is a list of current features available when using the HeatSuite Watch Application:
|
||||
|
||||
+ Per minute averaging and/or sum of onboard watch sensor data (Heart rate, barometer temperature and pressure, accelerometer, battery)
|
||||
+ High temporal resolution accelerometer logging (x,y,z per second)
|
||||
+ Can connect external bluetooth devices for added physiological monitoring (e.g. Bluetooth Heart Rate, CORE Sensor) - more being added
|
||||
+ Connect and store data from other devices including:
|
||||
+ Blood Pressure Monitor (A&D Medical UA651-BLE)
|
||||
+ Oral Temperature using custom dongle - Contact [Nicholas Ravanelli, PhD](emailto:nick.ravanelli@gmail.com)
|
||||
+ Body Mass Scale (Xiaomi Composition Scale 2)
|
||||
+ Collect perceptions and behaviour using ecological momentary assessments with onboard questionnaires
|
||||
+ Mictruition frequency and color analysis for index of hydration status
|
||||
+ Create study schedules for participants to receive programmatic nudges daily, specific to each task
|
||||
+ Programmatic GPS monitoring, with adaptive power switching for battery optimization
|
||||
+ Fall Detection and bluetooth broadcasting (beta)
|
||||
|
||||
## I just installed HeatSuite and I see a bunch of options. What do they mean?
|
||||
|
||||
HeatSuite uses a default studyTasks.json - visible and fully editable in the `Tasks` tab when installing HeatSuite on the watch. You can customize this to your liking by following the instructions [in the docs](https://heatsuitelabs.github.io/HeatSuiteDocs/watchapp/watchapp-tasks/). Some specific details are provided in this readme.
|
||||
|
||||
## What do the button colors mean?
|
||||
|
||||

|
||||
|
||||
The colors of each of the buttons provide the user some feedback:
|
||||
|
||||
+ <font color="green">Green</font> Means the task has been completed recently, or the user can press the button to navigate to or do a task.*
|
||||
+ <font color="red">Red</font> means that the task can't be completed yet because another step needs to be done. This example shows that the external devices have yet to be paired with the watch.
|
||||
+ <font color="yellow">Yellow</font> means you are scheduled to do this task.
|
||||
|
||||
_*Note: If the task requires a bluetooth device, the app will scan for the device and automatically handle the task once found._
|
||||
|
||||
## What does `Searching...` at the bottom of the HeatSuite app layout mean?
|
||||
|
||||
The `Searching...` text at the bottom of the screen shows that the watch is searching for devices that it may need to connect to via Bluetooth. This text will change when a device is found, and the appropriate handling of the task will ensue. As Bluetooth scanning drains the battery, the HeatSuite App will timeout after 3 minutes and revert back to the clock.
|
||||
|
||||
_Note: This will only show when a bluetooth device is associated with a task._
|
||||
|
||||
## Why does swiping right open the HeatSuite App?
|
||||
|
||||
The objective of HeatSuite was to make data collection in the field easier for participants. By default, swiping right when the HeatSuite widget is visible will open the app. This can be toggled off in the app settings.
|
||||
|
||||
## Can I request or add a feature?
|
||||
|
||||
Certainly!! To help ensure that the release on BangleApps is always functional and works with the other devices within the HeatSuite ecosystem, please make any feature requests or code updates to the forked version within the [HeatSuiteLabs repository](https://github.com/HeatSuiteLabs/BangleApps). That is our testing bed before we push to the official Bangle Apps git.
|
||||
|
||||
## Watch accelerometer data
|
||||
|
||||
The current interation of the HeatSuite application provides the option to average the accelerometer `x,y,z` every second or as needed (known as High Temporal Resolution Accelerometer Logging), and/or magnitude per minute. Magnitude is calculated in the Espruino firmware as:
|
||||
|
||||
```
|
||||
sqrt(x^2 + y^2 + z^2)
|
||||
```
|
||||
|
||||
To transform this to [Euclidean norm minus one (ENMO)](https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0061691) format:
|
||||
|
||||
```
|
||||
ENMO = acc_avg - 1
|
||||
```
|
||||
Where `acc_avg` is the average acceleration magnitude per minute, available in the CSV file. While this may be satisfactory for offline long term monitoring (+2 weeks), it is recommended to use per second `mag` data.
|
||||
|
||||
Previous research has demonstrated strong agreement between the onboard accelerometer of the Bangle.js2 and a research grade ActiGraph GT9X;
|
||||
|
||||
Van Laerhoven, K., Hoelzemann, A., Pahmeier, I. et al. Validation of an open-source ambulatory assessment system in support of replicable activity studies. *Ger J Exerc Sport Res* 52, 262–272 (2022). https://doi.org/10.1007/s12662-022-00813-2
|
||||
|
||||
## Applications/modules that HeatSuite integrates:
|
||||
|
||||
* [BTHRM](https://banglejs.com/apps/#bthrm)
|
||||
* [coretemp](https://banglejs.com/apps/#coretemp)
|
||||
* [gpssetup](https://banglejs.com/apps/#gpssetup)
|
||||
* [recorder](https://banglejs.com/apps/#recorder) (modified in HeatSuite code to incorporate per minute averaging)
|
||||
|
||||
## Icons
|
||||
|
||||
HeatSuite uses icons from [Flaticon.com](https://www.flaticon.com) & [Freepik.com](https://www.freepik.com)
|
||||
|
||||
## Research Using HeatSuite
|
||||
|
||||
A full list of peer-reviewed research and conference preceedings using HeatSuite can be found [here](https://heatsuitelabs.github.io/HeatSuiteDocs/research/)
|
||||
|
||||
## To Do
|
||||
|
||||
* Finish Download All and Delete All functions on custom.html
|
||||
* Graphical User Interface for EMA and Task Development
|
||||
|
||||
## Creator
|
||||
|
||||
[Nicholas Ravanelli, PhD](https://github.com/nravanelli)
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4kC///A4IDBo37x1zhljkEAvvv+ecjnn8n6oMI7//mGEsMIwUQkQABCwIAOgQUCCyQYGBpEM4AKIFxmTnIwLBZED0c6mAwJFxMD1l6C5MCC5U84YXKA43Iw5HFvGMTBkJ+MbwEMnWj4EI2MfuAXLgkV6NnGAIuBuMdqNAC5cP28PjIXDyPwvdQCpLPBgsVxcVyAXBhNR3FRC4MMCw2F+uQL4MR+P9p/970fiMfBYNfrIWEg9h+P5gGUrHF2Me8MbrmFpMAz8fjZ7EhsdgsYA4eb3IBBA4eBqFB6CLEjEFjp7E897+AHDoNAwIXEhNhsMRrPIxJ7Bi0ROYOYxmViMWi2QMAmLt9PiIaBwEA+1lgEICgMR+n29CnI6Mf6MXT4IHBa4PxKYKnHLYe8a4QACyNQMYoAHa4OGiqcCg/xa4YAHyczZYPhiOx/tf+tLjbXDmc5CwkM1U6m6HBpF02MbAIPcwiXBuej1RoBAAUI1MD1LXE8+We4IACzUwxSnECoIXG+4BBC4uaC4kHI4d47PNI4Uexmdw5HDQQR3G+MR8O1O4Nbj53KU4tBCAIGBDwNkqNAX5bXXaoOBi4HDuMYqNoChGf3+PiIAB6EAre1gEEBAX4/auEKgOxBYP5vFJMYIGBqEAymHz4GBjaPEgkY4sUcAcP3ObLYcMoNc6I7BC4kFjAHDv23vbXEwNQC4sJs329MAvtJ4nxj4BB6GU68Az1vizvEgGP3+Aa4Xx2h3BpbXDhH7/CPFhhmBa4PWiuQgcwhNRtrXCg7XK27XCC4LXDSYIAKF4LXCC4TXCF4IAKL4MW6EMnWj4EEsJfBC5cA5tZFwOjnQwByuMCA0gDRED1nKC4IAHgQXXkQXJI4YAHkUiGBEMmcz4AuJAAIjIhgWIFwQwKABIuDDCQWDA"))
|
||||
|
|
@ -0,0 +1,614 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||
<style>
|
||||
.jsoneditor-container {
|
||||
height: 500px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.editor-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ul class="tab tab-block" id="tab-navigate">
|
||||
<li class="tab-item active" id="tab-settingsContainer">
|
||||
<a href="javascript:showTab('settingsContainer')">Settings</a>
|
||||
</li>
|
||||
<li class="tab-item" id="tab-tasksContainer">
|
||||
<a href="javascript:showTab('tasksContainer')">Tasks</a>
|
||||
</li>
|
||||
<li class="tab-item" id="tab-surveyContainer">
|
||||
<a href="javascript:showTab('surveyContainer')">EMA</a>
|
||||
</li>
|
||||
<li class="tab-item" id="tab-downloadData">
|
||||
<a href="javascript:showTab('downloadData')">
|
||||
<div id="downloadData-tab-label" class="" data-badge="0">Download
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="container apploader-tab" id="settingsContainer">
|
||||
<form id="heatsuiteSettings">
|
||||
<div class="row pt-2"><strong>Recorder Options:</strong>
|
||||
<br>Select what sensor/data you want to record and average each minute:
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="steps">
|
||||
<i class="form-icon"></i> Steps
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="hrm">
|
||||
<i class="form-icon"></i> Optical Heart Rate
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="acc">
|
||||
<i class="form-icon"></i> Accelerometry (per minute magnitude)
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="bat">
|
||||
<i class="form-icon"></i> Battery
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="movement">
|
||||
<i class="form-icon"></i> Movement
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="baro">
|
||||
<i class="form-icon"></i> Temperature/Pressure
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="bthrm">
|
||||
<i class="form-icon"></i> Bluetooth HRM (Uses BTHRM app/module)
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="record" value="CORESensor">
|
||||
<i class="form-icon"></i> CORE Sensor (Uses coretemp app/module)
|
||||
</label>
|
||||
</div>
|
||||
<div class="row pt-2"><strong>High Resolution Accelerometer Data:</strong>
|
||||
<br>If you want to record high temporal resolution accelerometer data (magnitude of x,y,z; can store up to 4 days of data at 1 second, will rollover).
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="highAcc">
|
||||
<i class="form-icon"></i> High Temporal Accelerometry √(x² + y² + z²)
|
||||
</label>
|
||||
<label class="form-label" for="input-AccLogInt">Interval (seconds)</label>
|
||||
<input class="form-input" type="number" name="AccLogInt" id="input-AccLogInt" value=1 min=1>
|
||||
<p class="form-input-hint mb-0">Interval for logging averaged magnitude and sum from accelerometer - default is 1 second
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-2"><strong>GPS PSMOO Options:</strong>
|
||||
<br>Option to have GPS work in PSMOO (uses GPSSetup app/module)
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="GPS">
|
||||
<i class="form-icon"></i> Turn GPS On
|
||||
</label>
|
||||
<label class="form-label" for="input-GPSScanTime">Scan Time (mins)</label>
|
||||
<input class="form-input" type="number" name="GPSScanTime" id="input-GPSScanTime" value=2 min=0>
|
||||
<p class="form-input-hint mb-0">The time spent scanning for a GPS signal.</strong></p>
|
||||
<label class="form-label" for="input-GPSInterval">Scan Interval (mins)</label>
|
||||
<input class="form-input" type="number" name="GPSInterval" id="input-GPSInterval" value=10 min=0>
|
||||
<p class="form-input-hint mb-0">The time between scans when a <strong>signal is not acquired.</strong></p>
|
||||
<label class="form-label" for="input-GPSAdaptiveTime">Adaptive Time (mins)</label>
|
||||
<input class="form-input" type="number" name="GPSAdaptiveTime" id="input-GPSAdaptiveTime" value=2 min=0>
|
||||
<p class="form-input-hint mb-0">The time between scans when a <strong>signal is acquired.</strong></p>
|
||||
</div>
|
||||
<div class="row pt-2"><strong>Extras:</strong>
|
||||
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="fallDetect">
|
||||
<i class="form-icon"></i> Fall Detection (beta)
|
||||
</label>
|
||||
<p class="form-input-hint mb-0">Will detect falls.</p>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="surveyRandomize">
|
||||
<i class="form-icon"></i> Randomize EMA questions
|
||||
</label>
|
||||
<p class="form-input-hint mb-0">Enable this if you want your questions to be shuffled at random each time.</p>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" name="swipeOpen" checked>
|
||||
<i class="form-icon"></i> Swipe to Launch HeatSuite App
|
||||
</label>
|
||||
<p class="form-input-hint mb-0">On by default. Will enable quick access to the HeatSuite app for participants by simply swiping right on the screen.</p>
|
||||
<label class="form-label" for="input-studyID">Study ID:</label>
|
||||
<input class="form-input" type="text" name="studyID" id="input-studyID" value="" minlength="1" maxlength="4">
|
||||
<p class="form-input-hint mb-0">For communicating with HeatSuite Nodes. Maximum of 4 (no special) characters.
|
||||
</p>
|
||||
<label class="form-label" for="input-filePrefix">File Prefix</label>
|
||||
<input class="form-input" type="text" name="filePrefix" id="input-filePrefix" value="htst" minlength="1"
|
||||
maxlength="5">
|
||||
<p class="form-input-hint mb-0">ONLY CHANGE IF YOU KNOW WHAT YOU ARE DOING.</p>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="container apploader-tab pb-2" id="tasksContainer" style="display:none;">
|
||||
<div class="row pt-2"><strong>Edit your Task JSON File Editor:</strong>
|
||||
<br><a href="https://heatsuitelabs.github.io/HeatSuiteDocs/" target="_blank">Read the Docs</a> on how to properly format the JSON file. GUI coming soon to BangleApps.
|
||||
</div>
|
||||
|
||||
<div id="heatsuite_taskFile_editor" class="jsoneditor-container"></div>
|
||||
<button class="btn btn-primary" id="heatsuite_taskFile_resetBtn">Restore Default</button>
|
||||
</div>
|
||||
<div class="container apploader-tab pb-2" id="surveyContainer" style="display:none;">
|
||||
<div class="row pt-2"><strong>Ecological Momentary Assessment (EMA) JSON File Editor:</strong>
|
||||
<br><a href="https://heatsuitelabs.github.io/HeatSuiteDocs/" target="_blank">Read the Docs</a> on how to properly format the JSON file. GUI coming soon to BangleApps.
|
||||
</div>
|
||||
<div id="heatsuite_surveyFile_editor" class="jsoneditor-container"></div>
|
||||
<button class="btn btn-primary" id="heatsuite_surveyFile_resetBtn">Restore Default</button>
|
||||
</div>
|
||||
<div class="container apploader-tab pb-2" id="downloadData" style="display:none;">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File name</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="downloadData-tab-content">
|
||||
</tbody>
|
||||
<div id="progressElementHeatSuite" style="margin-top:10px; font-weight:bold;"></div>
|
||||
</table>
|
||||
</div>
|
||||
<p class="p-2 m-2"><button id="upload" class="btn btn-success">Upload</button></p>
|
||||
|
||||
<script src="../../core/lib/customize.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/jsoneditor@latest/dist/jsoneditor.min.css" rel="stylesheet" type="text/css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsoneditor@latest/dist/jsoneditor.min.js"></script>
|
||||
|
||||
<script>
|
||||
let HeatSuiteFileList = [];
|
||||
//default Schema
|
||||
let heatsuite__taskFile_defaultSchema = [
|
||||
{
|
||||
"id": "survey",
|
||||
"icon": 'require("heatshrink").decompress(atob("lEo4kA///6H7BIP2m9hjEpyQLBxeq0UAp3js1Z5F59GB9nChvL+83E4cCkQABkA6NgUq293rVCChsq6MRAAOcoQmM4ISCAAPqFBclCQkRjJpBExOxCYsR8goJkmRB4XuAYUVCZMsJYdEFgUZoATIpwTC1XnooFC2QTIoIhCC4OqCYVyMRGhBoXhjNLAoXSCZkR2mpAgXilGIwRnFCYlKPgd40czmeoCglJCYd5AgdYn////zCgknBwfZAgeqCQIUCwQTClQOD3oDCj1DCYf/nATHAAcawYTE/Q8CkoTHvE/+c6mY8FMYgACi9Kn84xAqDmSfC7euCYnE0f/1CKBPQRQBgWqkUk3zeCvWKBwJ4BnATFKQUipWq0koBoSyBxATDZYsCkUAOgk4AoYTGAAUjCYgmCUAgAFlQTDRQQTGgUowQuBJ4f/nQEDRYIiD81u0UAhAODmYECeAkCq1ms1nkECbIoACD4ImCCQIAB2QGBFAYADHYchs3nv1msguBY4IAD+bhBCYVGs261YTCMovz1GKnSfCCYIACtRXCxTHBeAJYBxGACYek8wnDSYWIxAjCdocls0eE4PiXhAAEhEWCQNhoATNgFOCYPkCRxABpdqIQQUP2gSQCYMiCaMAHRw="))',
|
||||
"cbBtn": "Bangle.load('heatsuite.survey.js');",
|
||||
"tod": [
|
||||
900,
|
||||
1200,
|
||||
1500,
|
||||
1800
|
||||
],
|
||||
"debounce": 300
|
||||
},
|
||||
{
|
||||
"id": "bloodPressure",
|
||||
"icon": 'require("heatshrink").decompress(atob("lEo4kA///zND//3BYOe98ggHGhEllM533ssW5odBhFOud642DhE10omCnczHSO3s3jmATP3MRjNrCh+2s0ZzNrCJkDn1mv1382X8YlMyMRi1xy1hzwSKhdxs12CYIWBu5RKm8WtMeCYfp8ATJ2MRiITDjPpsCUCu4ADv2w24TBjITCiOZE4MLCQgABve+MQMXCYeW2ATBv2ZAAeeve28w7Ey3mE4dCkQACx172INBtd+s1m81xJ4ITBpH/wn//oTB29pjIRBs1nAgITD7l3zGXv4nDsJRBiORz1hCYdCogBCCYWhiNusNutIWByA7EKIXXCYOq1QPBAAIZBiA7DvITCp4TDAAYTBHYp3CpgTNxvdAAX+CZl+9wAECZjDBbIQEBCZwlC9ITOy98zOcFAITOzl+vgTQvJSCJ6QWBCYMaCZED852CAAXj21pCgkRswTBgEzmc78/rAgMwm1usLvDz1m2ATBAAMD5+TAodpiIVBt3njN+CYvn8YGDnwnEu18uAMDgHpDQkAnfms1mtc+/4TFvngAwgxCAQV3EAt8AwoWFvIMF5gTKgF/HYv5mATKnPLAoc3+4TLh3J8ayCvieDKBN85LVBznJJxaSCvPM5nJuYSMCgW73ezBIoA="))',
|
||||
"tod": [
|
||||
830,
|
||||
1330,
|
||||
1930
|
||||
],
|
||||
"debounce": 3600,
|
||||
"btPair": true,
|
||||
"btInfo": {
|
||||
"service": "1810",
|
||||
"supported": [
|
||||
"A&D_UA-651"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "coreTemperature",
|
||||
"icon": 'require("heatshrink").decompress(atob("lEo4kA///A4Pf99fx3YtnjjEkkN/7+X0ujoFK/VusdBhH0ykIoXY2ozPjnBIyEBr2piATPiuq1WREx9aCYIoPjmaCYIoOgNo9ITC1gTMi3yvwoC9gmNw8nFAVRExmL5dyFAOlMZcBt+2223FAImN+9Emlru/cExlo6kzme8x9hJptkmYnBwwmN21DmdG3Am/TZF28YmPgvyl9rEx0AjsikX28wmNgFnkVyFAImNgHnl7pBvAmNgOyk/3u/2ExsM6UiEx8Ajd3u9/6omNgO3u/b8wSNgEeu974IRNEwPvu+xCRzrB+/+HBwABrF/8ISPhnXYBwAC5t9qImQ2/cCR8Ajm32ASPgPr5YmQj3eMCEB9qHQEycA5wmROoImRgAA=="))',
|
||||
"tod": [
|
||||
830,
|
||||
1330,
|
||||
1930
|
||||
],
|
||||
"TaskDesc": "coreTemperature_TaskDesc",
|
||||
"debounce": 3600,
|
||||
"btPair": true,
|
||||
"btInfo": {
|
||||
"service": "1809",
|
||||
"supported": [
|
||||
"BLEThermistorPod"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bodyMass",
|
||||
"icon": "require('heatshrink').decompress(atob('lEo4kB3//AAIJB7//wUQjnn485t97vvvoMIhlj9dasspptbz33kshiIApkUpylP/nGswAB5n/pOSkQSEl+3utVAAOI93uxAGCq96+QSCil1gczABdXyITBo4SDmt6ut61Wq09TBQU+ukRiVwDgcK5t1vvd7vc0ALD88hjgbDmcLvxOBAAOH2ALDgvBk5EEq1+wpfBrGHsoLDh1yyouEtN+q3M5llw+WvwMCgF5CYMFu9V3l2E4tn5dVu9Qh1ZCYN0/9E2s65phBMYWjrdE/9HCYOXmv2u9kqaLBRIKLDmtLu9vqF5y83o930lzWA83+4MCvMnmtKCZYgB2lQuQTBtmr5ijEAAc15mr+1euUqTYNESwgAFBgUO0Urgc+uwmIFAVn8fu2UW8czu2DCZM4s8z9FhCYVcE5fFmflsMcCAITP4MfCaPxigQB8yeIUAVumHloMZCYMLCZewCYORCYU32YTJndzCYUSEgM34p7BAA0+rgTBg8hCYM+ulE2oTHrdEo/g84TCmvLvdHgYSFmF009kuYTEu93tiNGDwN3vm+CYWjm9nCZvqkMR2E3pV6pQ7H1932lw9cRiNonGk+llMY9Wp9KwFWCYNFZgN3HQflrw8Du9emFUCYMqG41cdIIAEh2yCYMUbI1VqpSF89BCYMRo7FIAAc4ugSCiMZ2tY9wAGn3uwt7yITDiMp+1mAAtqrVmt+SCQgAl'))",
|
||||
"tod": [
|
||||
800
|
||||
],
|
||||
"debounce": 3600,
|
||||
"btPair": false,
|
||||
"btInfo": {
|
||||
"service": "181b",
|
||||
"supported": [
|
||||
"MIBFS"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "urine",
|
||||
"icon": "require('heatshrink').decompress(atob('lEo4kA///7/3oMInGVtdC//3rvn5lS7/Gx1rksZDYIXB7edkAFB4wqGhcukXgHp0Lkkz6eaCheykWq9Ux5nM42elWilwjH0c97VG5lVqtcjOp7s3JwIAEgU4xF6tgSBAAPBzXf/FACYsK7GD1vFCYdcjuIx+bCY/azgSDAANRyePzwTHzQ6EAAWax87CY2Z1I6EHgVqvNyMY0q1ATGqsX1XrRg1JoITH4dJRY0Ao5iGAAPN1MkogVFogTI42p1uIUItEngTJ0lE0AnQle7J59d1JjHoemO4/JoYTHpGjWZGTCY9BzWMCYvBzVjCZFn1tc5gADtOtqewCY2py/qvOZAAeu1OUCY0LSYN3ogAEo90Yo0AgU4xAAJYooTBzvdABLFFAAMq1QAJMY+0ExMyCQ0AgkRjiKEAAQTIhVhirbGrmeCY8C6MRCY1VvYTHhwTI4x2HAANhCY/Ndw4AB0YTG5mERQ5QCods5nFrnM4Np1YSIAAOy1PWiMRtE31wSKgEL0mq0lEAQJNJHgc96c5zM9xFLCZcE7GPAAP4x7FIAAe5xGP/4AC7QTLlMzAAk0RRIAB8UiAAkuCYwA=='))",
|
||||
"cbBtn": "Bangle.load('heatsuite.urine.js');",
|
||||
"debounce": 0,
|
||||
"tod": [],
|
||||
"btPair": false,
|
||||
"btInfo": null,
|
||||
"notify": false
|
||||
},
|
||||
{
|
||||
"id": "sleep",
|
||||
"icon": "require('heatshrink').decompress(atob('lEo4kA///v3nBIPDjE968CqOzxlX/1m0VBhGBkHb61qpV4/0978Y2xZ1gQTmkQ9VkA9llJKMJZMjmRADlM85gAC4eSBYUCCIOW23Mmc281v30RAAXr+wLBmfMt9py0Rj27CAgAEBYIMCtMsB5AAE///33u4UD2ISLjfMnNu9cwhPxE5nr5m+9+QgVhBYhpBAAVrA4Mcn/ui0gCYsWnVVAANa0YLBsfxCZFjrGIAAWFm0fRQP+CY0fmoSDAANT/dmswTCMYkc1ATFxXBUIPuMYMDCYfsquqAAOlxGqqs7CYcwT4mzqfGGgNmu9onQTB93uT4ITFzHdAAd3xM/CYo7D2eWCYtpCYY7BMYgTNY4vzCY1jz4TCT4r5Bs92v996/3AoPM2J3BCYsfm/duvDtvWnk3u9z+ITIm3d63IE4Nss48BCYhjE5l9J4t35jHFT4fzr/dJoJRBu9lO4ITBT4MsNAPhHgPM7vfnnD/t3wf+8ITB93Cy0R9e//exspNBtGGHQQLC30RtOW+3Mmcz5/ztpOC692ngLC5lvtMjmUggAABhh5BWQciBYUCCIIQCAAcjs4SBuwMHAA+V42MrISOAAMikQSQAD4='))",
|
||||
"cbBtn": 'modHS.saveDataToFile("sleep", "event", {"marker":"sleep"});Bangle.showClock();',
|
||||
"debounce": 0,
|
||||
"tod": [],
|
||||
"btPair": false,
|
||||
"btInfo": null,
|
||||
"notify": false
|
||||
}
|
||||
];
|
||||
let heatsuite__surveyFile_defaultSchema = {
|
||||
"supported":{
|
||||
"en_GB":"English (GB)",
|
||||
"fr_CA":"Francais (CA)"
|
||||
},
|
||||
"questions":[{
|
||||
"key":"comfort",
|
||||
"text": {
|
||||
"en_GB":"Thermal comfort?",
|
||||
"fr_CA":"Confort thermique?"
|
||||
},
|
||||
"tod":[[0,2359]],
|
||||
"oncePerDay": true,
|
||||
"orderFix":false,
|
||||
"options": [{
|
||||
"text":{
|
||||
"en_GB":"Comfortable",
|
||||
"fr_CA":"Confortable"
|
||||
},
|
||||
"value":0,
|
||||
"color":"#ffffff",
|
||||
"btnColor":"#38ed35"
|
||||
},{
|
||||
"text":{
|
||||
"en_GB":"Uncomfortable",
|
||||
"fr_CA": "Inconfortable"
|
||||
},
|
||||
"value":1,
|
||||
"color":"#ffffff",
|
||||
"btnColor":"#ff0019"
|
||||
}]
|
||||
}]
|
||||
};
|
||||
//function from original index.js of BangleApps - required to bring here as the custom.html is loaded in an iframe
|
||||
function showTab(tabname) {
|
||||
document.querySelectorAll("#tab-navigate .tab-item").forEach(tab => {
|
||||
tab.classList.remove("active");
|
||||
});
|
||||
document.querySelectorAll(".apploader-tab").forEach(tab => {
|
||||
tab.style.display = "none";
|
||||
});
|
||||
document.getElementById("tab-" + tabname).classList.add("active");
|
||||
document.getElementById(tabname).style.display = "inherit";
|
||||
}
|
||||
function formDataToJson(form) {
|
||||
const formData = new FormData(form);
|
||||
const data = {};
|
||||
const forceArrayFields = ['record']; // force these to always be arrays
|
||||
const checkboxHandled = new Set();
|
||||
for (let [name, value] of formData.entries()) {
|
||||
const field = form.querySelector(`[name="${name}"]`);
|
||||
if (field && field.type === 'checkbox') {
|
||||
if (checkboxHandled.has(name)) continue;
|
||||
checkboxHandled.add(name);
|
||||
const checkboxes = form.querySelectorAll(`input[type="checkbox"][name="${name}"]`);
|
||||
const values = Array.from(checkboxes)
|
||||
.filter(cb => cb.checked)
|
||||
.map(cb => cb.hasAttribute('value') ? cb.value : true);
|
||||
data[name] = forceArrayFields.includes(name) ? values : (values.length ? values[0] : false);
|
||||
} else {
|
||||
if (data.hasOwnProperty(name)) {
|
||||
if (!Array.isArray(data[name])) {
|
||||
data[name] = [data[name]];
|
||||
}
|
||||
data[name].push(value);
|
||||
} else {
|
||||
if (name === "studyID" && !value.length) continue;
|
||||
data[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
forceArrayFields.forEach(field => {
|
||||
if (!Array.isArray(data[field])) {
|
||||
data[field] = data[field] !== undefined ? [data[field]] : [];
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}
|
||||
function fillFormFromJson(form, data) {
|
||||
for (let [name, value] of Object.entries(data)) {
|
||||
const fields = form.querySelectorAll(`[name="${name}"]`);
|
||||
if (!fields.length) continue;
|
||||
fields.forEach(field => {
|
||||
if (field.type === 'checkbox') {
|
||||
if (Array.isArray(value)) {
|
||||
field.checked = value.includes(field.value);
|
||||
} else {
|
||||
field.checked = value === true || value === field.value || value === "true";
|
||||
}
|
||||
} else if (field.type === 'radio') {
|
||||
field.checked = (field.value === value);
|
||||
} else {
|
||||
field.value = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//autosave form data to localstorage
|
||||
function autosaveSettings(){
|
||||
const form = document.getElementById('heatsuiteSettings');
|
||||
const formjson = formDataToJson(form);
|
||||
localStorage.setItem("heatuite__settings", JSON.stringify(formjson));
|
||||
}
|
||||
function readStorageJSONAsync(filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Util.readStorageJSON(filename, function (data) {
|
||||
try {
|
||||
resolve(data || {});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function readStorageAsync(filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Util.readStorageFile(filename, function (data) {
|
||||
try {
|
||||
resolve(data);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function downloadSingleFile(filename, callback) {
|
||||
Util.readStorageFile(filename, (c) => {
|
||||
let url;
|
||||
let blob;
|
||||
const a = document.createElement('a');
|
||||
let fnArr = filename.split('_');
|
||||
|
||||
if (fnArr[1] !== 'accel') {
|
||||
blob = new Blob([c], { type: 'text/plain' });
|
||||
url = URL.createObjectURL(blob);
|
||||
if (callback) return callback(filename, blob); // Call with original blob
|
||||
} else {
|
||||
function secondsToClock(seconds) {
|
||||
let h = Math.floor(seconds / 3600);
|
||||
let m = Math.floor((seconds % 3600) / 60);
|
||||
let s = seconds % 60;
|
||||
return [h, m, s].map(v => v.toString().padStart(2, '0')).join(':');
|
||||
}
|
||||
let csv = "time,seconds,mag_avg,mag_sum\n";
|
||||
let lines = c.trim().split("\n");
|
||||
for (let line of lines) {
|
||||
let parts = line.split(",").map(v => parseInt(v, 10));
|
||||
if (parts.length !== 3) continue;
|
||||
let seconds = parts[0];
|
||||
let avg = parts[1] / 8192;
|
||||
let sum = parts[2] / 1024;
|
||||
let time = secondsToClock(seconds);
|
||||
csv += `${time},${seconds},${avg},${sum}\n`;
|
||||
}
|
||||
blob = new Blob([csv], { type: 'text/csv' });
|
||||
}
|
||||
if (callback) return callback(filename, blob);
|
||||
url = URL.createObjectURL(blob);
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
}
|
||||
function downloadAllFiles() {
|
||||
console.log("Downloading all files", HeatSuiteFileList);
|
||||
const zip = new JSZip();
|
||||
let index = 0;
|
||||
const progressEl = document.getElementById("progressElementHeatSuite");
|
||||
if (progressEl) progressEl.textContent = "Preparing to download...";
|
||||
function processNext() {
|
||||
if (index >= HeatSuiteFileList.length) {
|
||||
if (progressEl) progressEl.textContent = "Creating ZIP...";
|
||||
zip.generateAsync({ type: "blob" }).then(content => {
|
||||
const a = document.createElement('a');
|
||||
const url = URL.createObjectURL(content);
|
||||
a.href = url;
|
||||
a.download = "all_files.zip";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
if (progressEl) progressEl.textContent = "Download complete!";
|
||||
});
|
||||
return;
|
||||
}
|
||||
const filename = HeatSuiteFileList[index];
|
||||
if (progressEl) progressEl.textContent = `Downloading file ${index + 1} of ${HeatSuiteFileList.length}: ${filename}`;
|
||||
downloadSingleFile(filename, (name, blob) => {
|
||||
zip.file(name, blob);
|
||||
index++;
|
||||
processNext();
|
||||
});
|
||||
}
|
||||
processNext();
|
||||
}
|
||||
|
||||
function deleteFile(filename){
|
||||
if (confirm(`Are you sure you want to delete ${filename}?`)) {
|
||||
Util.eraseStorageFile(filename,(c) =>{
|
||||
var filePrefix = settings.filePrefix || 'htst';
|
||||
return Puck.eval('require("Storage").list(/^'+filePrefix+'/)',renderDownloadTab);
|
||||
});
|
||||
}
|
||||
}
|
||||
function deleteAllFiles() {
|
||||
if (!confirm("Are you sure you want to delete all files?")) return;
|
||||
console.log("Deleting all files", HeatSuiteFileList);
|
||||
let index = 0;
|
||||
const progressEl = document.getElementById("progressElementHeatSuite");
|
||||
if (progressEl) progressEl.textContent = "Preparing to delete files...";
|
||||
function processNext() {
|
||||
if (index >= HeatSuiteFileList.length) {
|
||||
if (progressEl) progressEl.textContent = "All files deleted.";
|
||||
// Refresh the list via Puck/E.show
|
||||
const filePrefix = settings.filePrefix || 'htst';
|
||||
Puck.eval('require("Storage").list(/^' + filePrefix + '/)', renderDownloadTab);
|
||||
return;
|
||||
}
|
||||
const filename = HeatSuiteFileList[index];
|
||||
if (progressEl) progressEl.textContent = `Deleting file ${index + 1} of ${HeatSuiteFileList.length}: ${filename}`;
|
||||
Util.eraseStorageFile(filename, () => {
|
||||
index++;
|
||||
processNext();
|
||||
});
|
||||
}
|
||||
processNext(); // Start the chain
|
||||
}
|
||||
|
||||
function renderDownloadTabLogs(e){
|
||||
var element = document.getElementById("downloadData-tab-content");
|
||||
if(e.length > 0){
|
||||
|
||||
}else{
|
||||
e.innerHTML = "No files to download.";
|
||||
}
|
||||
}
|
||||
function renderDownloadTab(e){
|
||||
HeatSuiteFileList = e.map(file => file.replace(/\x01$/, '')); //save it globally for later use...safer
|
||||
var element = document.getElementById("downloadData-tab-content");
|
||||
var tab_badge = document.getElementById("downloadData-tab-label");
|
||||
if(HeatSuiteFileList.length > 0){
|
||||
tab_badge.classList.add("badge");
|
||||
tab_badge.dataset.badge = HeatSuiteFileList.length;
|
||||
element.innerHTML = "";
|
||||
HeatSuiteFileList.forEach((filename)=>{
|
||||
element.insertAdjacentHTML('beforeend', `<tr id="file-${filename}"><td>${filename}</td><td><i class="icon icon-download" onclick="downloadSingleFile('${filename}');"></i> <i class="icon icon-delete" onclick="deleteFile('${filename}');"></i></td></tr>`);
|
||||
});
|
||||
element.insertAdjacentHTML('beforeend',`<tr id="end" class="text-bold"><td><button class="btn" onclick="downloadAllFiles();">Download All <i class="icon icon-download" ></i></button></td><td><button class="btn" onclick="">Delete All <i class="icon icon-delete" ></i></button> </td></tr>`);
|
||||
}else{
|
||||
tab_badge.classList.remove("badge");
|
||||
tab_badge.dataset.badge = 0;
|
||||
element.innerHTML = '<tr class="active"><td>No Files</td></tr>';
|
||||
}
|
||||
}
|
||||
function onInit(device) {
|
||||
let settings = {};
|
||||
let promise = Promise.resolve();
|
||||
promise = promise.then(() => {
|
||||
return readStorageJSONAsync("heatsuite.default.json");
|
||||
});
|
||||
promise = promise.then(defaults => {
|
||||
return readStorageJSONAsync("heatsuite.settings.json").then(user => {
|
||||
settings = Object.assign({}, defaults, user);
|
||||
});
|
||||
});
|
||||
promise = promise.then(() => {
|
||||
const settingsForm = document.getElementById('heatsuiteSettings');
|
||||
fillFormFromJson(settingsForm, settings);
|
||||
});
|
||||
promise = promise.then(() =>{
|
||||
document.getElementById("downloadData-tab-content").innerHTML = '<div class="loading"></div>';;
|
||||
var filePrefix = settings.filePrefix || 'htst';
|
||||
return Puck.eval('require("Storage").list(/^'+filePrefix+'/)',renderDownloadTab);
|
||||
});
|
||||
//promise = promise.then(()=>{
|
||||
// return Puck.eval('require("Storage").list(heatsuite.log)',renderDownloadTabLogs);
|
||||
//});
|
||||
promise.catch(error => {
|
||||
console.error("Error loading settings:", error);
|
||||
});
|
||||
}
|
||||
|
||||
const editors = {};
|
||||
function initJsonEditor({ elementId, storageKey, defaultSchema, resetBtnId }) {
|
||||
const container = document.getElementById(elementId);
|
||||
const savedContent = localStorage.getItem(storageKey);
|
||||
const initialContent = savedContent ? JSON.parse(savedContent) : defaultSchema;
|
||||
const options = {
|
||||
modes: ['tree', 'form', 'code', 'text'],
|
||||
mode: 'code',
|
||||
onChange: function () {
|
||||
try {
|
||||
const currentContent = editor.get();
|
||||
localStorage.setItem(storageKey, JSON.stringify(currentContent));
|
||||
} catch (err) {
|
||||
console.error(`[${storageKey}] Invalid JSON not saved.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const editor = new JSONEditor(container, options);
|
||||
editor.set(initialContent);
|
||||
editors[storageKey] = { editor, defaultSchema };
|
||||
if (resetBtnId) {
|
||||
const resetBtn = document.getElementById(resetBtnId);
|
||||
resetBtn.addEventListener('click', () => {
|
||||
editor.set(defaultSchema);
|
||||
localStorage.setItem(storageKey, JSON.stringify(defaultSchema));
|
||||
});
|
||||
}
|
||||
}
|
||||
window.onload = function () {
|
||||
const studyIDInput = document.getElementById('input-studyID');
|
||||
studyIDInput.addEventListener("input", () => {
|
||||
studyIDInput.value = studyIDInput.value.replace(/[^a-zA-Z0-9]/g, '');
|
||||
});
|
||||
const filePrefixInput = document.getElementById('input-filePrefix');
|
||||
filePrefixInput.addEventListener("input", () => {
|
||||
filePrefixInput.value = filePrefixInput.value.replace(/[^a-zA-Z0-9]/g, '');
|
||||
});
|
||||
const settingsForm = document.getElementById('heatsuiteSettings');
|
||||
settingsForm.addEventListener('input', autosaveSettings);
|
||||
settingsForm.addEventListener('change', autosaveSettings);
|
||||
const storedSettings = localStorage.getItem("heatuite__settings");
|
||||
if (storedSettings) { //lets fill form:
|
||||
fillFormFromJson(settingsForm, JSON.parse(storedSettings));
|
||||
}
|
||||
//initialize both JsonEditors for tasks and surveys
|
||||
initJsonEditor({
|
||||
elementId: 'heatsuite_taskFile_editor',
|
||||
storageKey: 'heatsuite__taskFile',
|
||||
defaultSchema: heatsuite__taskFile_defaultSchema,
|
||||
resetBtnId: 'heatsuite_taskFile_resetBtn'
|
||||
});
|
||||
initJsonEditor({
|
||||
elementId: 'heatsuite_surveyFile_editor',
|
||||
storageKey: 'heatsuite__surveyFile',
|
||||
defaultSchema: heatsuite__surveyFile_defaultSchema,
|
||||
resetBtnId: 'heatsuite_surveyFile_resetBtn'
|
||||
});
|
||||
};
|
||||
|
||||
// When the 'upload' button is clicked...
|
||||
document.getElementById("upload").addEventListener("click", function () {
|
||||
const form = document.getElementById('heatsuiteSettings');
|
||||
//TO DO VALIDATE jsonEditors!
|
||||
sendCustomizedApp({
|
||||
storage: [
|
||||
{ 'name': "heatsuite.settings.json", content: JSON.stringify(formDataToJson(form)) },
|
||||
{ "name": "heatsuite.tasks.json", content: JSON.stringify(editors['heatsuite__taskFile'].editor.get()) },
|
||||
{ "name": "heatsuite.survey.json", content: JSON.stringify(editors['heatsuite__surveyFile'].editor.get()) },
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
{
|
||||
let studyTasksJSON = "heatsuite.tasks.json";
|
||||
let studyTasks = require('Storage').readJSON(studyTasksJSON, true) || {};
|
||||
|
||||
let Layout = require("Layout");
|
||||
let modHS = require("HSModule");
|
||||
let layout;
|
||||
let NRFFindDeviceTimeout, TaskScreenTimeout;
|
||||
|
||||
let settings = modHS.getSettings();
|
||||
|
||||
let appCache = modHS.getCache();
|
||||
|
||||
function queueNRFFindDeviceTimeout() {
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
NRFFindDeviceTimeout = setTimeout(function () {
|
||||
NRFFindDeviceTimeout = undefined;
|
||||
findBtDevices();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function findBtDevices() {
|
||||
NRF.setScan(); //clear any scans running!
|
||||
NRF.findDevices(function (devices) {
|
||||
let found = false;
|
||||
if (devices.length !== 0) {
|
||||
devices.every((d) => {
|
||||
modHS.log("Found device", d);
|
||||
let services = d.services;
|
||||
modHS.log("Services: ", services);
|
||||
if (services !== undefined && services.includes('1810') && d.id === settings.bt_bloodPressure_id) {
|
||||
//Blood Pressure
|
||||
found = true;
|
||||
layout.msg.label = "BP Found";
|
||||
layout.render();
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
return Bangle.load('heatsuite.bp.js');
|
||||
} else if (services !== undefined && services.includes('181b') && studyTasks.filter(task => task.id === "bodyMass")) {
|
||||
let data = d.serviceData[services];
|
||||
let ctlByte = data[1];
|
||||
let weightRemoved = ctlByte & (1 << 7);
|
||||
modHS.log(weightRemoved);
|
||||
if (weightRemoved === 0) {
|
||||
//Mass found
|
||||
found = true;
|
||||
layout.msg.label = "Scale Found";
|
||||
layout.render();
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
return Bangle.load('heatsuite.mass.js');
|
||||
}
|
||||
modHS.log("No weight on scale");
|
||||
} else if (services !== undefined && services.includes('1809') && d.id === settings.bt_coreTemperature_id) {
|
||||
//Core Temperature
|
||||
found = true;
|
||||
layout.msg.label = "Temp Found";
|
||||
layout.render();
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
return Bangle.load('heatsuite.bletemp.js');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!found) {
|
||||
modHS.log("Search Complete, No Devices Found");
|
||||
queueNRFFindDeviceTimeout();
|
||||
} else {
|
||||
if (TaskScreenTimeout) clearTimeout(TaskScreenTimeout);
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
}
|
||||
}, { timeout: 3000, active: true});
|
||||
}
|
||||
|
||||
function taskButtonInterpretter(string) {
|
||||
//turn off FindDeviceHandler whenever we navigate off task screen
|
||||
let command = 'if (NRFFindDeviceTimeout){clearTimeout(NRFFindDeviceTimeout);}' + string;
|
||||
return eval(command);
|
||||
}
|
||||
|
||||
function queueTaskScreenTimeout() {
|
||||
if (TaskScreenTimeout) clearTimeout(TaskScreenTimeout);
|
||||
if (TaskScreenTimeout === undefined) {
|
||||
TaskScreenTimeout = setTimeout(function () {
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
Bangle.load();
|
||||
}, 180000);
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
let btRequired = false;
|
||||
g.clear();
|
||||
g.reset();
|
||||
if (studyTasks.length === 0) {
|
||||
if(require("Storage").list().includes("heatsuite.survey.json")){ //likely just using for EMA survey
|
||||
return Bangle.load('heatsuite.survey.js'); //go right to survey!
|
||||
}
|
||||
modHS.log('No Study Tasks loaded...');
|
||||
layout = new Layout({
|
||||
type: "v",
|
||||
c: [
|
||||
{
|
||||
type: "txt",
|
||||
font: "Vector:30",
|
||||
label: "No Study Tasks Loaded.",
|
||||
wrap: true,
|
||||
fillx: 1,
|
||||
filly: 1
|
||||
}
|
||||
]
|
||||
});
|
||||
layout.render();
|
||||
return;
|
||||
}
|
||||
let taskArr = appCache.taskQueue;
|
||||
let taskID = [];
|
||||
if (taskArr !== undefined) {
|
||||
taskID = taskArr.filter(function (taskArr) {
|
||||
return taskArr.id;
|
||||
}).map(function (taskArr) {
|
||||
return taskArr.id;
|
||||
});
|
||||
}
|
||||
let layoutOut = { type: "v", c: [] };
|
||||
let row = { type: "h", c: [] };
|
||||
let rowCount = 2;
|
||||
if( studyTasks.length > 4){
|
||||
rowCount = 3; //so we can include up to 9 tasks on the screen at once
|
||||
}
|
||||
studyTasks.forEach(task => {
|
||||
let btn = { type: "btn", fillx: 1, filly: 1 };
|
||||
btn.id = task.id;
|
||||
btn.src = eval(task.icon);
|
||||
//callback on button press
|
||||
if (task.cbBtn) {
|
||||
btn.cb = l => taskButtonInterpretter(task.cbBtn);
|
||||
}
|
||||
//back color determination
|
||||
btn.btnFaceCol = "#90EE90";
|
||||
//a to do!!
|
||||
if (taskID.includes(task.id)) {
|
||||
btn.btnFaceCol = "#FFFF00";
|
||||
}
|
||||
//no bt paired
|
||||
if (task.btPair === true) {
|
||||
if (settings["bt_" + task.id + "_id"] === undefined || !settings["bt_" + task.id + "_id"]) {
|
||||
//make it clickable so we can go to settings and pair something
|
||||
btn.btnFaceCol = "#FF0000";
|
||||
btn.cb = l => eval(require("Storage").read("heatsuite.settings.js"))(()=>load("heatsuite.app.js"));
|
||||
}
|
||||
}
|
||||
if(task.btInfo !== undefined){
|
||||
btRequired = true;//we will be scanning for bluetooth devices
|
||||
}
|
||||
//builder for each icon in taskScreen
|
||||
if (row.c.length >= rowCount) {
|
||||
layoutOut.c.push(row);
|
||||
row = { type: "h", c: [] };
|
||||
}
|
||||
row.c.push(btn);
|
||||
});
|
||||
//push that last row in if needed
|
||||
if (row.c.length > 0) {
|
||||
layoutOut.c.push(row);
|
||||
}
|
||||
//Final
|
||||
if(btRequired) layoutOut.c.push({ type: "txt", font: "6x8:2", label: "Searching...", id: "msg", fillx: 1 });
|
||||
let options = {
|
||||
lazy: true,
|
||||
btns:[{label:"Exit", cb: l=>Bangle.showClock() }],
|
||||
remove: () => {
|
||||
NRF.setScan(); //clear scan
|
||||
if (TaskScreenTimeout) clearTimeout(TaskScreenTimeout);
|
||||
if (NRFFindDeviceTimeout) clearTimeout(NRFFindDeviceTimeout);
|
||||
NRFFindDeviceTimeout = undefined;
|
||||
TaskScreenTimeout = undefined;
|
||||
require("widget_utils").show();
|
||||
}
|
||||
};
|
||||
layout = new Layout(layoutOut, options);
|
||||
layout.render();
|
||||
if(btRequired) queueNRFFindDeviceTimeout();
|
||||
queueTaskScreenTimeout();
|
||||
}
|
||||
|
||||
Bangle.setLocked(false); //unlock screen!
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
require("widget_utils").hide();
|
||||
draw();
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
var Layout = require("Layout");
|
||||
const modHS = require('HSModule');
|
||||
var layout;
|
||||
|
||||
var settings = modHS.getSettings();
|
||||
//var appCache = modHS.getCache();
|
||||
function log(msg) {
|
||||
if (!settings.DEBUG) {
|
||||
return;
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
//Schema for the message coming from the BLE ThermistorPod:
|
||||
const Schema_ThermistorPodBLE = {
|
||||
msgType: 'int32',
|
||||
ta: 'float32',
|
||||
rh: 'float32',
|
||||
batP: 'int32',
|
||||
temp: 'float32',
|
||||
tempAvg: 'float32',
|
||||
adc: 'int32',
|
||||
resistance: 'float32',
|
||||
ambLight: 'int32'
|
||||
};
|
||||
|
||||
function getTcore(id) {
|
||||
layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Oral Temp", fillx: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Waiting...", fillx: 1 },
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
var gatt;
|
||||
var startTime;
|
||||
var complete = false;
|
||||
var TCoreData = {
|
||||
"temp": null,
|
||||
"ta": null,
|
||||
"rh": null,
|
||||
"measures": []
|
||||
};
|
||||
NRF.connect(id).then(function (g) {
|
||||
gatt = g;
|
||||
startTime = parseInt((getTime()).toFixed(0));
|
||||
gatt.device.on('gattserverdisconnected', function (reason) {
|
||||
gatt = null;
|
||||
Bangle.load();
|
||||
log("Disconnected ", reason);
|
||||
});
|
||||
return gatt.getPrimaryService("1809");
|
||||
}).then(function (s) {
|
||||
return s.getCharacteristic("00002A1F-0000-1000-8000-00805F9B34FB");
|
||||
}).then(function (c) {
|
||||
c.on('characteristicvaluechanged', function (event) {
|
||||
const receivedData = modHS.parseBLEData(event.target.value, Schema_ThermistorPodBLE);
|
||||
TCoreData.temp = receivedData.tempAvg;
|
||||
TCoreData.ta = receivedData.ta;
|
||||
TCoreData.rh = receivedData.rh;
|
||||
TCoreData.measures.push(receivedData.adc);
|
||||
var timeNow = parseInt((getTime()).toFixed(0));
|
||||
var diff = timeNow - startTime;
|
||||
var display;
|
||||
if (diff > 90 && !complete) { // time to save the data and disconnect
|
||||
complete = true;
|
||||
if (modHS.saveDataToFile('coreTemp', 'coreTemperature', TCoreData)) {
|
||||
display = {
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Saved!", fillx: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
}
|
||||
} else {
|
||||
var remaining = 90 - diff;
|
||||
display = {
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: remaining + " secs", fillx: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "4x6:2", label: receivedData.adc + " " + receivedData.temp.toFixed(2) + "C", fillx: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
layout = new Layout(display);
|
||||
g.clear();
|
||||
layout.render();
|
||||
if (complete) {
|
||||
if(gatt){
|
||||
gatt.disconnect();
|
||||
}
|
||||
setTimeout(() => { Bangle.load() }, 2000);
|
||||
}
|
||||
});
|
||||
return c.startNotifications();
|
||||
}).then(function (d) {
|
||||
}).catch(function (e) {
|
||||
E.showAlert("error! " + e).then(function () { Bangle.load(); });
|
||||
});
|
||||
}
|
||||
|
||||
let macID = settings.bt_coreTemperature_id.split(" ");
|
||||
//so you can see timeout
|
||||
Bangle.setOptions({backlightTimeout: 0}) // turn off the timeout
|
||||
Bangle.setBacklight(1); // keep screen on
|
||||
getTcore(macID[0]);
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
var Layout = require("Layout");
|
||||
const modHS = require('HSModule');
|
||||
var layout;
|
||||
var settings = modHS.getSettings();
|
||||
//var appCache = modHS.getCache();
|
||||
function log(msg) {
|
||||
if (!settings.DEBUG) {
|
||||
return;
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
//Schema for the message coming from the A&D Medical UA651BLE:
|
||||
function analyzeBPData(data) {
|
||||
const flags = data.getUint8(0, 1);
|
||||
const buf = data.buffer;
|
||||
let result = { //Schema for BP measures
|
||||
"sbp" : null,
|
||||
"dbp" : null,
|
||||
"map" : null,
|
||||
"hr" : null,
|
||||
"moved" : null,
|
||||
"cuffLoose" : null,
|
||||
"irregularPulse" : null,
|
||||
"improperMeasure" : null,
|
||||
"year" : null,
|
||||
"month" : null,
|
||||
"day" : null,
|
||||
"hour" : null,
|
||||
"minute" : null,
|
||||
"second" : null
|
||||
};
|
||||
let index = 1;
|
||||
result.sbp = buf[index];
|
||||
index += 2;
|
||||
result.dbp = buf[index];
|
||||
index += 2;
|
||||
result.map = buf[index];
|
||||
index += 2;
|
||||
if (flags & 0x02) {
|
||||
result.year = buf[index] + (buf[index + 1] << 8),
|
||||
result.month = buf[index + 2],
|
||||
result.day = buf[index + 3],
|
||||
result.hour = buf[index + 4],
|
||||
result.minute = buf[index + 5],
|
||||
result.second = buf[index + 6],
|
||||
index += 7;
|
||||
}
|
||||
if (flags & 0x04) {
|
||||
result.hr = buf[index];
|
||||
index += 2;
|
||||
}
|
||||
if (flags & 0x08) {
|
||||
index += 1;
|
||||
}
|
||||
if (flags & 0x10) {
|
||||
const ms = buf[index];
|
||||
result.moved = (ms & 0b1) ? 1 : 0;
|
||||
result.cuffLoose = (ms & 0b10) ? 1 : 0;
|
||||
result.irregularPulse = (ms & 0b100) ? 1 : 0;
|
||||
result.improperMeasure = (ms & 0b100000) ? 1 : 0;
|
||||
index += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getBP(id) {
|
||||
layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "6x8:2", label: "Blood Pressure", fillx: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "6x8:2", label: "Waiting...", fillx: 1 },
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
var device;
|
||||
var service;
|
||||
log("connecting to ", id);
|
||||
NRF.connect(id).then(function (d) {
|
||||
device = d;
|
||||
return new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}).then(function () {
|
||||
log("connected");
|
||||
if (device.getSecurityStatus().bonded) {
|
||||
log("Already bonded");
|
||||
return true;
|
||||
} else {
|
||||
log("Start bonding");
|
||||
return device.startBonding();
|
||||
}
|
||||
}).then(function () {
|
||||
device.device.on('gattserverdisconnected', function (reason) {
|
||||
Bangle.load();
|
||||
log("Disconnected ", reason);
|
||||
});
|
||||
return device.getPrimaryService("1810");
|
||||
}).then(function (s) {
|
||||
service = s;
|
||||
return service.getCharacteristic("2A08");
|
||||
}).then(function (characteristic) {
|
||||
//set time on device during pairing
|
||||
var date = new Date();
|
||||
var b = new ArrayBuffer(7);
|
||||
var v = new DataView(b);
|
||||
v.setUint16(0, date.getFullYear(), true);
|
||||
v.setUint8(2, date.getMonth() + 1);
|
||||
v.setUint8(3, date.getDate());
|
||||
v.setUint8(4, date.getHours());
|
||||
v.setUint8(5, date.getMinutes());
|
||||
v.setUint8(5, date.getSeconds());
|
||||
var arr = [];
|
||||
for (let i = 0; i < v.buffer.length; i++) {
|
||||
arr[i] = v.buffer[i];
|
||||
}
|
||||
return characteristic.writeValue(arr);
|
||||
}).then(function () {
|
||||
return service.getCharacteristic("2A35");
|
||||
}).then(function (c) {
|
||||
c.on('characteristicvaluechanged', function (event) {
|
||||
//log("-> "); // this is a DataView
|
||||
//log(event.target.value);
|
||||
const receivedData = analyzeBPData(event.target.value);
|
||||
modHS.saveDataToFile('bpres', 'bloodPressure', receivedData);
|
||||
layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: receivedData.sbp, fillx: 1 },
|
||||
{ type: "txt", font: "12x20:2", label: "/", fillx: 1 },
|
||||
{ type: "txt", font: "12x20:2", label: receivedData.dbp, fillx: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: receivedData.hr, fillx: 1 },
|
||||
{ type: "txt", font: "12x20:2", label: "BPM", fillx: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Saved!", fillx: 1 }
|
||||
]
|
||||
},
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
});
|
||||
return c.startNotifications();
|
||||
}).then(function (d) {
|
||||
log("Setting Notification Interval");
|
||||
log("Waiting for notifications");
|
||||
}).catch(function (e) {
|
||||
log("GATT ", device);
|
||||
layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "6x8:2", label: "ERROR!", fillx: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "6x8:1", label: e, fillx: 1 },
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
if (!device.connected) {
|
||||
getBP(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
var macID = settings.bt_bloodPressure_id.split(" ");
|
||||
setTimeout(() => { getBP(macID[0]) }, 2000);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"DEBUG" : false,
|
||||
"SAVE_DEBUG": false,
|
||||
"notifications" : true,
|
||||
"record" : ["bat","steps","hrm","baro","acc"],
|
||||
"filePrefix": "htst",
|
||||
"GPS" : true,
|
||||
"GPSAdaptiveTime" : 2,
|
||||
"GPSInterval" : 30,
|
||||
"GPSScanTime" : 5
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
var Layout = require("Layout");
|
||||
var modHS = require("HSModule");
|
||||
var layout;
|
||||
|
||||
/** --------- MI SCALE --------------------------- */
|
||||
function getMass(service) {
|
||||
var datareceived = [];
|
||||
layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Body Mass", fillx: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Waiting...", fillx: 1 },
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
|
||||
NRF.setScan();//clear other scans
|
||||
|
||||
NRF.setScan(function (devices) {
|
||||
var data = devices.serviceData[service];
|
||||
datareceived.push(data);
|
||||
var ctlByte = data[1];
|
||||
var stabilized = ctlByte & (1 << 5);
|
||||
var weight = ((data[12] << 8) + data[11]) / 200;
|
||||
var impedance = (data[10] << 8) + data[9];
|
||||
if (stabilized && datareceived.length > 1 && impedance > 0 && impedance < 65534) {
|
||||
NRF.setScan();
|
||||
datareceived = [];
|
||||
var dataOut ={
|
||||
'mass' : weight,
|
||||
'impedance' : impedance
|
||||
};
|
||||
modHS.saveDataToFile('mass', 'bodyMass', dataOut);
|
||||
layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: weight, fillx: 1 },
|
||||
{ type: "txt", font: "12x20:2", label: "kg", fillx: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "6x8:2", label: impedance, fillx: 1 },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "txt", font: "12x20:2", label: "Saved!", fillx: 1 }
|
||||
]
|
||||
},
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
layout.render();
|
||||
setTimeout(function () { Bangle.load(); }, 3000);
|
||||
}
|
||||
}, { timeout: 2000, filters: [{ services: [service] }] });
|
||||
}
|
||||
|
||||
//init
|
||||
getMass('181b');
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
function _getSettings() {
|
||||
var out = Object.assign(
|
||||
require('Storage').readJSON("heatsuite.default.json", true) || {},
|
||||
require('Storage').readJSON("heatsuite.settings.json", true) || {}
|
||||
);
|
||||
out.StudyTasks = require('Storage').readJSON("heatsuite.tasks.json", true) || {};
|
||||
return out;
|
||||
}
|
||||
function _checkFileHeaders(filename,header){
|
||||
var storageFile = require("Storage").open(filename, "r");
|
||||
var headers = storageFile.readLine().trim();
|
||||
var headerString = header.join(",");
|
||||
if(headers === headerString){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function _renameOldFile(file){
|
||||
var rename = false;
|
||||
var i = 1;
|
||||
while(!rename){
|
||||
var filename = file+"_"+String(i);
|
||||
if(require('Storage').list(filename).length == 0){
|
||||
var newFile = require("Storage").open(filename, "w");
|
||||
var oldFile = require("Storage").open(file, "r");
|
||||
var l = oldFile.readLine();
|
||||
while (l!==undefined) {
|
||||
newFile.write(l);
|
||||
l = oldFile.readLine();
|
||||
}
|
||||
oldFile.erase(); //erase old file
|
||||
require("Storage").compact(); //compact memory
|
||||
rename = true;
|
||||
}else{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
function _getRecordFile(type, headers) {
|
||||
var settings = _getSettings();
|
||||
var dt = new Date();
|
||||
var hour = dt.getHours();
|
||||
if (hour < 10) hour = '0' + hour;
|
||||
var month = dt.getMonth() + 1;
|
||||
if (month < 10) month = '0' + month;
|
||||
var day = dt.getDate();
|
||||
if (day < 10) day = '0' + day;
|
||||
var date = dt.getFullYear() + "" + month + "" + day;
|
||||
var fileName = settings.filePrefix + "_" + type + "_";
|
||||
fileName = fileName + date;
|
||||
if (require('Storage').list(fileName).length > 0 && type !== "accel") {
|
||||
if(_checkFileHeaders(fileName,headers)){
|
||||
return require('Storage').open(fileName, 'a');
|
||||
}else{ // need to rename the old file as headers have changed
|
||||
_renameOldFile(fileName);
|
||||
}
|
||||
}
|
||||
if (type !== "accel") {
|
||||
var storageFile = require("Storage").open(fileName, "w");
|
||||
storageFile.write(headers.join(",") + "\n");
|
||||
}
|
||||
return require("Storage").open(fileName, "a");
|
||||
}
|
||||
function _checkStorageFree(type) {
|
||||
var settings = _getSettings();
|
||||
var freeSpace = require("Storage").getFree();
|
||||
var filePrefix = settings.filePrefix + type;
|
||||
var csvList = require("Storage").list(filePrefix);
|
||||
if (freeSpace < 500000) {
|
||||
if(csvList.length > 0){
|
||||
require("Storage").open(csvList[0],"r").erase();
|
||||
}
|
||||
require("Storage").compact();
|
||||
}
|
||||
}
|
||||
function _saveDataToFile(type, task, arr) {
|
||||
var newArr = {
|
||||
'unix' : parseInt((getTime()).toFixed(0)),
|
||||
'tz' : (new Date()).getTimezoneOffset() * -60
|
||||
}
|
||||
for (var key in arr) {
|
||||
newArr[key] = arr[key];
|
||||
}
|
||||
var data = [];
|
||||
var headers = [];
|
||||
for (var key in newArr) {
|
||||
if(Array.isArray(newArr[key])){
|
||||
newArr[key] = newArr[key].join(';');
|
||||
}
|
||||
data.push(newArr[key]);
|
||||
headers.push(key);
|
||||
}
|
||||
var currFile = _getRecordFile(type, headers);
|
||||
if (currFile) {
|
||||
var String = data.join(',') + '\n';
|
||||
currFile.write(String);
|
||||
_updateTaskQueue(task, newArr);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
function _updateTaskQueue(task, arr) {
|
||||
var appCache = _getCache();
|
||||
var taskQueue = appCache.taskQueue;
|
||||
var tasktime = parseInt((getTime()).toFixed(0));
|
||||
if (taskQueue !== undefined) {
|
||||
var newTaskQueue = taskQueue.filter(function (taskQueue) {
|
||||
return taskQueue.id !== task;
|
||||
});
|
||||
appCache.taskQueue = newTaskQueue;
|
||||
}
|
||||
if (appCache[task] === undefined) appCache[task] = {};
|
||||
if (task === 'survey') { //we will refactor the value to be an object with keys
|
||||
var key = arr.key;
|
||||
if(appCache.survey[key] === undefined) appCache.survey[key] = {};
|
||||
appCache.survey[key] = {
|
||||
unix: tasktime,
|
||||
resp: arr.value
|
||||
};
|
||||
}else{
|
||||
appCache[task] = arr;
|
||||
}
|
||||
appCache[task].unix = tasktime;
|
||||
//lets always store cache so we can restore values if needed
|
||||
_writeCache(appCache);
|
||||
}
|
||||
function _getCache() {
|
||||
return require('Storage').readJSON("heatsuite.cache.json", true) || {};
|
||||
}
|
||||
function _writeCache(cache) {
|
||||
var oldCache = _getCache();
|
||||
if (oldCache !== cache) require('Storage').writeJSON("heatsuite.cache.json", cache);
|
||||
return _getCache();
|
||||
}
|
||||
function _clearCache() {
|
||||
require('Storage').writeJSON("heatsuite.cache.json", {});
|
||||
return _getCache();
|
||||
}
|
||||
function _parseBLEData(buffer, dataSchema) {
|
||||
let offset = 0;
|
||||
let result = {};
|
||||
for (let field in dataSchema) {
|
||||
const dataType = dataSchema[field];
|
||||
let value;
|
||||
switch (dataType) {
|
||||
case 'uint8':
|
||||
value = buffer.getUint8(offset,true);
|
||||
offset += 1; // 1 byte for uint8
|
||||
break;
|
||||
case 'uint16':
|
||||
value = buffer.getUint16(offset,true); // Assuming little-endian format
|
||||
offset += 4; // 2 bytes for uint16
|
||||
break;
|
||||
case 'int32':
|
||||
value = buffer.getInt32(offset,true); // Assuming little-endian format
|
||||
offset += 4; // 4 bytes for int32
|
||||
break;
|
||||
case 'float32':
|
||||
value = buffer.getFloat32(offset,true); // Assuming little-endian format
|
||||
offset += 4; // 4 bytes for float32
|
||||
break;
|
||||
case 'float64':
|
||||
value = buffer.getFloat64(offset,true); // Assuming little-endian format
|
||||
offset += 8; // 8 bytes for float64
|
||||
break;
|
||||
case 'array':
|
||||
value = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
value.push(buffer.getUint8(offset,true));
|
||||
offset += 1; // 1 byte for each uint8
|
||||
}
|
||||
break;
|
||||
case 'float16':{
|
||||
const b0 = buffer.getUint8(offset, true);
|
||||
const b1 = buffer.getUint8(offset + 1, true);
|
||||
const mantissa = (b1 << 8) | b0;
|
||||
const sign = mantissa & 0x8000 ? -1 : 1;
|
||||
const exponent = (mantissa >> 11) & 0x0F;
|
||||
const fraction = mantissa & 0x7FF;
|
||||
value = sign * (1 + fraction / 2048) * Math.pow(2, exponent - 15);
|
||||
offset += 2;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown data type: ${dataType}`);
|
||||
}
|
||||
result[field] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function _log(msg) {
|
||||
var settings = _getSettings();
|
||||
if(settings.SAVE_DEBUG){
|
||||
var file = require('Storage').open('heatsuite.log', 'a');
|
||||
var string = String(parseInt((new Date().getTime() / 1000).toFixed(0)))+": "+msg+"\n";
|
||||
file.write(string);
|
||||
return;
|
||||
}
|
||||
else if (!settings.DEBUG) {
|
||||
return;
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
exports = {
|
||||
getSettings: _getSettings,
|
||||
getRecordFile: _getRecordFile,
|
||||
saveDataToFile: _saveDataToFile,
|
||||
checkStorageFree : _checkStorageFree,
|
||||
getCache: _getCache,
|
||||
writeCache: _writeCache,
|
||||
clearCache: _clearCache,
|
||||
updateTaskQueue: _updateTaskQueue,
|
||||
parseBLEData: _parseBLEData,
|
||||
log: _log,
|
||||
};
|
||||
|
|
@ -0,0 +1,359 @@
|
|||
(function (back) {
|
||||
|
||||
var settingsJSON = "heatsuite.settings.json";
|
||||
var studyTasksJSON = "heatsuite.tasks.json";
|
||||
|
||||
function log(msg) {
|
||||
if (!settings.DEBUG) {
|
||||
return;
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function writeSettings(key, value) {
|
||||
var s = require('Storage').readJSON(settingsJSON, true) || {};
|
||||
s[key] = value;
|
||||
require('Storage').writeJSON(settingsJSON, s);
|
||||
settings = readSettings();
|
||||
if (global.WIDGETS && WIDGETS["heatsuite"]) WIDGETS["heatsuite"].changed(); //redraw widget on settings update if open
|
||||
}
|
||||
|
||||
function readSettings() {
|
||||
var out = Object.assign(
|
||||
require('Storage').readJSON("heatsuite.default.json", true) || {},
|
||||
require('Storage').readJSON(settingsJSON, true) || {}
|
||||
);
|
||||
out.StudyTasks = require('Storage').readJSON(studyTasksJSON, true) || {};
|
||||
return out;
|
||||
}
|
||||
var settings = readSettings();
|
||||
|
||||
/*---- PAIRING FUNCTIONS FOR DEVICES ----*/
|
||||
function BPPair(id) {
|
||||
var device;
|
||||
E.showMessage(`Pairing /n ${id}`, "Bluetooth");
|
||||
NRF.connect(id).then(function (d) {
|
||||
device = d;
|
||||
return new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}).then(function () {
|
||||
log("connected");
|
||||
if (device.getSecurityStatus().bonded) {
|
||||
log("Already bonded");
|
||||
return true;
|
||||
} else {
|
||||
log("Start bonding");
|
||||
return device.startBonding();
|
||||
}
|
||||
}).then(function () {
|
||||
device.device.on('gattserverdisconnected', function (reason) {
|
||||
log("Disconnected ", reason);
|
||||
});
|
||||
return device.getPrimaryService("1810");
|
||||
}).then(function (service) {
|
||||
log(service);
|
||||
return service.getCharacteristic("2A08");
|
||||
}).then(function (characteristic) {
|
||||
//set time on device during pairing
|
||||
var date = new Date();
|
||||
var b = new ArrayBuffer(7);
|
||||
var v = new DataView(b);
|
||||
v.setUint16(0, date.getFullYear(), true);
|
||||
v.setUint8(2, date.getMonth() + 1);
|
||||
v.setUint8(3, date.getDate());
|
||||
v.setUint8(4, date.getHours());
|
||||
v.setUint8(5, date.getMinutes());
|
||||
v.setUint8(5, date.getSeconds());
|
||||
var arr = [];
|
||||
for (let i = 0; i < v.buffer.length; i++) {
|
||||
arr[i] = v.buffer[i];
|
||||
}
|
||||
return characteristic.writeValue(arr);
|
||||
}).then(function () {
|
||||
writeSettings("bt_bloodPressure_id", id);
|
||||
// Store the name for displaying later. Will connect by ID
|
||||
if (device.name) {
|
||||
writeSettings("bt_bloodPressure_name", device.name);
|
||||
}
|
||||
E.showAlert("Paired!").then(function () { E.showMenu(deviceSettings()) });
|
||||
log("Device ID paired, time set, Done!");
|
||||
return device.disconnect();
|
||||
}).catch(function (e) {
|
||||
log(e);
|
||||
E.showAlert("error! " + e).then(function () { E.showMenu(deviceSettings()) });
|
||||
});
|
||||
}
|
||||
function PairTcore(id) {
|
||||
E.showMessage(`Pairing /n ${id}`, "Bluetooth");
|
||||
var gatt;
|
||||
NRF.connect(id).then(function (g) {
|
||||
gatt = g;
|
||||
console.log("connected!!!");
|
||||
// return gatt.startBonding();
|
||||
//}).then(function() {
|
||||
console.log("bonded", gatt.getSecurityStatus());
|
||||
writeSettings("bt_coreTemperature_id", id);
|
||||
E.showAlert("Paired!").then(function () { E.showMenu(deviceSettings()) });
|
||||
log("Device ID paired, Done!");
|
||||
gatt.disconnect();
|
||||
}).catch(function (e) {
|
||||
log("ERROR: " + e);
|
||||
E.showAlert("error! " + e).then(function () { E.showMenu(deviceSettings()) });
|
||||
});
|
||||
}
|
||||
|
||||
function deviceSettings() {
|
||||
var menu = { '< Back': function () { E.showMenu(mainMenuSettings()); } };
|
||||
menu[''] = { 'title': 'Devices' };
|
||||
settings.StudyTasks.forEach(task => {
|
||||
if (task.btPair === undefined || !task.btPair) return;
|
||||
let key = task.id; // Adjust based on how you identify tasks
|
||||
let id = "bt_" + key + "_id";
|
||||
if (settings[id] !== undefined) {
|
||||
menu["Clear " + key] = function () {
|
||||
E.showPrompt("Clear " + key + " device?").then((r) => {
|
||||
if (r) {
|
||||
writeSettings("bt_" + key + "_id", undefined);
|
||||
writeSettings("bt_" + key + "_name", undefined);
|
||||
}
|
||||
E.showMenu(mainMenuSettings());
|
||||
});
|
||||
};
|
||||
} else {
|
||||
menu["Pair " + key] = () => createMenuFromScan(key, task.btInfo.service);
|
||||
}
|
||||
});
|
||||
return menu;
|
||||
}
|
||||
|
||||
function recordMenu(){
|
||||
var updateRecorder = function(name,v){
|
||||
var r = settings.record;
|
||||
r = r.filter(item => item !== name);
|
||||
if(v){
|
||||
r.push(name);
|
||||
}
|
||||
writeSettings("record",r);
|
||||
}
|
||||
var menu = { '< Back': function () { E.showMenu(mainMenuSettings()); } };
|
||||
menu[''] = { 'title': 'Recorder' };
|
||||
var recorderOptions = {
|
||||
'hrm' : 'Optical HR',
|
||||
'steps' : "Steps",
|
||||
'bat' : 'Battery',
|
||||
'movement': 'Movement',
|
||||
'acc':'Accelerometry',
|
||||
'baro':'Temp/Pressure',
|
||||
'bthrm': 'BT HRM',
|
||||
'CORESensor':'CORE Sensor'
|
||||
}
|
||||
for (let key in recorderOptions) {
|
||||
let name = recorderOptions[key];
|
||||
menu[name] = {
|
||||
value: settings.record.includes(key),
|
||||
onchange: v => {updateRecorder(key,v);}
|
||||
};
|
||||
}
|
||||
menu['High Acc'] = {
|
||||
value: settings.highAcc || false,
|
||||
onchange: v => {
|
||||
settings.highAcc = v;
|
||||
writeSettings("highAcc", v);
|
||||
}
|
||||
};
|
||||
return menu;
|
||||
}
|
||||
|
||||
function mainMenuSettings() {
|
||||
var menu = {
|
||||
'': { 'title': 'Main' },
|
||||
'< Back': back
|
||||
};
|
||||
|
||||
menu['Recorders'] = function () {E.showMenu(recordMenu()) };
|
||||
menu['Devices'] = function () { E.showMenu(deviceSettings()) };
|
||||
menu['GPS'] = function () { E.showMenu(gpsSettings()) };
|
||||
menu['Language'] = function () { E.showMenu(languageMenu()) };
|
||||
menu['Swipe Launch'] = {
|
||||
value: settings.swipeOpen || false,
|
||||
onchange: v => {
|
||||
settings.swipeOpen = v;
|
||||
writeSettings("swipeOpen", v);
|
||||
}
|
||||
};
|
||||
menu['Survey Random'] = {
|
||||
value: settings.surveyRandomize || false,
|
||||
onchange: v => {
|
||||
settings.GPS = v;
|
||||
writeSettings("surveyRandomize", v);
|
||||
}
|
||||
};
|
||||
menu['HRM Interval'] = {
|
||||
value: settings.HRMInterval || 0,
|
||||
min: 0, max: 60,
|
||||
onchange: v => {
|
||||
settings.HRMInterval = v;
|
||||
writeSettings("HRMInterval", v);
|
||||
}
|
||||
};
|
||||
menu['Restart BLE'] = function () {
|
||||
E.showPrompt("Restart Bluetooth?").then((r) => {
|
||||
if (r) {
|
||||
NRF.disconnect()
|
||||
NRF.restart();
|
||||
}
|
||||
E.showMenu(mainMenuSettings());
|
||||
});
|
||||
};
|
||||
menu['Clear Cache'] = function () {
|
||||
E.showPrompt("Clear Cache?").then((r) => {
|
||||
if (r) {
|
||||
require('Storage').writeJSON("heatsuite.cache.json", {});
|
||||
}
|
||||
E.showMenu(mainMenuSettings());
|
||||
});
|
||||
}
|
||||
menu['Clear Study ID'] = function () {
|
||||
E.showPrompt("Clear study ID (includes ignored)?").then((r) => {
|
||||
if (r) {
|
||||
writeSettings("studyID", undefined);
|
||||
writeSettings("studyIDIgnore", []);
|
||||
}
|
||||
E.showMenu(mainMenuSettings());
|
||||
});
|
||||
}
|
||||
menu['Notifications'] = {
|
||||
value: settings.notifications || false,
|
||||
onchange: v => {
|
||||
settings.notifications = v;
|
||||
writeSettings("notifications", v);
|
||||
}
|
||||
};
|
||||
menu['Debug'] = function () { E.showMenu(debugMenu()) };
|
||||
return menu;
|
||||
}
|
||||
function debugMenu(){
|
||||
var menu = {
|
||||
'': { 'title': 'Debug' },
|
||||
'< Back': function () { E.showMenu(mainMenuSettings()); }
|
||||
};
|
||||
menu['Console'] = {
|
||||
value: settings.DEBUG || false,
|
||||
onchange: v => {
|
||||
settings.DEBUG = v;
|
||||
writeSettings("DEBUG", v);
|
||||
}
|
||||
};
|
||||
menu['Log (file)'] = {
|
||||
value: settings.SAVE_DEBUG || false,
|
||||
onchange: v => {
|
||||
settings.SAVE_DEBUG = v;
|
||||
writeSettings("SAVE_DEBUG", v);
|
||||
}
|
||||
};
|
||||
return menu;
|
||||
}
|
||||
function gpsSettings() {
|
||||
var menu = {
|
||||
'': { 'title': 'GPS' },
|
||||
'< Back': function () { E.showMenu(mainMenuSettings()); }
|
||||
};
|
||||
menu['GPS'] = {
|
||||
value: settings.GPS || false,
|
||||
onchange: v => {
|
||||
settings.GPS = v;
|
||||
writeSettings("GPS", v);
|
||||
}
|
||||
};
|
||||
menu['Scan Time (min)'] = {
|
||||
value: settings.GPSScanTime || 1,
|
||||
min: 0, max: 60,
|
||||
onchange: v => {
|
||||
settings.GPSScanTime = v;
|
||||
writeSettings("GPSScanTime", v);
|
||||
}
|
||||
};
|
||||
menu['Interval (min)'] = {
|
||||
value: settings.GPSInterval || 10,
|
||||
min: 0, max: 180,
|
||||
onchange: v => {
|
||||
settings.GPSinterval = v;
|
||||
writeSettings("GPSInterval", v);
|
||||
}
|
||||
};
|
||||
menu['Adaptive (min)'] = {
|
||||
value: settings.GPSAdaptiveTime || 2,
|
||||
min: 0, max: 60,
|
||||
onchange: v => {
|
||||
settings.GPSAdaptiveTime = v;
|
||||
writeSettings("GPSAdaptiveTime", v);
|
||||
}
|
||||
};
|
||||
return menu;
|
||||
}
|
||||
|
||||
function languageMenu() {
|
||||
var menu = { '< Back': function () { E.showMenu(mainMenuSettings()); } };
|
||||
menu[''] = { 'title': 'Language' };
|
||||
var surveySettings = require('Storage').readJSON("heatsuite.survey.json", true) || {};
|
||||
|
||||
Object.keys(surveySettings.supported).forEach(key => {
|
||||
//var id = surveySettings.supported[key];
|
||||
menu[key] = function () {
|
||||
E.showPrompt("Set " + key + "?").then((r) => {
|
||||
if (r) {
|
||||
writeSettings('lang', key);
|
||||
}
|
||||
E.showMenu(mainMenuSettings());
|
||||
});
|
||||
};
|
||||
});
|
||||
return menu;
|
||||
}
|
||||
|
||||
function createMenuFromScan(type, service) {
|
||||
E.showMenu();
|
||||
E.showMessage("Scanning for 4 seconds");
|
||||
var submenu_scan = {
|
||||
'< Back': function () { E.showMenu(deviceSettings()); }
|
||||
};
|
||||
NRF.findDevices(function (devices) {
|
||||
submenu_scan[''] = { title: `Scan (${devices.length} found)` };
|
||||
if (devices.length === 0) {
|
||||
E.showAlert("No " + type + " devices found")
|
||||
.then(() => E.showMenu(deviceSettings()));
|
||||
return;
|
||||
} else {
|
||||
devices.forEach((d) => {
|
||||
print("Found device", d);
|
||||
var shown = (d.name || d.id.substr(0, 17));
|
||||
submenu_scan[shown] = function () {
|
||||
E.showPrompt("Set " + shown + "?").then((r) => {
|
||||
if (r) {
|
||||
switch (type) {
|
||||
case "bloodPressure":
|
||||
BPPair(d.id);
|
||||
break;
|
||||
case "coreTemperature":
|
||||
PairTcore(d.id);
|
||||
break;
|
||||
case "bthrm":
|
||||
|
||||
break;
|
||||
default:
|
||||
E.showMenu(deviceSettings());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
E.showMenu(deviceSettings());
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
E.showMenu(submenu_scan);
|
||||
}, { timeout: 4000, active: true, filters: [{ services: [service] }] });
|
||||
}
|
||||
|
||||
E.showMenu(mainMenuSettings());
|
||||
})
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
var surveyFileJSON = "heatsuite.survey.json";
|
||||
var Layout = require("Layout");
|
||||
const modHS = require('HSModule');
|
||||
var layout;
|
||||
var TaskScreenTimeout;
|
||||
Bangle.setOptions({
|
||||
'backlightTimeout':30000,
|
||||
'lockTimeout':30000
|
||||
});
|
||||
|
||||
var settings = modHS.getSettings();
|
||||
//randomize question order
|
||||
function shuffle(array) {
|
||||
const result = [];
|
||||
var itemsLeft = array;
|
||||
while (itemsLeft.length) {
|
||||
var Item;
|
||||
if (itemsLeft[0].orderFix !== undefined && itemsLeft[0].orderFix == true) {
|
||||
Item = itemsLeft.splice(0, 1)[0];
|
||||
} else {
|
||||
var randomIndex = Math.floor(Math.random() * itemsLeft.length);
|
||||
Item = itemsLeft.splice(randomIndex, 1)[0];
|
||||
}
|
||||
result.push(Item); // ...and add it to the result
|
||||
}
|
||||
return result;
|
||||
}
|
||||
var surveyFile = require('Storage').readJSON(surveyFileJSON, true) || {"questions":[{"text":{"en_GB":"Thermal Comfort?"},"options":[{"text":{"en_GB":"Comfortable"},"value":0,"color":"#ffffff","btnColor":"#38ed35"},{"text":{"en_GB":"Uncomfortable"},"value":1,"color":"#ffffff","btnColor":"#ff0019"}],"tod":[[0,2359]],"key":"comfort"}],"supported":{"en_GB":"English (GB)"}};
|
||||
var QArr = surveyFile.questions;
|
||||
if (settings.surveyRandomize !== undefined && settings.surveyRandomize) {
|
||||
QArr = shuffle(QArr);
|
||||
}
|
||||
|
||||
|
||||
function log(msg) {
|
||||
if (!settings.DEBUG) {
|
||||
return;
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
var appCache = modHS.getCache();
|
||||
var lang = settings.lang || require("locale").name || "en_GB";
|
||||
|
||||
function queueTaskScreenTimeout() {
|
||||
if (TaskScreenTimeout) clearTimeout(TaskScreenTimeout);
|
||||
if (TaskScreenTimeout === undefined) {
|
||||
TaskScreenTimeout = setTimeout(function () {
|
||||
Bangle.load();
|
||||
}, 180000);
|
||||
}
|
||||
}
|
||||
|
||||
/** -----------==== SURVEYS ====---------------- */
|
||||
var scrollInterval;
|
||||
function drawScrollingText(text,height) {
|
||||
Bangle.appRect = { x: 0, y: height, w: g.getWidth(), h: g.getHeight() - height, x2: g.getWidth()-1, y2: g.getHeight()-1 };
|
||||
let stringWidth = g.stringWidth(text);
|
||||
let textX = (stringWidth > g.getWidth())? (stringWidth/2) : 0;
|
||||
g.setColor("#000");
|
||||
g.setBgColor("#FFF");
|
||||
g.setFont("Vector:20", 2);
|
||||
g.clearRect(0, 0, g.getWidth(), height);
|
||||
function QuestionText() {
|
||||
g.setColor("#000");
|
||||
g.setBgColor("#FFF");
|
||||
g.setFont("Vector:20", 2);
|
||||
g.clearRect(0, 0, g.getWidth(), height);
|
||||
g.drawString(text, textX, height/2);
|
||||
textX -= 5;
|
||||
if (textX < (-(stringWidth/2)+g.getWidth())) textX = (stringWidth/2);
|
||||
g.flip();
|
||||
}
|
||||
if(scrollInterval) clearTimeout(scrollInterval);
|
||||
if(stringWidth > g.getWidth()){
|
||||
scrollInterval = setInterval(QuestionText, 60); //will need to scroll as its too long
|
||||
}else{
|
||||
QuestionText();
|
||||
}
|
||||
}
|
||||
function drawResponseOpts(ind){
|
||||
//force scrolling of question at the top
|
||||
var question = QArr[ind];
|
||||
drawScrollingText(question.text[lang].replace(/\\n/g, " "),30);
|
||||
var height = 30;
|
||||
var options = question.options;
|
||||
if(options.length < 4){
|
||||
height = Math.floor(Bangle.appRect.h / options.length);
|
||||
}
|
||||
let drawItem = function (idx,r){
|
||||
var optionText = options[idx].value;
|
||||
if (options[idx].text !== undefined) {
|
||||
optionText = options[idx].text[lang];
|
||||
}
|
||||
g.setColor((options[idx].color)?options[idx].color:"#000");
|
||||
g.setBgColor((options[idx].btnColor)?options[idx].btnColor:"#CCC").clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1);
|
||||
g.setFontAlign(0, 0, 0);
|
||||
g.setFont("Vector:20").drawString(optionText,r.x+(g.getWidth()/2),r.y+(height/2));
|
||||
};
|
||||
let selectItem = function(id) {
|
||||
var resp = (options[id] && options[id].text && lang in options[id].text) ? options[id].text[lang] : options[id].value;
|
||||
const cbString = ind + "," + question.key + "," + resp + "," + options[id].value;
|
||||
return surveyResponse(cbString);
|
||||
};
|
||||
E.showScroller({
|
||||
h : height,
|
||||
c : options.length,
|
||||
draw : drawItem,
|
||||
select : selectItem
|
||||
});
|
||||
}
|
||||
function drawSurveyLayout(index) {
|
||||
if(scrollInterval) clearTimeout(scrollInterval);
|
||||
if (surveyFile === undefined) {
|
||||
log('No Survey File');
|
||||
E.showAlert("No Survey File Found.").then(function () {
|
||||
Bangle.showClock();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (index == QArr.length) {
|
||||
//at the end, so we can show a saved image and redirect to time screen
|
||||
g.clear();
|
||||
g.reset();
|
||||
g.setBgColor("#FFF");
|
||||
Bangle.buzz(150);
|
||||
layout = new Layout({
|
||||
type: "v",
|
||||
c: [{
|
||||
type: "img",
|
||||
pad: 4,
|
||||
src: require("heatshrink").decompress(atob("ikUwYFCgVJkgMDhMkyVJAwQFCAQNAgESAoQCBwEBBwlIgAFDpNkyAjDkm/5MEBwdf+gUEl/6AoVZkmX/oLClv6pf+DQn1/4+E3//0gFBkACBv/SBYI7D5JiDLJx9CBAR4CAoWQQ4Z9DgAA=="))
|
||||
},
|
||||
{
|
||||
type: "txt",
|
||||
font: "Vector:30",
|
||||
label: "Done!"
|
||||
}]
|
||||
});
|
||||
layout.render();
|
||||
setTimeout(function () {
|
||||
Bangle.load();
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
var question = QArr[index];
|
||||
var dateN = new Date();
|
||||
if (question.tod !== undefined && question.tod.length > 0) {
|
||||
//Now we need to see if we are in window of the day that we are eligible to ask the question
|
||||
var windowOfDay = false;
|
||||
var currMT = parseInt(dateN.getHours() + "" + dateN.getMinutes());
|
||||
for (let d = 0; d < question.tod.length; d++) {
|
||||
if (currMT > question.tod[d][0] && currMT < question.tod[d][1]) {
|
||||
windowOfDay = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!windowOfDay) {
|
||||
drawSurveyLayout(index + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (appCache.survey !== undefined){ //first time we are asking this question
|
||||
if(appCache.survey[question.key] !== undefined) {
|
||||
//lets just check if this survey question can be shown right now, otherwise we will skip it
|
||||
var lastS = new Date(appCache.survey[question.key].unix * 1000);
|
||||
if (question.oncePerDay !== undefined && question.oncePerDay) { // check if we can only show survey once a day and if we already have
|
||||
//if (dateN.getFullYear() + dateN.getMonth() + dateN.getDate() === lastS.getFullYear() + lastS.getMonth() + lastS.getDate()) {
|
||||
if(Math.floor(dateN.getTime() / 86400000) === Math.floor(lastS.getTime() / 86400000)){
|
||||
drawSurveyLayout(index + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Bangle.buzz(100);
|
||||
g.clear();
|
||||
g.reset();
|
||||
var out = {
|
||||
type: "v",
|
||||
c: []
|
||||
};
|
||||
//default to English if question isn't translated
|
||||
if (!question.text[lang]) {
|
||||
lang = "en_GB";
|
||||
}
|
||||
var questionText = question.text[lang].replace(/\\n/g, "\n");
|
||||
var q = {
|
||||
type: "txt",
|
||||
wrap: true,
|
||||
fillx: 1,
|
||||
filly: 1,
|
||||
font: "Vector:20",
|
||||
label: questionText,
|
||||
id: "label"
|
||||
};
|
||||
out.c.push(q);
|
||||
var optFont = 'Vector:30';
|
||||
if (question.optFont !== undefined) optFont = question.optFont;
|
||||
var opt = {
|
||||
type: "btn",
|
||||
font: optFont,
|
||||
label: ">>",
|
||||
pad: 1,
|
||||
btnFaceCol: "#0f0",
|
||||
cb: l => drawResponseOpts(index)
|
||||
};
|
||||
out.c.push(opt);
|
||||
layout = new Layout(out);
|
||||
layout.render();
|
||||
return;
|
||||
}
|
||||
|
||||
function surveyResponse(text) {
|
||||
var arr = text.split(',');
|
||||
var nextSurvey = parseInt(arr[0]) + 1;
|
||||
let newArr = {
|
||||
"key": arr[1],
|
||||
"resp": arr[2],
|
||||
"value": arr[3]
|
||||
}
|
||||
modHS.saveDataToFile('survey', 'survey', newArr);
|
||||
drawSurveyLayout(nextSurvey);
|
||||
}
|
||||
|
||||
drawSurveyLayout(0);
|
||||
queueTaskScreenTimeout();
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
var Layout = require("Layout");
|
||||
const modHS = require('HSModule');
|
||||
var layout;
|
||||
|
||||
//var settings = modHS.getSettings();
|
||||
var appCache = modHS.getCache();
|
||||
|
||||
var results = {
|
||||
color: 0,
|
||||
volume: null,
|
||||
colorAssessment: appCache.urine && appCache.urine.colorAssessment ? appCache.urine.colorAssessment : 0
|
||||
}
|
||||
|
||||
function YMDInt(date) {
|
||||
var year = date.getFullYear().toString();
|
||||
var month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
var day = date.getDate().toString().padStart(2, '0');
|
||||
var concatenatedDate = year + month + day;
|
||||
var concatenatedInteger = parseInt(concatenatedDate);
|
||||
return concatenatedInteger;
|
||||
}
|
||||
|
||||
function saveUrineData(color) {
|
||||
if (color > 0) {
|
||||
var d = new Date();
|
||||
results.color = color;
|
||||
results.colorAssessment = YMDInt(d);
|
||||
}
|
||||
Bangle.buzz(150);
|
||||
modHS.saveDataToFile('urine', 'urine', results);
|
||||
g.clear();
|
||||
g.reset();
|
||||
layout = new Layout({
|
||||
type: "v",
|
||||
c: [{
|
||||
type: "img",
|
||||
pad: 4,
|
||||
src: require("heatshrink").decompress(atob("ikUwYFCgVJkgMDhMkyVJAwQFCAQNAgESAoQCBwEBBwlIgAFDpNkyAjDkm/5MEBwdf+gUEl/6AoVZkmX/oLClv6pf+DQn1/4+E3//0gFBkACBv/SBYI7D5JiDLJx9CBAR4CAoWQQ4Z9DgAA=="))
|
||||
},
|
||||
{
|
||||
type: "txt",
|
||||
font: "Vector:30",
|
||||
label: "Saved!"
|
||||
}]
|
||||
});
|
||||
layout.render();
|
||||
setTimeout(function () {
|
||||
Bangle.load();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function drawColorAssessment(){
|
||||
var dateNow = new Date();
|
||||
var lastUrineColorDate = appCache.urine && appCache.urine.colorAssessment ? appCache.urine.colorAssessment : 0;
|
||||
var hourCurrent = dateNow.getHours();
|
||||
var currentDay = YMDInt(dateNow);
|
||||
if (hourCurrent >= 16 && currentDay > lastUrineColorDate) {
|
||||
var layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "btn", font: "6x8:2", label: " ", btnFaceCol: E.HSBtoRGB(0.3, 0.99, 1), cb: l => saveUrineData(2), fillx: 1, filly: 1, pad: 1 },
|
||||
{ type: "btn", font: "6x8:2", label: " ", btnFaceCol: E.HSBtoRGB(0.2, 1, 1), cb: l => saveUrineData(1), fillx: 1, filly: 1, pad: 1 }
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "h", c: [
|
||||
{ type: "btn", font: "6x8:2", label: " ", btnFaceCol: E.HSBtoRGB(0.38, 1, 1), cb: l => saveUrineData(3), fillx: 1, filly: 1, pad: 1 },
|
||||
{ type: "btn", font: "6x8:2", label: " ", btnFaceCol: E.HSBtoRGB(0.44, 1, 0.9), cb: l => saveUrineData(4), fillx: 1, filly: 1, pad: 1 },
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
g.clear();
|
||||
g.reset();
|
||||
layout.render();
|
||||
} else {
|
||||
saveUrineData(0);
|
||||
}
|
||||
}
|
||||
|
||||
drawColorAssessment();
|
||||
/*
|
||||
//Urine Colours adapted NSW chart and from the Hillmen Urine Chart to includes blood presence
|
||||
//https://www.health.nsw.gov.au/environment/beattheheat/Pages/urine-colour-chart.aspx
|
||||
|
||||
*/
|
||||
|
|
@ -0,0 +1,819 @@
|
|||
(() => {
|
||||
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));
|
||||
});
|
||||
})();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1,30 @@
|
|||
{ "id": "heatsuite",
|
||||
"name": "HeatSuite",
|
||||
"shortName":"HeatSuite",
|
||||
"version":"0.10",
|
||||
"description": "The smartwatch software to integrate with HeatSuite",
|
||||
"icon": "icon.png",
|
||||
"type": "app",
|
||||
"tags": "health,tool",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"dependencies" : { "bthrm":"app", "gpssetup" : "app", "coretemp":"app"},
|
||||
"customConnect": true,
|
||||
"custom": "custom.html",
|
||||
"storage": [
|
||||
{"name":"heatsuite.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"HSModule","url":"heatsuite.module.js"},
|
||||
{"name":"heatsuite.app.js","url":"heatsuite.app.js"},
|
||||
{"name":"heatsuite.settings.js","url":"heatsuite.settings.js"},
|
||||
{"name":"heatsuite.wid.js","url":"heatsuite.wid.js"},
|
||||
{"name":"heatsuite.survey.js","url":"heatsuite.survey.js"},
|
||||
{"name":"heatsuite.bletemp.js","url":"heatsuite.bletemp.js"},
|
||||
{"name":"heatsuite.bp.js","url":"heatsuite.bp.js"},
|
||||
{"name":"heatsuite.mass.js","url":"heatsuite.mass.js"},
|
||||
{"name":"heatsuite.urine.js","url":"heatsuite.urine.js"}
|
||||
],
|
||||
"data": [
|
||||
{"name":"heatsuite.settings.json"},
|
||||
{"name":"heatsuite.cache.json"}
|
||||
]
|
||||
}
|
||||
2
core
2
core
|
|
@ -1 +1 @@
|
|||
Subproject commit ef99424a9fbd01be504841a6c759ba4292a542f7
|
||||
Subproject commit 76b887dd6fc5693786fc1e63b97f3e4ab4306ed7
|
||||
Loading…
Reference in New Issue