bthrm - Move caching of characteristics out of the lib into the settings app

pull/3349/head
Martin Boonk 2024-04-10 19:02:06 +02:00
parent 47af3b482a
commit 4e696ee36e
3 changed files with 186 additions and 142 deletions

View File

@ -15,8 +15,7 @@
"custom_fallbackTimeout": 10, "custom_fallbackTimeout": 10,
"gracePeriodNotification": 0, "gracePeriodNotification": 0,
"gracePeriodConnect": 0, "gracePeriodConnect": 0,
"gracePeriodService": 0,
"gracePeriodRequest": 0, "gracePeriodRequest": 0,
"bonding": false, "bonding": false,
"active": true "active": false
} }

View File

@ -16,55 +16,20 @@ exports.enable = () => {
log("Settings: ", settings); log("Settings: ", settings);
if (settings.enabled){ if (settings.enabled && settings.cache){
var clearCache = function() { log("Start");
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 {};
};
var addNotificationHandler = function(characteristic) { var addNotificationHandler = function(characteristic) {
log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/); log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/);
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value)); 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 characteristicsFromCache = function(device) {
var service = { device : device }; // fake a BluetoothRemoteGATTService var service = { device : device }; // fake a BluetoothRemoteGATTService
log("Read cached characteristics"); log("Read cached characteristics");
var cache = getCache(); var cache = settings.cache;
if (!cache.characteristics) return []; if (!cache.characteristics) return [];
var restored = []; var restored = [];
for (var c in cache.characteristics){ for (var c in cache.characteristics){
@ -84,18 +49,6 @@ exports.enable = () => {
return restored; return restored;
}; };
log("Start");
var lastReceivedData={
};
var supportedServices = [
"0x180d", // Heart Rate
"0x180f", // Battery
];
var bpmTimeout;
var supportedCharacteristics = { var supportedCharacteristics = {
"0x2a37": { "0x2a37": {
//Heart rate measurement //Heart rate measurement
@ -177,6 +130,7 @@ exports.enable = () => {
//Body sensor location //Body sensor location
handler: function(dv){ handler: function(dv){
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {}; if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
log("Got location", dv);
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10); lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
} }
}, },
@ -189,6 +143,11 @@ exports.enable = () => {
} }
}; };
var lastReceivedData={
};
var bpmTimeout;
var device; var device;
var gatt; var gatt;
var characteristics = []; var characteristics = [];
@ -198,11 +157,6 @@ exports.enable = () => {
var maxRetryTime = 60000; var maxRetryTime = 60000;
var retryTime = initialRetryTime; var retryTime = initialRetryTime;
var connectSettings = {
minInterval: 7.5,
maxInterval: 1500
};
var waitingPromise = function(timeout) { var waitingPromise = function(timeout) {
return new Promise(function(resolve){ return new Promise(function(resolve){
log("Start waiting for " + timeout); log("Start waiting for " + timeout);
@ -416,22 +370,13 @@ exports.enable = () => {
promise = promise.then(()=>{ promise = promise.then(()=>{
gatt = device.gatt; 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); return Promise.resolve(gatt);
}); });
promise = promise.then((gatt)=>{ promise = promise.then((gatt)=>{
if (!gatt.connected){ if (!gatt.connected){
log("Connecting..."); log("Connecting...");
var connectPromise = gatt.connect(connectSettings).then(function() { var connectPromise = gatt.connect().then(function() {
log("Connected."); log("Connected.");
}); });
if (settings.gracePeriodConnect){ if (settings.gracePeriodConnect){
@ -447,63 +392,20 @@ exports.enable = () => {
} }
}); });
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(()=>{ promise = promise.then(()=>{
if (!characteristics || characteristics.length == 0){ if (!characteristics || characteristics.length == 0){
characteristics = characteristicsFromCache(device); characteristics = characteristicsFromCache(device);
} }
}); let characteristicsPromise = Promise.resolve();
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){ for (var characteristic of characteristics){
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true); characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
} }
}
return characteristicsPromise; return characteristicsPromise;
}); });
return promise.then(()=>{ return promise.then(()=>{
log("Connection established, waiting for notifications"); log("Connection established, waiting for notifications");
characteristicsToCache(characteristics);
clearRetryTimeout(true); clearRetryTimeout(true);
}).catch((e) => { }).catch((e) => {
characteristics = []; characteristics = [];

View File

@ -1,6 +1,6 @@
(function(back) { (function(back) {
function writeSettings(key, value) { function writeSettings(key, value) {
var s = require('Storage').readJSON(FILE, true) || {}; let s = require('Storage').readJSON(FILE, true) || {};
s[key] = value; s[key] = value;
require('Storage').writeJSON(FILE, s); require('Storage').writeJSON(FILE, s);
readSettings(); readSettings();
@ -13,10 +13,16 @@
); );
} }
var FILE="bthrm.json"; let FILE="bthrm.json";
var settings; let settings;
readSettings(); readSettings();
let log = ()=>{};
if (settings.debuglog)
log = print;
const bthrm = require("bthrm");
function applyCustomSettings(){ function applyCustomSettings(){
writeSettings("enabled",true); writeSettings("enabled",true);
writeSettings("replace",settings.custom_replace); writeSettings("replace",settings.custom_replace);
@ -26,7 +32,7 @@
} }
function buildMainMenu(){ function buildMainMenu(){
var mainmenu = { let mainmenu = {
'': { 'title': 'Bluetooth HRM' }, '': { 'title': 'Bluetooth HRM' },
'< Back': back, '< Back': back,
'Mode': { 'Mode': {
@ -63,12 +69,13 @@
}; };
if (settings.btname || settings.btid){ if (settings.btname || settings.btid){
var name = "Clear " + (settings.btname || settings.btid); let name = "Clear " + (settings.btname || settings.btid);
mainmenu[name] = function() { mainmenu[name] = function() {
E.showPrompt("Clear current device?").then((r)=>{ E.showPrompt("Clear current device?").then((r)=>{
if (r) { if (r) {
writeSettings("btname",undefined); writeSettings("btname",undefined);
writeSettings("btid",undefined); writeSettings("btid",undefined);
writeSettings("cache", undefined);
} }
E.showMenu(buildMainMenu()); E.showMenu(buildMainMenu());
}); });
@ -81,7 +88,7 @@
return mainmenu; return mainmenu;
} }
var submenu_debug = { let submenu_debug = {
'' : { title: "Debug"}, '' : { title: "Debug"},
'< Back': function() { E.showMenu(buildMainMenu()); }, '< Back': function() { E.showMenu(buildMainMenu()); },
'Alert on disconnect': { 'Alert on disconnect': {
@ -111,11 +118,137 @@
'Grace periods': function() { E.showMenu(submenu_grace); } '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(){ function createMenuFromScan(){
E.showMenu(); E.showMenu();
E.showMessage("Scanning for 4 seconds"); E.showMessage("Scanning for 4 seconds");
var submenu_scan = { let submenu_scan = {
'< Back': function() { E.showMenu(buildMainMenu()); } '< Back': function() { E.showMenu(buildMainMenu()); }
}; };
NRF.findDevices(function(devices) { NRF.findDevices(function(devices) {
@ -126,27 +259,47 @@
return; return;
} else { } else {
devices.forEach((d) => { devices.forEach((d) => {
print("Found device", d); log("Found device", d);
var shown = (d.name || d.id.substr(0, 17)); let shown = (d.name || d.id.substr(0, 17));
submenu_scan[shown] = function () { submenu_scan[shown] = function () {
E.showPrompt("Set " + shown + "?").then((r) => { E.showPrompt("Set " + shown + "?").then((r) => {
if (r) { if (r) {
E.showMessage("Connecting...");
let count = 0;
const successHandler = ()=>{
E.showAlert("Success").then(()=>{
writeSettings("btid", d.id); writeSettings("btid", d.id);
// Store the name for displaying later. Will connect by ID // Store the name for displaying later. Will connect by ID
if (d.name) { if (d.name) {
writeSettings("btname", d.name); writeSettings("btname", d.name);
} }
}
E.showMenu(buildMainMenu()); 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(submenu_scan); E.showMenu(submenu_scan);
}, { timeout: 4000, active: true, filters: [{services: [ "180d" ]}]}); }, { timeout: 4000, active: true, filters: [{services: [ "180d" ]}]});
} }
var submenu_custom = { let submenu_custom = {
'' : { title: "Custom mode"}, '' : { title: "Custom mode"},
'< Back': function() { E.showMenu(buildMainMenu()); }, '< Back': function() { E.showMenu(buildMainMenu()); },
'Replace HRM': { 'Replace HRM': {
@ -183,7 +336,7 @@
}, },
}; };
var submenu_grace = { let submenu_grace = {
'' : { title: "Grace periods"}, '' : { title: "Grace periods"},
'< Back': function() { E.showMenu(submenu_debug); }, '< Back': function() { E.showMenu(submenu_debug); },
'Request': { 'Request': {
@ -215,16 +368,6 @@
onchange: v => { onchange: v => {
writeSettings("gracePeriodNotification",v); writeSettings("gracePeriodNotification",v);
} }
},
'Service': {
value: settings.gracePeriodService,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodService",v);
}
} }
}; };