533 lines
19 KiB
JavaScript
533 lines
19 KiB
JavaScript
// This file should contain exactly one function, which shows the app's settings
|
|
/**
|
|
* @param {function} back Use back() to return to settings menu
|
|
*/
|
|
(function (back) {
|
|
var settings = {};
|
|
const SETTINGS_FILE = 'coretemp.json'
|
|
var CORECONNECTED = false;
|
|
// creates a function to safe a specific setting, e.g. save('color')(1)
|
|
function writeSettings(key, value) {
|
|
let s = require('Storage').readJSON(SETTINGS_FILE, true) || {};
|
|
s[key] = value;
|
|
require('Storage').writeJSON(SETTINGS_FILE, s);
|
|
readSettings();
|
|
}
|
|
|
|
function readSettings() {
|
|
settings = Object.assign(
|
|
require('Storage').readJSON(SETTINGS_FILE, true) || {}
|
|
);
|
|
}
|
|
|
|
readSettings();
|
|
let log = () => { };
|
|
if (settings.debuglog)
|
|
log = print;
|
|
|
|
let supportedServices = [
|
|
"00002100-5b1e-4347-b07c-97b514dae121", // Core Body Temperature Service
|
|
"0x180f", // Battery
|
|
"0x1809", // Health Thermometer Service
|
|
];
|
|
|
|
let supportedCharacteristics = [
|
|
"00002101-5b1e-4347-b07c-97b514dae121", // Core Body Temperature Characteristic
|
|
"00002102-5b1e-4347-b07c-97b514dae121", //Core Temp Control Point (opCode for extra function)
|
|
//"0x2a1c", //Thermometer
|
|
//"0x2a1d", //Sensor Location (CORE)
|
|
"0x2a19", // Battery
|
|
];
|
|
|
|
var characteristicsToCache = function (characteristics) {
|
|
log("Cache characteristics");
|
|
let cache = {};
|
|
if (!cache.characteristics) cache.characteristics = {};
|
|
for (var c of characteristics) {
|
|
log("Saving handle " + c.handle_value + " for characteristic: ", c.uuid);
|
|
cache.characteristics[c.uuid] = {
|
|
"handle": c.handle_value,
|
|
"uuid": c.uuid,
|
|
"notify": c.properties.notify,
|
|
"read": c.properties.read,
|
|
"write": c.properties.write
|
|
};
|
|
}
|
|
writeSettings("cache", cache);
|
|
};
|
|
|
|
var controlPointChar;
|
|
|
|
let createCharacteristicPromise = function (newCharacteristic) {
|
|
log("Create characteristic promise", newCharacteristic.uuid);
|
|
if (newCharacteristic.uuid === "00002102-5b1e-4347-b07c-97b514dae121") {
|
|
log("Subscribing to CoreTemp Control Point Indications.");
|
|
controlPointChar = newCharacteristic;
|
|
return controlPointChar.writeValue(new Uint8Array([0x02]), {
|
|
type: "command",
|
|
handle: true
|
|
})
|
|
.then(() => {
|
|
log("Indications enabled! Listening for responses...");
|
|
return controlPointChar.startNotifications(); //now we can send opCodes
|
|
})
|
|
.then(() => log("Finished handling CoreTemp Control Point."))
|
|
.catch(error => {
|
|
log("Error enabling indications:", error);
|
|
});
|
|
}
|
|
return Promise.resolve().then(() => log("Handled characteristic", newCharacteristic.uuid));
|
|
};
|
|
|
|
let attachCharacteristicPromise = function (promise, characteristic) {
|
|
return promise.then(() => {
|
|
log("Handling characteristic:", characteristic.uuid);
|
|
return createCharacteristicPromise(characteristic);
|
|
});
|
|
};
|
|
|
|
let characteristics;
|
|
let createCharacteristicsPromise = function (newCharacteristics) {
|
|
log("Create characteristics promise ", newCharacteristics.length);
|
|
let result = Promise.resolve();
|
|
for (let c of newCharacteristics) {
|
|
if (!supportedCharacteristics.includes(c.uuid)) continue;
|
|
log("Supporting characteristic", c.uuid);
|
|
characteristics.push(c);
|
|
|
|
result = attachCharacteristicPromise(result, c);
|
|
}
|
|
return result.then(() => log("Handled characteristics"));
|
|
};
|
|
|
|
let createServicePromise = function (service) {
|
|
log("Create service promise", service.uuid);
|
|
let result = Promise.resolve();
|
|
result = result.then(() => {
|
|
log("Handling service", service.uuid);
|
|
return service.getCharacteristics().then((c) => createCharacteristicsPromise(c));
|
|
});
|
|
return result.then(() => log("Handled service", service.uuid));
|
|
};
|
|
|
|
let attachServicePromise = function (promise, service) {
|
|
return promise.then(() => createServicePromise(service));
|
|
};
|
|
|
|
function writeToControlPoint(opCode, params) {
|
|
return new Promise((resolve, reject) => {
|
|
let data = new Uint8Array([opCode].concat(params));
|
|
|
|
if (!controlPointChar) {
|
|
log("Control Point characteristic not found! Reconnecting...");
|
|
return;
|
|
}
|
|
// 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();
|
|
}) |