diff --git a/apps/coretemp/ChangeLog b/apps/coretemp/ChangeLog index 30c775a49..141a2bfe5 100644 --- a/apps/coretemp/ChangeLog +++ b/apps/coretemp/ChangeLog @@ -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) diff --git a/apps/coretemp/README.md b/apps/coretemp/README.md index 87be44bb6..0ee967b31 100644 --- a/apps/coretemp/README.md +++ b/apps/coretemp/README.md @@ -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) diff --git a/apps/coretemp/app-settings.json b/apps/coretemp/app-settings.json index 05e922f9d..121c18b1a 100644 --- a/apps/coretemp/app-settings.json +++ b/apps/coretemp/app-settings.json @@ -1,3 +1,4 @@ { - "enabled":false + "enabled":false, + "debuglog": false } diff --git a/apps/coretemp/boot.js b/apps/coretemp/boot.js index 27d437cb1..647a0c4bc 100644 --- a/apps/coretemp/boot.js +++ b/apps/coretemp/boot.js @@ -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(); \ No newline at end of file diff --git a/apps/coretemp/coretemp.js b/apps/coretemp/coretemp.js index 0337891e1..e64747cd0 100644 --- a/apps/coretemp/coretemp.js +++ b/apps/coretemp/coretemp.js @@ -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) { @@ -50,17 +39,11 @@ function drawBackground(message) { g.drawImage(corelogo, px - 146 / 2, 30); g.drawString(message, g.getWidth() / 2, g.getHeight() / 2 + 16); } - -Bangle.on('CoreTemp', onCore); - -settings = require("Storage").readJSON("coretemp.json", 1) || {}; +Bangle.setCORESensorPower(1,"COREAPP"); +Bangle.on('CORESensor', onCore); if (!settings.enabled) { - drawBackground("Sensor off\nBTN" + - ((process.env.HWVERSION == 1) ? '2' : '1') + " to enable"); + drawBackground("Sensor off\nEnable in Settings"); } else { drawBackground("Waiting for\ndata..."); } - -setWatch(() => { enableSensor(); }, (process.env.HWVERSION == 1) ? BTN2 : BTN1, - {repeat : false}); diff --git a/apps/coretemp/lib.js b/apps/coretemp/lib.js new file mode 100644 index 000000000..885261061 --- /dev/null +++ b/apps/coretemp/lib.js @@ -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; + } + }); + } +}; \ No newline at end of file diff --git a/apps/coretemp/metadata.json b/apps/coretemp/metadata.json index 2b7de0bf0..66379ad7f 100644 --- a/apps/coretemp/metadata.json +++ b/apps/coretemp/metadata.json @@ -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"}] diff --git a/apps/coretemp/recorder.js b/apps/coretemp/recorder.js index 1499605f3..fb0352fb6 100644 --- a/apps/coretemp/recorder.js +++ b/apps/coretemp/recorder.js @@ -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) }; diff --git a/apps/coretemp/settings.js b/apps/coretemp/settings.js index 23ea09167..f79a11a4d 100644 --- a/apps/coretemp/settings.js +++ b/apps/coretemp/settings.js @@ -2,45 +2,532 @@ /** * @param {function} back Use back() to return to settings menu */ -(function(back) { +(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) { - s[key] = value; - storage.write(SETTINGS_FILE, s); - } + function writeSettings(key, value) { + let s = require('Storage').readJSON(SETTINGS_FILE, true) || {}; + s[key] = value; + require('Storage').writeJSON(SETTINGS_FILE, s); + readSettings(); } - function updateSettings() { - require("Storage").write("coretemp.json", s); - if (WIDGETS["coretemp"]) - WIDGETS["coretemp"].reload(); - return; -} - -const menu = { - '' : {'title' : 'CoreTemp sensor'}, - '< Back' : back, - 'Enabled' : { - value : !!s.enabled, - onchange : v => { - s.enabled = v; - updateSettings(); - } + function readSettings() { + settings = Object.assign( + require('Storage').readJSON(SETTINGS_FILE, true) || {} + ); } -} - 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(); +}) \ No newline at end of file diff --git a/apps/coretemp/widget.js b/apps/coretemp/widget.js index 446325118..777d622ac 100644 --- a/apps/coretemp/widget.js +++ b/apps/coretemp/widget.js @@ -1,62 +1,54 @@ -// 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=="), - 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); - + 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(-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", - width : 24, - draw : draw, - reload : function() { + area: "tl", + width: 24, + draw: draw, + reload: function () { reload(); Bangle.drawWidgets(); // relayout all widgets } diff --git a/apps/gpssetup/metadata.json b/apps/gpssetup/metadata.json index ffe8d3fd8..1d9970d86 100644 --- a/apps/gpssetup/metadata.json +++ b/apps/gpssetup/metadata.json @@ -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",