From 4e696ee36efeabc01b59a8e3c170596ad8308989 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Wed, 10 Apr 2024 19:02:06 +0200 Subject: [PATCH] bthrm - Move caching of characteristics out of the lib into the settings app --- apps/bthrm/default.json | 3 +- apps/bthrm/lib.js | 128 +++----------------------- apps/bthrm/settings.js | 197 ++++++++++++++++++++++++++++++++++------ 3 files changed, 186 insertions(+), 142 deletions(-) diff --git a/apps/bthrm/default.json b/apps/bthrm/default.json index 79605b412..e1464a9d4 100644 --- a/apps/bthrm/default.json +++ b/apps/bthrm/default.json @@ -15,8 +15,7 @@ "custom_fallbackTimeout": 10, "gracePeriodNotification": 0, "gracePeriodConnect": 0, - "gracePeriodService": 0, "gracePeriodRequest": 0, "bonding": false, - "active": true + "active": false } diff --git a/apps/bthrm/lib.js b/apps/bthrm/lib.js index a49329818..f6074a0db 100644 --- a/apps/bthrm/lib.js +++ b/apps/bthrm/lib.js @@ -16,55 +16,20 @@ exports.enable = () => { log("Settings: ", settings); - if (settings.enabled){ + if (settings.enabled && settings.cache){ - var clearCache = function() { - return require('Storage').erase("bthrm.cache.json"); - }; - - var getCache = function() { - var cache = require('Storage').readJSON("bthrm.cache.json", true) || {}; - if (settings.btid && settings.btid === cache.id) return cache; - clearCache(); - return {}; - }; + log("Start"); var addNotificationHandler = function(characteristic) { log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/); characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value)); }; - var writeCache = function(cache) { - var oldCache = getCache(); - if (oldCache !== cache) { - log("Writing cache"); - require('Storage').writeJSON("bthrm.cache.json", cache); - } else { - log("No changes, don't write cache"); - } - }; - - var characteristicsToCache = function(characteristics) { - log("Cache characteristics"); - var cache = getCache(); - if (!cache.characteristics) cache.characteristics = {}; - for (var c of characteristics){ - //"handle_value":16,"handle_decl":15 - log("Saving handle " + c.handle_value + " for characteristic: ", c); - cache.characteristics[c.uuid] = { - "handle": c.handle_value, - "uuid": c.uuid, - "notify": c.properties.notify, - "read": c.properties.read - }; - } - writeCache(cache); - }; var characteristicsFromCache = function(device) { var service = { device : device }; // fake a BluetoothRemoteGATTService log("Read cached characteristics"); - var cache = getCache(); + var cache = settings.cache; if (!cache.characteristics) return []; var restored = []; for (var c in cache.characteristics){ @@ -84,18 +49,6 @@ exports.enable = () => { return restored; }; - log("Start"); - - var lastReceivedData={ - }; - - var supportedServices = [ - "0x180d", // Heart Rate - "0x180f", // Battery - ]; - - var bpmTimeout; - var supportedCharacteristics = { "0x2a37": { //Heart rate measurement @@ -177,6 +130,7 @@ exports.enable = () => { //Body sensor location handler: function(dv){ if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {}; + log("Got location", dv); lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10); } }, @@ -189,6 +143,11 @@ exports.enable = () => { } }; + var lastReceivedData={ + }; + + var bpmTimeout; + var device; var gatt; var characteristics = []; @@ -198,11 +157,6 @@ exports.enable = () => { var maxRetryTime = 60000; var retryTime = initialRetryTime; - var connectSettings = { - minInterval: 7.5, - maxInterval: 1500 - }; - var waitingPromise = function(timeout) { return new Promise(function(resolve){ log("Start waiting for " + timeout); @@ -317,7 +271,7 @@ exports.enable = () => { result = result.then(()=>{ log("Starting notifications", newCharacteristic); var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic)); - + if (settings.gracePeriodNotification){ log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications"); startPromise = startPromise.then(()=>{ @@ -395,7 +349,7 @@ exports.enable = () => { onDisconnect(e); return; } - + if (settings.gracePeriodRequest){ log("Add " + settings.gracePeriodRequest + "ms grace period after request"); promise = promise.then(()=>{ @@ -416,22 +370,13 @@ exports.enable = () => { promise = promise.then(()=>{ gatt = device.gatt; - - let cache = getCache(); - if (device.id !== cache.id){ - log("Device ID changed from " + cache.id + " to " + device.id + ", clearing cache"); - clearCache(); - var newCache = getCache(); - newCache.id = device.id; - writeCache(newCache); - } return Promise.resolve(gatt); }); promise = promise.then((gatt)=>{ if (!gatt.connected){ log("Connecting..."); - var connectPromise = gatt.connect(connectSettings).then(function() { + var connectPromise = gatt.connect().then(function() { log("Connected."); }); if (settings.gracePeriodConnect){ @@ -446,56 +391,14 @@ exports.enable = () => { return Promise.resolve(); } }); - - if (settings.bonding){ - 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(()=>{ if (!characteristics || characteristics.length == 0){ characteristics = characteristicsFromCache(device); } - }); - - promise = promise.then(()=>{ - var characteristicsPromise = Promise.resolve(); - if (characteristics.length == 0){ - characteristicsPromise = characteristicsPromise.then(()=>{ - log("Getting services"); - return gatt.getPrimaryServices(); - }); - - characteristicsPromise = characteristicsPromise.then((services)=>{ - log("Got services", services); - var result = Promise.resolve(); - for (var service of services){ - if (!(supportedServices.includes(service.uuid))) continue; - log("Supporting service", service.uuid); - result = attachServicePromise(result, service); - } - if (settings.gracePeriodService){ - log("Add " + settings.gracePeriodService + "ms grace period after services"); - result = result.then(()=>{ - log("Wait after services"); - return waitingPromise(settings.gracePeriodService); - }); - } - return result; - }); - } else { - for (var characteristic of characteristics){ - characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true); - } + let characteristicsPromise = Promise.resolve(); + for (var characteristic of characteristics){ + characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true); } return characteristicsPromise; @@ -503,7 +406,6 @@ exports.enable = () => { return promise.then(()=>{ log("Connection established, waiting for notifications"); - characteristicsToCache(characteristics); clearRetryTimeout(true); }).catch((e) => { characteristics = []; diff --git a/apps/bthrm/settings.js b/apps/bthrm/settings.js index 459ed29fc..928ce85e7 100644 --- a/apps/bthrm/settings.js +++ b/apps/bthrm/settings.js @@ -1,6 +1,6 @@ (function(back) { function writeSettings(key, value) { - var s = require('Storage').readJSON(FILE, true) || {}; + let s = require('Storage').readJSON(FILE, true) || {}; s[key] = value; require('Storage').writeJSON(FILE, s); readSettings(); @@ -13,10 +13,16 @@ ); } - var FILE="bthrm.json"; - var settings; + let FILE="bthrm.json"; + let settings; readSettings(); + let log = ()=>{}; + if (settings.debuglog) + log = print; + + const bthrm = require("bthrm"); + function applyCustomSettings(){ writeSettings("enabled",true); writeSettings("replace",settings.custom_replace); @@ -26,7 +32,7 @@ } function buildMainMenu(){ - var mainmenu = { + let mainmenu = { '': { 'title': 'Bluetooth HRM' }, '< Back': back, 'Mode': { @@ -63,12 +69,13 @@ }; if (settings.btname || settings.btid){ - var name = "Clear " + (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); } E.showMenu(buildMainMenu()); }); @@ -81,7 +88,7 @@ return mainmenu; } - var submenu_debug = { + let submenu_debug = { '' : { title: "Debug"}, '< Back': function() { E.showMenu(buildMainMenu()); }, 'Alert on disconnect': { @@ -111,11 +118,137 @@ 'Grace periods': function() { E.showMenu(submenu_grace); } }; + let supportedServices = [ + "0x180d", // Heart Rate + "0x180f", // Battery + ]; + + let supportedCharacteristics = [ + "0x2a37", // Heart Rate + "0x2a38", // Body sensor location + "0x2a19", // Battery + ]; + + var characteristicsToCache = function(characteristics, deviceId) { + log("Cache characteristics"); + let cache = { + id: deviceId + }; + if (!cache.characteristics) cache.characteristics = {}; + for (var c of characteristics){ + //"handle_value":16,"handle_decl":15 + 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 + }; + } + writeSettings("cache", cache); + }; + + let createCharacteristicPromise = function(newCharacteristic) { + log("Create characteristic promise", newCharacteristic.uuid); + 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 cacheDevice(deviceId){ + let promise; + let filters; + let gatt; + characteristics = []; + filters = [{ id: deviceId }]; + + log("Requesting device with filters", filters); + promise = NRF.requestDevice({ filters: filters, active: settings.active }); + + promise = promise.then((d)=>{ + log("Got device", d); + gatt = d.gatt; + log("Connecting..."); + return gatt.connect().then(function() { + log("Connected."); + }); + }); + + if (settings.bonding){ + 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"); + characteristicsToCache(characteristics, deviceId); + }); + } + function createMenuFromScan(){ E.showMenu(); E.showMessage("Scanning for 4 seconds"); - var submenu_scan = { + let submenu_scan = { '< Back': function() { E.showMenu(buildMainMenu()); } }; NRF.findDevices(function(devices) { @@ -126,18 +259,38 @@ return; } else { devices.forEach((d) => { - print("Found device", d); - var shown = (d.name || d.id.substr(0, 17)); + log("Found device", d); + let shown = (d.name || d.id.substr(0, 17)); submenu_scan[shown] = function () { E.showPrompt("Set " + shown + "?").then((r) => { if (r) { - writeSettings("btid", d.id); - // Store the name for displaying later. Will connect by ID - if (d.name) { - writeSettings("btname", d.name); - } + E.showMessage("Connecting..."); + let count = 0; + const successHandler = ()=>{ + E.showAlert("Success").then(()=>{ + writeSettings("btid", d.id); + // Store the name for displaying later. Will connect by ID + if (d.name) { + writeSettings("btname", d.name); + } + E.showMenu(buildMainMenu()); + }); + }; + const errorHandler = (e)=>{ + count++; + log("ERROR", e); + if (count <= 10){ + E.showMessage("Error during caching, Retry " + count + "/10", e); + return cacheDevice(d.id).then(successHandler).catch(errorHandler); + } else { + E.showAlert("Error during caching", e).then(()=>{ + E.showMenu(buildMainMenu()); + }); + } + }; + + return cacheDevice(d.id).then(successHandler).catch(errorHandler); } - E.showMenu(buildMainMenu()); }); }; }); @@ -146,7 +299,7 @@ }, { timeout: 4000, active: true, filters: [{services: [ "180d" ]}]}); } - var submenu_custom = { + let submenu_custom = { '' : { title: "Custom mode"}, '< Back': function() { E.showMenu(buildMainMenu()); }, 'Replace HRM': { @@ -183,7 +336,7 @@ }, }; - var submenu_grace = { + let submenu_grace = { '' : { title: "Grace periods"}, '< Back': function() { E.showMenu(submenu_debug); }, 'Request': { @@ -215,16 +368,6 @@ onchange: v => { writeSettings("gracePeriodNotification",v); } - }, - 'Service': { - value: settings.gracePeriodService, - min: 0, - max: 3000, - step: 100, - format: v=>v+"ms", - onchange: v => { - writeSettings("gracePeriodService",v); - } } };