1
0
Fork 0
BangleApps/apps/btadv/app.ts

357 lines
9.1 KiB
TypeScript
Raw Normal View History

2023-01-20 22:28:14 +00:00
// TODO: emit other data beside HRM (via set/updateServices)
const enum Intervals {
BLE_ADVERT = 30 * 1000,
BLE = 1000,
MENU_WAKE = 1000,
MENU_SLEEP = 30 * 1000,
}
2023-01-20 21:04:11 +00:00
let acc: undefined | AccelData;
let bar: undefined | PressureData;
let gps: undefined | GPSFix;
let hrm: undefined | { bpm: number, confidence: number };
let mag: undefined | CompassData;
type BtAdvMenu = "acc" | "bar" | "gps" | "hrm" | "mag" | "main";
let curMenu: BtAdvMenu = "main";
let mainMenuScroll = 0;
const settings = {
barEnabled: false,
gpsEnabled: false,
hrmEnabled: false,
magEnabled: false,
};
const showMainMenu = () => {
const onOff = (b: boolean) => b ? " (on)" : " (off)";
const mainMenu: Menu = {};
const showMenu = (menu: Menu, scroll: number, cur: BtAdvMenu) => () => {
E.showMenu(menu);
mainMenuScroll = scroll;
curMenu = cur;
};
mainMenu[""] = {
2023-01-20 22:28:14 +00:00
"title": "BLE Advert",
2023-01-20 21:04:11 +00:00
};
(mainMenu[""] as any).scroll = mainMenuScroll; // typehack
mainMenu["Acceleration"] = showMenu(accMenu, 0, "acc");
mainMenu["Barometer" + onOff(settings.barEnabled)] = showMenu(barMenu, 1, "bar");
mainMenu["GPS" + onOff(settings.gpsEnabled)] = showMenu(gpsMenu, 2, "gps");
mainMenu["Heart Rate" + onOff(settings.hrmEnabled)] = showMenu(hrmMenu, 3, "hrm");
mainMenu["Magnetometer" + onOff(settings.magEnabled)] = showMenu(magMenu, 4, "mag");
mainMenu["Exit"] = () => (load as any)(); // avoid `this` + typehack
E.showMenu(mainMenu);
curMenu = "main";
};
2023-01-20 22:28:14 +00:00
const optionsCommon = {
back: showMainMenu,
};
2023-01-20 21:04:11 +00:00
const accMenu = {
2023-01-20 22:28:14 +00:00
"": { "title" : "Acceleration -", ...optionsCommon },
2023-01-20 21:04:11 +00:00
"Active": { value: "true (fixed)" },
"x": { value: "" },
"y": { value: "" },
"z": { value: "" },
};
const barMenu = {
2023-01-20 22:28:14 +00:00
"": { "title" : "Barometer", ...optionsCommon },
2023-01-20 21:04:11 +00:00
"Active": {
value: settings.barEnabled,
onchange: (v: boolean) => updateSetting('barEnabled', v),
},
"Altitude": { value: "" },
"Press": { value: "" },
"Temp": { value: "" },
};
const gpsMenu = {
2023-01-20 22:28:14 +00:00
"": { "title" : "GPS", ...optionsCommon },
2023-01-20 21:04:11 +00:00
"Active": {
value: settings.gpsEnabled,
onchange: (v: boolean) => updateSetting('gpsEnabled', v),
},
"Lat": { value: "" },
"Lon": { value: "" },
"Altitude": { value: "" },
"Satellites": { value: "" },
"HDOP": { value: "" },
};
const hrmMenu = {
2023-01-20 22:28:14 +00:00
"": { "title" : "- Heart Rate -", ...optionsCommon },
2023-01-20 21:04:11 +00:00
"Active": {
value: settings.hrmEnabled,
onchange: (v: boolean) => updateSetting('hrmEnabled', v),
},
"BPM": { value: "" },
"Confidence": { value: "" },
};
const magMenu = {
2023-01-20 22:28:14 +00:00
"": { "title" : "Magnetometer", ...optionsCommon },
2023-01-20 21:04:11 +00:00
"Active": {
value: settings.magEnabled,
onchange: (v: boolean) => updateSetting('magEnabled', v),
},
"x": { value: "" },
"y": { value: "" },
"z": { value: "" },
"Heading": { value: "" },
};
const updateMenu = () => {
switch (curMenu) {
case "acc":
if (acc) {
accMenu.x.value = acc.x.toFixed(2);
accMenu.y.value = acc.y.toFixed(2);
accMenu.z.value = acc.z.toFixed(2);
E.showMenu(accMenu);
}
break;
case "bar":
if (bar) {
barMenu.Altitude.value = bar.altitude.toFixed(1) + 'm';
barMenu.Press.value = bar.pressure.toFixed(1) + 'mbar';
barMenu.Temp.value = bar.temperature.toFixed(1) + 'C';
E.showMenu(barMenu);
}
break;
case "gps":
if (gps) {
gpsMenu.Lat.value = gps.lat.toFixed(4);
gpsMenu.Lon.value = gps.lon.toFixed(4);
gpsMenu.Altitude.value = gps.alt + 'm';
gpsMenu.Satellites.value = "" + gps.satellites;
gpsMenu.HDOP.value = (gps.hdop * 5).toFixed(1) + 'm';
E.showMenu(gpsMenu);
}
break;
case "hrm":
if (hrm) {
hrmMenu.BPM.value = "" + hrm.bpm;
hrmMenu.Confidence.value = hrm.confidence + '%';
E.showMenu(hrmMenu);
}
break;
case "mag":
if (mag) {
magMenu.x.value = "" + mag.x;
magMenu.y.value = "" + mag.y;
magMenu.z.value = "" + mag.z;
magMenu.Heading.value = mag.heading.toFixed(1);
E.showMenu(magMenu);
}
break;
}
};
2023-01-20 22:28:14 +00:00
const enum BleServ {
HRM = "0x180d",
}
const enum BleChar {
HRM = "0x2a37",
}
2023-01-20 21:04:11 +00:00
const updateBleAdvert = () => {
2023-01-20 22:28:14 +00:00
let bleAdvert: { [key: string]: undefined };
2023-01-20 21:04:11 +00:00
if (!(bleAdvert = (Bangle as any).bleAdvert))
bleAdvert = (Bangle as any).bleAdvert = {};
// const data = [ APP_ADVERTISING_DATA ]; // Always advertise at least app name
// if (bar) {
// data.push(encodeBarServiceData(bar));
// bar = undefined;
// }
// if (gps && gps.lat && gps.lon) {
// data.push(encodeGpsServiceData(gps));
// gps = undefined;
// }
if (hrm) {
2023-01-20 22:28:14 +00:00
bleAdvert[BleServ.HRM] = undefined; // Advertise HRM
2023-01-20 21:04:11 +00:00
// hack
if (NRF.getSecurityStatus().connected) {
NRF.updateServices({
2023-01-20 22:28:14 +00:00
[BleServ.HRM]: {
[BleChar.HRM]: {
2023-01-20 21:04:11 +00:00
value: [0, hrm.bpm],
notify: true,
}
}
})
return;
}
// data.push({ 0x2a37: [ 0, hrm.bpm ] });
// hrm = undefined;
}
// if (mag) {
// data.push(encodeMagServiceData(mag));
// mag = undefined;
// }
2023-01-20 22:28:14 +00:00
NRF.setAdvertising((Bangle as any).bleAdvert);
2023-01-20 21:04:11 +00:00
};
// {
// flags: u8,
// bytes: [u8...]
// }
// flags {
// 1 << 0: 16bit bpm
// 1 << 1: sensor contact available
// 1 << 2: sensor contact boolean
// 1 << 3: energy expended, next 16 bits
// 1 << 4: "rr" data available, u16s, intervals
// }
const encodeHrm = () => [0, hrm ? hrm.bpm : 0];
const encodeBarServiceData = (data: PressureData) => {
const t = toByteArray(Math.round(data.temperature * 100), 2, true);
const p = toByteArray(Math.round(data.pressure * 1000), 4, false);
const e = toByteArray(Math.round(data.altitude * 100), 3, true);
return [
0x02, 0x01, 0x06, // Flags
0x05, 0x16, 0x6e, 0x2a, t[0], t[1], // Temperature
0x07, 0x16, 0x6d, 0x2a, p[0], p[1], p[2], p[3], // Pressure
0x06, 0x16, 0x6c, 0x2a, e[0], e[1], e[2] // Elevation
];
};
const encodeGpsServiceData = (data: GPSFix) => {
const s = toByteArray(Math.round(1000 * data.speed / 36), 2, false);
const lat = toByteArray(Math.round(data.lat * 10000000), 4, true);
const lon = toByteArray(Math.round(data.lon * 10000000), 4, true);
const e = toByteArray(Math.round(data.alt * 100), 3, true);
const h = toByteArray(Math.round(data.course * 100), 2, false);
return [
0x02, 0x01, 0x06, // Flags
0x14, 0x16, 0x67, 0x2a, 0x9d, 0x02, s[0], s[1], lat[0], lat[1], lat[2],
lat[3], lon[0], lon[1], lon[2], lon[3], e[0], e[1], e[2], h[0], h[1]
// Location and Speed
];
};
const encodeMagServiceData = (data: CompassData) => {
const x = toByteArray(data.x, 2, true);
const y = toByteArray(data.y, 2, true);
const z = toByteArray(data.z, 2, true);
return [
0x02, 0x01, 0x06, // Flags
0x09, 0x16, 0xa1, 0x2a, x[0], x[1], y[0], y[1], z[0], z[1] // Mag 3D
];
};
const toByteArray = (value: number, numberOfBytes: number, isSigned: boolean) => {
const byteArray: Array<number> = new Array(numberOfBytes);
if(isSigned && (value < 0)) {
value += 1 << (numberOfBytes * 8);
}
for(let index = 0; index < numberOfBytes; index++) {
byteArray[index] = (value >> (index * 8)) & 0xff;
}
return byteArray;
};
const enableSensors = () => {
Bangle.setBarometerPower(settings.barEnabled, "btadv");
Bangle.setGPSPower(settings.gpsEnabled, "btadv");
Bangle.setHRMPower(settings.hrmEnabled, "btadv");
Bangle.setCompassPower(settings.magEnabled, "btadv");
};
const updateSetting = (
name: keyof typeof settings,
value: boolean,
) => {
settings[name] = value;
//require('Storage').writeJSON(SETTINGS_FILENAME, settings);
enableSensors();
};
// ----------------------------
NRF.setServices(
{
2023-01-20 22:28:14 +00:00
[BleServ.HRM]: {
[BleChar.HRM]: {
2023-01-20 21:04:11 +00:00
value: encodeHrm(),
readable: true,
notify: true,
//indicate: true, // notify + ACK
},
},
},
{
advertise: [
'180d',
]
},
);
const updateServices = () => {
NRF.updateServices({
2023-01-20 22:28:14 +00:00
[BleServ.HRM]: {
[BleChar.HRM]: {
2023-01-20 21:04:11 +00:00
value: encodeHrm(),
notify: true,
}
}
});
};
Bangle.on('accel', newAcc => acc = newAcc);
Bangle.on('pressure', newBar => bar = newBar);
Bangle.on('GPS', newGps => gps = newGps);
Bangle.on('HRM', newHrm => hrm = newHrm);
Bangle.on('mag', newMag => mag = newMag);
2023-01-20 22:28:14 +00:00
// show menu first to have it reserve space for widgets (appRect)
Bangle.loadWidgets();
Bangle.drawWidgets();
2023-01-20 21:04:11 +00:00
showMainMenu();
2023-01-20 22:28:14 +00:00
enableSensors();
2023-01-20 21:04:11 +00:00
2023-01-20 22:28:14 +00:00
setInterval(updateBleAdvert, Intervals.BLE_ADVERT);
2023-01-20 21:04:11 +00:00
2023-01-20 22:28:14 +00:00
const menuInterval = setInterval(updateMenu, Intervals.MENU_WAKE);
Bangle.on("lock", locked => {
changeInterval(
menuInterval,
locked ? Intervals.MENU_SLEEP : Intervals.MENU_WAKE,
);
});
let serviceInterval: undefined | number;
2023-01-20 21:04:11 +00:00
NRF.on("connect", () => {
2023-01-20 22:28:14 +00:00
serviceInterval = setInterval(updateServices, Intervals.BLE);
2023-01-20 21:04:11 +00:00
});
NRF.on("disconnect", () => {
2023-01-20 22:28:14 +00:00
clearInterval(serviceInterval);
serviceInterval = undefined;
2023-01-20 21:04:11 +00:00
});