updates to coretemp

master
nravanelli 2025-04-25 10:53:51 +08:00
parent 8f9490459c
commit f6b8aa5942
11 changed files with 890 additions and 223 deletions

View File

@ -3,3 +3,4 @@
0.03: Move code for recording to this app 0.03: Move code for recording to this app
0.04: Use default Bangle formatter for booleans 0.04: Use default Bangle formatter for booleans
0.05: Minor code improvements 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)

View File

@ -1,19 +1,43 @@
# CoreTemp display # 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 ## Usage
Background task connects to any CoreTemp device (2100/2101) and emits a CoreTemp signal value for each reading. 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 'CoreTemp' signal on activity if activated in settings. 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 with a mini value and blinks on use. The widget shows when the sensor is enabled and connected (green) or disconnected (grey).
The app listens for 'CoreTemp' signals and shows the current skin and core temperatures in large numbers. 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 ## TODO
* Integrate with other tracking/sports apps to log data. * 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 Ivor Hewitt
[Nicholas Ravanelli](https://github.com/nravanelli)

View File

@ -1,3 +1,4 @@
{ {
"enabled":false "enabled":false,
"debuglog": false
} }

View File

@ -1,114 +1 @@
// if ((require('Storage').readJSON("coretemp.json", true) || {}).enabled != false) require("CORESensor").enable();
// 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(); });
})();

View File

@ -1,5 +1,5 @@
var settings = require("Storage").readJSON("coretemp.json", 1) || {};
// Simply listen for core events and show data // Simply listen for core events and show data
//var btm = g.getHeight() - 1; //var btm = g.getHeight() - 1;
var px = g.getWidth() / 2; var px = g.getWidth() / 2;
@ -21,25 +21,14 @@ var corelogo = {
function onCore(c) { function onCore(c) {
// Large or small font // Large or small font
var sz = ((process.env.HWVERSION == 1) ? 3 : 2); var sz = ((process.env.HWVERSION == 1) ? 3 : 2);
g.setFontAlign(0, 0); g.setFontAlign(0, 0);
g.clearRect(0, 32 + 48, g.getWidth(), 32 + 48 + 24 * 4); g.clearRect(0, 32 + 48, g.getWidth(), 32 + 48 + 24 * 4);
g.setColor(g.theme.dark ? "#CCC" : "#333"); // gray g.setColor(g.theme.dark ? "#CCC" : "#333"); // gray
g.setFont("6x8", sz).drawString( g.setFont("6x8", sz).drawString("Core: " + ((c.core < 327) ? (c.core + c.unit) : 'n/a'), px, 48 + 48);
"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("Skin: " + c.skin + c.unit, px, 48 + 48 + 24); 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);
// 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...");
}
} }
function drawBackground(message) { function drawBackground(message) {
@ -50,17 +39,11 @@ function drawBackground(message) {
g.drawImage(corelogo, px - 146 / 2, 30); g.drawImage(corelogo, px - 146 / 2, 30);
g.drawString(message, g.getWidth() / 2, g.getHeight() / 2 + 16); g.drawString(message, g.getWidth() / 2, g.getHeight() / 2 + 16);
} }
Bangle.setCORESensorPower(1,"COREAPP");
Bangle.on('CoreTemp', onCore); Bangle.on('CORESensor', onCore);
settings = require("Storage").readJSON("coretemp.json", 1) || {};
if (!settings.enabled) { if (!settings.enabled) {
drawBackground("Sensor off\nBTN" + drawBackground("Sensor off\nEnable in Settings");
((process.env.HWVERSION == 1) ? '2' : '1') + " to enable");
} else { } else {
drawBackground("Waiting for\ndata..."); drawBackground("Waiting for\ndata...");
} }
setWatch(() => { enableSensor(); }, (process.env.HWVERSION == 1) ? BTN2 : BTN1,
{repeat : false});

279
apps/coretemp/lib.js Normal file
View File

@ -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;
}
});
}
};

View File

@ -1,7 +1,7 @@
{ {
"id": "coretemp", "id": "coretemp",
"name": "CoreTemp", "name": "CoreTemp",
"version": "0.05", "version": "0.06",
"description": "Display CoreTemp device sensor data", "description": "Display CoreTemp device sensor data",
"icon": "coretemp.png", "icon": "coretemp.png",
"type": "app", "type": "app",
@ -11,10 +11,11 @@
"storage": [ "storage": [
{"name":"coretemp.wid.js","url":"widget.js"}, {"name":"coretemp.wid.js","url":"widget.js"},
{"name":"coretemp.app.js","url":"coretemp.js"}, {"name":"coretemp.app.js","url":"coretemp.js"},
{"name":"CORESensor","url":"lib.js"},
{"name":"coretemp.recorder.js","url":"recorder.js"}, {"name":"coretemp.recorder.js","url":"recorder.js"},
{"name":"coretemp.settings.js","url":"settings.js"}, {"name":"coretemp.settings.js","url":"settings.js"},
{"name":"coretemp.img","url":"coretemp-icon.js","evaluate":true}, {"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"}], "data": [{"name":"coretemp.json","url":"app-settings.json"}],
"screenshots": [{"url":"screenshot.png"}] "screenshots": [{"url":"screenshot.png"}]

View File

@ -1,28 +1,40 @@
(function(recorders) { (function(recorders) {
recorders.coretemp = function() { recorders.coretemp = function() {
var core = "", skin = ""; var core = "", skin = "", unit="", hr="", heatflux="", hsi="", battery="", quality="";
var hasCore = false; var hasCore = false;
function onCore(c) { function onCore(c) {
core=c.core; core=c.core;
skin=c.skin; skin=c.skin;
hasCore = true; hasCore = true;
unit = c.unit;
hr = c.hr;
heatflux = c.heatflux;
hsi = c.hsi;
battery= c.battery;
quality = c.dataQuality;
} }
return { return {
name : "Core", name : "Core",
fields : ["Core","Skin"], fields : ["Core","Skin","Unit","HeartRate","HeatFlux","HeatStrainIndex","Battery","Quality"],
getValues : () => { getValues : () => {
var r = [core,skin]; var r = [core,skin,unit,hr,heatflux,hsi,battery,quality];
core = ""; core = "";
skin = ""; skin = "";
unit="";
hr="";
heatflux="";
hsi="";
battery="";
quality="";
return r; return r;
}, },
start : () => { start : () => {
hasCore = false; hasCore = false;
Bangle.on('CoreTemp', onCore); Bangle.on('CORESensor', onCore);
}, },
stop : () => { stop : () => {
hasCore = false; hasCore = false;
Bangle.removeListener('CoreTemp', onCore); Bangle.removeListener('CORESensor', onCore);
}, },
draw : (x,y) => g.setColor(hasCore?"#0f0":"#8f8").drawImage(atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),x,y) draw : (x,y) => g.setColor(hasCore?"#0f0":"#8f8").drawImage(atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),x,y)
}; };

View File

@ -2,45 +2,532 @@
/** /**
* @param {function} back Use back() to return to settings menu * @param {function} back Use back() to return to settings menu
*/ */
(function(back) { (function (back) {
var settings = {};
const SETTINGS_FILE = 'coretemp.json' const SETTINGS_FILE = 'coretemp.json'
// initialize with default settings... var CORECONNECTED = false;
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];
}
// creates a function to safe a specific setting, e.g. save('color')(1) // creates a function to safe a specific setting, e.g. save('color')(1)
function save(key) { function writeSettings(key, value) {
return function (value) { let s = require('Storage').readJSON(SETTINGS_FILE, true) || {};
s[key] = value; s[key] = value;
storage.write(SETTINGS_FILE, s); require('Storage').writeJSON(SETTINGS_FILE, s);
} readSettings();
} }
function updateSettings() { function readSettings() {
require("Storage").write("coretemp.json", s); settings = Object.assign(
if (WIDGETS["coretemp"]) require('Storage').readJSON(SETTINGS_FILE, true) || {}
WIDGETS["coretemp"].reload(); );
return;
}
const menu = {
'' : {'title' : 'CoreTemp sensor'},
'< Back' : back,
'Enabled' : {
value : !!s.enabled,
onchange : v => {
s.enabled = v;
updateSettings();
}
} }
}
E.showMenu(menu); 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;
}
// 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);
}
}
}
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();
})

View File

@ -1,62 +1,54 @@
// TODO Change to a generic multiple sensor widget?
(() => { (() => {
var settings = {}; var settings = {};
var count = 0; var CORESensorStatus = false;
var core = 0;
// draw your widget // draw your widget
function draw() { function draw() {
if (!settings.enabled) if (!settings.widget)
return; return;
g.reset(); g.reset();
g.setFont("6x8", 1).setFontAlign(0, 0); g.setFont("6x8", 1).setFontAlign(0, 0);
g.setFontAlign(0, 0); g.setFontAlign(0, 0);
g.clearRect(this.x, this.y, this.x + 23, this.y + 23); g.clearRect(this.x, this.y, this.x + 23, this.y + 23);
if (count & 1) { if (CORESensorStatus) {
g.setColor("#0f0"); // green g.setColor("#0f0"); // green
} else { } else {
g.setColor(g.theme.dark ? "#333" : "#CCC"); // off = grey g.setColor(g.theme.dark ? "#333" : "#CCC"); // off = grey
} }
g.drawImage( 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); 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); 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 // Called by sensor app to update status
function reload() { function reload() {
settings = require("Storage").readJSON("coretemp.json", 1) || {}; settings = require("Storage").readJSON("coretemp.json", 1) || {};
if (!settings.widget) {
Bangle.removeListener('CoreTemp', onTemp); delete WIDGETS["coretemp"];
return;
}
if (settings.enabled) { if (settings.enabled) {
WIDGETS["coretemp"].width = 24; WIDGETS["coretemp"].width = 24;
Bangle.on('CoreTemp', onTemp);
} else { } else {
WIDGETS["coretemp"].width = 0; WIDGETS["CORESensor"].width = 0;
count = 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 // add the widget
WIDGETS["coretemp"] = { WIDGETS["coretemp"] = {
area : "tl", area: "tl",
width : 24, width: 24,
draw : draw, draw: draw,
reload : function() { reload: function () {
reload(); reload();
Bangle.drawWidgets(); // relayout all widgets Bangle.drawWidgets(); // relayout all widgets
} }

View File

@ -2,7 +2,7 @@
"id": "gpssetup", "id": "gpssetup",
"name": "GPS Setup", "name": "GPS Setup",
"shortName": "GPS Setup", "shortName": "GPS Setup",
"version": "0.03", "version": "0.04",
"description": "Configure the GPS power options and store them in the GPS nvram", "description": "Configure the GPS power options and store them in the GPS nvram",
"icon": "gpssetup.png", "icon": "gpssetup.png",
"tags": "gps,tools,outdoors", "tags": "gps,tools,outdoors",