btadv: handle remaining sensors

pull/2587/head
Rob Pilling 2023-01-22 21:59:27 +00:00
parent 48f950f0b9
commit c36a6a5fbb
2 changed files with 447 additions and 210 deletions

View File

@ -10,12 +10,17 @@ var __assign = (this && this.__assign) || function () {
};
return __assign.apply(this, arguments);
};
var _a, _b;
var services = ["0x180d", "0x181a", "0x1819"];
var acc;
var bar;
var gps;
var hrm;
var mag;
var haveNewAcc = false;
var haveNewBar = false;
var haveNewGps = false;
var haveNewHrm = false;
var haveNewMag = false;
var curMenu = "main";
var mainMenuScroll = 0;
var settings = {
@ -49,7 +54,7 @@ var optionsCommon = {
back: showMainMenu,
};
var accMenu = {
"": __assign({ "title": "Acceleration -" }, optionsCommon),
"": __assign({ "title": "Acceleration" }, optionsCommon),
"Active": { value: "true (fixed)" },
"x": { value: "" },
"y": { value: "" },
@ -78,7 +83,7 @@ var gpsMenu = {
"HDOP": { value: "" },
};
var hrmMenu = {
"": __assign({ "title": "- Heart Rate -" }, optionsCommon),
"": __assign({ "title": "Heart Rate" }, optionsCommon),
"Active": {
value: settings.hrmEnabled,
onchange: function (v) { return updateSetting('hrmEnabled', v); },
@ -164,59 +169,39 @@ var updateMenu = function () {
break;
}
};
var updateBleAdvert = function () {
var _a, _b;
var bleAdvert;
if (!(bleAdvert = Bangle.bleAdvert))
bleAdvert = Bangle.bleAdvert = {};
if (hrm) {
bleAdvert["0x180d"] = undefined;
if (NRF.getSecurityStatus().connected) {
NRF.updateServices((_a = {},
_a["0x180d"] = (_b = {},
_b["0x2a37"] = {
value: [0, hrm.bpm],
notify: true,
},
_b),
_a));
return;
}
}
NRF.setAdvertising(Bangle.bleAdvert);
var encodeHrm = function (hrm) {
return [0, hrm ? hrm.bpm : 0];
};
var encodeHrm = function () { return [0, hrm ? hrm.bpm : 0]; };
var encodeBarServiceData = function (data) {
var t = toByteArray(Math.round(data.temperature * 100), 2, true);
var p = toByteArray(Math.round(data.pressure * 1000), 4, false);
var e = toByteArray(Math.round(data.altitude * 100), 3, true);
return [
0x02, 0x01, 0x06,
0x05, 0x16, 0x6e, 0x2a, t[0], t[1],
0x07, 0x16, 0x6d, 0x2a, p[0], p[1], p[2], p[3],
0x06, 0x16, 0x6c, 0x2a, e[0], e[1], e[2]
];
var encodePressure = function (data) {
return toByteArray(Math.round(data.pressure * 1000), 4, false);
};
var encodeGpsServiceData = function (data) {
var s = toByteArray(Math.round(1000 * data.speed / 36), 2, false);
var encodeElevation = function (data) {
return toByteArray(Math.round(data.altitude * 100), 3, true);
};
var encodeTemp = function (data) {
return toByteArray(Math.round(data.temperature * 100), 2, true);
};
var encodeGps = function (data) {
var speed = toByteArray(Math.round(1000 * data.speed / 36), 2, false);
var lat = toByteArray(Math.round(data.lat * 10000000), 4, true);
var lon = toByteArray(Math.round(data.lon * 10000000), 4, true);
var e = toByteArray(Math.round(data.alt * 100), 3, true);
var h = toByteArray(Math.round(data.course * 100), 2, false);
var elevation = toByteArray(Math.round(data.alt * 100), 3, true);
var heading = toByteArray(Math.round(data.course * 100), 2, false);
return [
0x02, 0x01, 0x06,
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]
0x9d,
0x2,
speed[0], speed[1],
lat[0], lat[1], lat[2], lat[3],
lon[0], lon[1], lon[2], lon[3],
elevation[0], elevation[1], elevation[2],
heading[0], heading[1]
];
};
var encodeMagServiceData = function (data) {
var encodeMag = function (data) {
var x = toByteArray(data.x, 2, true);
var y = toByteArray(data.y, 2, true);
var z = toByteArray(data.z, 2, true);
return [
0x02, 0x01, 0x06,
0x09, 0x16, 0xa1, 0x2a, x[0], x[1], y[0], y[1], z[0], z[1]
];
return [x[0], x[1], y[0], y[1], z[0], z[1]];
};
var toByteArray = function (value, numberOfBytes, isSigned) {
var byteArray = new Array(numberOfBytes);
@ -241,54 +226,149 @@ var enableSensors = function () {
Bangle.setCompassPower(settings.magEnabled, "btadv");
if (!settings.magEnabled)
mag = undefined;
updateBleAdvert();
updateServices();
};
var updateSetting = function (name, value) {
settings[name] = value;
enableSensors();
};
NRF.setServices((_a = {},
_a["0x180d"] = (_b = {},
_b["0x2a37"] = {
value: encodeHrm(),
readable: true,
notify: true,
},
_b),
_a), {
advertise: [
'180d',
]
});
var updateServices = function () {
var _a, _b;
NRF.updateServices((_a = {},
_a["0x180d"] = (_b = {},
_b["0x2a37"] = {
value: encodeHrm(),
notify: true,
},
_b),
_a));
var haveNew = function () {
return haveNewAcc || haveNewBar || haveNewGps || haveNewHrm || haveNewMag;
};
Bangle.on('accel', function (newAcc) { return acc = newAcc; });
Bangle.on('pressure', function (newBar) { return bar = newBar; });
Bangle.on('GPS', function (newGps) { return gps = newGps; });
Bangle.on('HRM', function (newHrm) { return hrm = newHrm; });
Bangle.on('mag', function (newMag) { return mag = newMag; });
var serviceActive = function (serv) {
switch (serv) {
case "0x180d": return !!hrm;
case "0x181a": return !!(bar || mag);
case "0x1819": return !!(gps && gps.lat && gps.lon);
}
};
var serviceToAdvert = function (serv) {
var _a, _b;
switch (serv) {
case "0x180d":
if (hrm) {
return _a = {},
_a["0x2a37"] = {
value: encodeHrm(hrm),
readable: true,
notify: true,
},
_a;
}
return {};
case "0x1819":
if (gps) {
return _b = {},
_b["0x2a67"] = {
value: encodeGps(gps),
readable: true,
notify: true,
},
_b;
}
return {};
case "0x181a": {
var o = {};
if (bar) {
o["0x2a6c"] = {
value: encodeElevation(bar),
readable: true,
notify: true,
};
o["0x2A1F"] = {
value: encodeTemp(bar),
readable: true,
notify: true,
};
o["0x2a6d"] = {
value: encodePressure(bar),
readable: true,
notify: true,
};
}
if (mag) {
o["0x2aa1"] = {
value: encodeMag(mag),
readable: true,
notify: true,
};
}
;
return o;
}
}
};
var getBleAdvert = function (map) {
var advert = {};
for (var _i = 0, services_1 = services; _i < services_1.length; _i++) {
var serv = services_1[_i];
if (serviceActive(serv)) {
advert[serv] = map(serv);
}
}
return advert;
};
var updateBleAdvert = function () {
var bleAdvert;
if (!(bleAdvert = Bangle.bleAdvert)) {
bleAdvert = getBleAdvert(function (_) { return undefined; });
Bangle.bleAdvert = bleAdvert;
}
try {
NRF.setAdvertising(bleAdvert);
}
catch (e) {
console.log("setAdvertising(): " + e);
}
};
var updateServices = function () {
var newAdvert = getBleAdvert(serviceToAdvert);
if (NRF.getSecurityStatus().connected) {
NRF.updateServices(newAdvert);
}
else {
NRF.setServices(newAdvert, {
advertise: Object
.keys(newAdvert)
.map(function (k) { return k.replace("0x", ""); })
});
}
};
Bangle.on('accel', function (newAcc) { acc = newAcc; haveNewAcc = true; });
Bangle.on('pressure', function (newBar) { bar = newBar; haveNewBar = true; });
Bangle.on('GPS', function (newGps) { gps = newGps; haveNewGps = true; });
Bangle.on('HRM', function (newHrm) { hrm = newHrm; haveNewHrm = true; });
Bangle.on('mag', function (newMag) { mag = newMag; haveNewMag = true; });
Bangle.loadWidgets();
Bangle.drawWidgets();
showMainMenu();
enableSensors();
setInterval(updateBleAdvert, 30000);
var menuInterval = setInterval(updateMenu, 1000);
Bangle.on("lock", function (locked) {
changeInterval(menuInterval, locked ? 30000 : 1000);
});
var serviceInterval;
enableSensors();
var iv;
var setIntervals = function (connected) {
if (connected) {
if (iv) {
changeInterval(iv, 1000);
}
else {
iv = setInterval(function () {
if (haveNew())
updateServices();
}, 1000);
}
}
else if (iv) {
clearInterval(iv);
}
};
setIntervals(NRF.getSecurityStatus().connected);
NRF.on("connect", function () {
serviceInterval = setInterval(updateServices, 1000);
setIntervals(true);
});
NRF.on("disconnect", function () {
clearInterval(serviceInterval);
serviceInterval = undefined;
setIntervals(false);
});

View File

@ -1,17 +1,81 @@
// TODO: emit other data beside HRM (via set/updateServices)
const enum Intervals {
BLE_ADVERT = 30 * 1000,
BLE_ADVERT = 60 * 1000,
BLE = 1000,
MENU_WAKE = 1000,
MENU_SLEEP = 30 * 1000,
}
type Hrm = { bpm: number, confidence: number };
// https://github.com/sputnikdev/bluetooth-gatt-parser/blob/master/src/main/resources/gatt/
const enum BleServ {
// org.bluetooth.service.heart_rate
// contains: HRM
HRM = "0x180d",
// org.bluetooth.service.environmental_sensing
// contains: Elevation, Temp(Celsius), Pressure, Mag
EnvSensing = "0x181a",
// org.bluetooth.service.location_and_navigation
// contains: LocationAndSpeed
LocationAndNavigation = "0x1819",
// Acc // none known for this
}
const services = [BleServ.HRM, BleServ.EnvSensing, BleServ.LocationAndNavigation];
const enum BleChar {
// org.bluetooth.characteristic.heart_rate_measurement
// <see encode function>
HRM = "0x2a37",
// org.bluetooth.characteristic.elevation
// s24, meters 0.01
Elevation = "0x2a6c",
// org.bluetooth.characteristic.temperature
// s16 *10^2
Temp = "0x2a6e",
// org.bluetooth.characteristic.temperature_celsius
// s16 *10^2
TempCelsius = "0x2A1F",
// org.bluetooth.characteristic.pressure
// u32 *10
Pressure = "0x2a6d",
// org.bluetooth.characteristic.location_and_speed
// <see encodeGps>
LocationAndSpeed = "0x2a67",
// org.bluetooth.characteristic.magnetic_flux_density_3d
// s16: x, y, z, tesla (10^-7)
MagneticFlux3D = "0x2aa1",
}
type BleCharAdvert = {
value: Array<number>,
readable?: true,
notify?: true,
indicate?: true, // notify + ACK
};
type BleServAdvert = {
[key in BleChar]?: BleCharAdvert;
};
let acc: undefined | AccelData;
let bar: undefined | PressureData;
let gps: undefined | GPSFix;
let hrm: undefined | { bpm: number, confidence: number };
let hrm: undefined | Hrm;
let mag: undefined | CompassData;
let haveNewAcc = false;
let haveNewBar = false;
let haveNewGps = false;
let haveNewHrm = false;
let haveNewMag = false;
type BtAdvMenu = "acc" | "bar" | "gps" | "hrm" | "mag" | "main";
let curMenu: BtAdvMenu = "main";
@ -54,7 +118,7 @@ const optionsCommon = {
};
const accMenu = {
"": { "title" : "Acceleration -", ...optionsCommon },
"": { "title" : "Acceleration", ...optionsCommon },
"Active": { value: "true (fixed)" },
"x": { value: "" },
"y": { value: "" },
@ -86,7 +150,7 @@ const gpsMenu = {
};
const hrmMenu = {
"": { "title" : "- Heart Rate -", ...optionsCommon },
"": { "title" : "Heart Rate", ...optionsCommon },
"Active": {
value: settings.hrmEnabled,
onchange: (v: boolean) => updateSetting('hrmEnabled', v),
@ -174,109 +238,91 @@ const updateMenu = () => {
}
};
const enum BleServ {
HRM = "0x180d",
}
const enum BleChar {
HRM = "0x2a37",
}
const updateBleAdvert = () => {
let bleAdvert: { [key: string]: undefined };
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;
const encodeHrm = (hrm: Hrm) =>
// {
// flags: u8,
// bytes: [u8...]
// }
// if (gps && gps.lat && gps.lon) {
// data.push(encodeGpsServiceData(gps));
// gps = undefined;
// 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
// }
[0, hrm ? hrm.bpm : 0];
if (hrm) {
bleAdvert[BleServ.HRM] = undefined; // Advertise HRM
const encodePressure = (data: PressureData) =>
toByteArray(Math.round(data.pressure * 1000), 4, false);
// hack
if (NRF.getSecurityStatus().connected) {
NRF.updateServices({
[BleServ.HRM]: {
[BleChar.HRM]: {
value: [0, hrm.bpm],
notify: true,
}
}
})
return;
}
const encodeElevation = (data: PressureData) =>
toByteArray(Math.round(data.altitude * 100), 3, true);
// data.push({ 0x2a37: [ 0, hrm.bpm ] });
// hrm = undefined;
}
const encodeTemp = (data: PressureData) =>
toByteArray(Math.round(data.temperature * 100), 2, true);
// if (mag) {
// data.push(encodeMagServiceData(mag));
// mag = undefined;
// }
const encodeGps = (data: GPSFix) => {
// flags: 16 bits
// bit 0: Instantaneous Speed Present
// bit 1: Total Distance Present
// bit 2: Location Present
// bit 3: Elevation Present
// bit 4: Heading Present
// bit 5: Rolling Time Present
// bit 6: UTC Time Present
//
// bit 7-8: position status
// 0 (0b00): no position
// 1 (0b01): position ok
// 2 (0b10): estimated position
// 3 (0b11): last known position
//
// bit 9: speed & distance format
// 0: 2d
// 1: 3d
//
// bit 10-11: elevation source
// 0: Positioning System
// 1: Barometric Air Pressure
// 2: Database Service (or similiar)
// 3: Other
//
// bit 12: Heading Source
// 0: Heading based on movement
// 1: Heading based on magnetic compass
//
// speed: u16 (m/s), 1/100
// distance: u24, 1/10
// lat: s32, 1/10^7
// lon: s32, 1/10^7
// elevation: s24, 1/100
// heading: u16 (deg), 1/100
// rolling time: u8 (s)
// utc time: org.bluetooth.characteristic.date_time
NRF.setAdvertising((Bangle as any).bleAdvert);
};
// {
// 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 speed = 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);
const elevation = toByteArray(Math.round(data.alt * 100), 3, true);
const heading = 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
0x9d, // 0b10011101: speed, location, elevation, heading [...]
0x2, // 0b00000010: position ok, 3d speed/distance(?)
speed[0]!, speed[1]!,
lat[0]!, lat[1]!, lat[2]!, lat[3]!,
lon[0]!, lon[1]!, lon[2]!, lon[3]!,
elevation[0]!, elevation[1]!, elevation[2]!,
heading[0]!, heading[1]!
];
};
const encodeMagServiceData = (data: CompassData) => {
const encodeMag = (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
];
return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ];
};
const toByteArray = (value: number, numberOfBytes: number, isSigned: boolean) => {
@ -309,6 +355,9 @@ const enableSensors = () => {
Bangle.setCompassPower(settings.magEnabled, "btadv");
if (!settings.magEnabled)
mag = undefined;
updateBleAdvert();
updateServices();
};
const updateSetting = (
@ -322,49 +371,136 @@ const updateSetting = (
// ----------------------------
NRF.setServices(
{
[BleServ.HRM]: {
[BleChar.HRM]: {
value: encodeHrm(),
readable: true,
notify: true,
//indicate: true, // notify + ACK
},
},
},
{
advertise: [
'180d',
]
},
);
const haveNew = () =>
haveNewAcc || haveNewBar || haveNewGps || haveNewHrm || haveNewMag;
const updateServices = () => {
NRF.updateServices({
[BleServ.HRM]: {
[BleChar.HRM]: {
value: encodeHrm(),
notify: true,
}
}
});
const serviceActive = (serv: BleServ): boolean => {
switch (serv) {
case BleServ.HRM: return !!hrm;
case BleServ.EnvSensing: return !!(bar || mag);
case BleServ.LocationAndNavigation: return !!(gps && gps.lat && gps.lon);
}
};
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);
const serviceToAdvert = (serv: BleServ): BleServAdvert => {
switch (serv) {
case BleServ.HRM:
if (hrm) {
return {
[BleChar.HRM]: {
value: encodeHrm(hrm),
readable: true,
notify: true,
},
};
}
return {};
case BleServ.LocationAndNavigation:
if (gps) {
return {
[BleChar.LocationAndSpeed]: {
value: encodeGps(gps),
readable: true,
notify: true,
},
};
}
return {};
case BleServ.EnvSensing: {
const o: BleServAdvert = {};
if (bar) {
o[BleChar.Elevation] = {
value: encodeElevation(bar),
readable: true,
notify: true,
};
o[BleChar.TempCelsius] = {
value: encodeTemp(bar),
readable: true,
notify: true,
};
o[BleChar.Pressure] = {
value: encodePressure(bar),
readable: true,
notify: true,
};
}
if (mag) {
o[BleChar.MagneticFlux3D] = {
value: encodeMag(mag),
readable: true,
notify: true,
};
};
return o;
}
}
};
const getBleAdvert = <T>(map: (s: BleServ) => T) => {
const advert: { [key in BleServ]?: T } = {};
for (const serv of services) {
if (serviceActive(serv)) {
advert[serv] = map(serv);
}
}
return advert;
};
// call this when settings changes
const updateBleAdvert = () => {
let bleAdvert: ReturnType<typeof getBleAdvert<undefined>>;
if (!(bleAdvert = (Bangle as any).bleAdvert)) {
bleAdvert = getBleAdvert(_ => undefined);
(Bangle as any).bleAdvert = bleAdvert;
}
try {
NRF.setAdvertising(bleAdvert);
} catch (e) {
console.log("setAdvertising(): " + e);
}
};
// call this when settings changes, or when we have new data to send/serve
const updateServices = () => {
const newAdvert = getBleAdvert(serviceToAdvert);
if (NRF.getSecurityStatus().connected) {
NRF.updateServices(newAdvert);
} else {
NRF.setServices(
newAdvert,
{
advertise: Object
.keys(newAdvert)
.map((k: string) => k.replace("0x", ""))
},
);
}
};
Bangle.on('accel', newAcc => { acc = newAcc; haveNewAcc = true; });
Bangle.on('pressure', newBar => { bar = newBar; haveNewBar = true; });
Bangle.on('GPS', newGps => { gps = newGps; haveNewGps = true; });
Bangle.on('HRM', newHrm => { hrm = newHrm; haveNewHrm = true; });
Bangle.on('mag', newMag => { mag = newMag; haveNewMag = true; });
// show menu first to have it reserve space for widgets (appRect)
Bangle.loadWidgets();
Bangle.drawWidgets();
// show UI
showMainMenu();
enableSensors();
setInterval(updateBleAdvert, Intervals.BLE_ADVERT);
const menuInterval = setInterval(updateMenu, Intervals.MENU_WAKE);
Bangle.on("lock", locked => {
changeInterval(
@ -373,11 +509,32 @@ Bangle.on("lock", locked => {
);
});
let serviceInterval: undefined | number;
// turn things on
enableSensors(); // calls updateBleAdvert
let iv: undefined | number;
const setIntervals = (connected: boolean) => {
if (connected) {
if (iv) {
changeInterval(iv, Intervals.BLE);
} else {
iv = setInterval(
() => {
if (haveNew())
updateServices();
},
Intervals.BLE
);
}
} else if (iv) {
clearInterval(iv);
}
};
setIntervals(NRF.getSecurityStatus().connected);
NRF.on("connect", () => {
serviceInterval = setInterval(updateServices, Intervals.BLE);
setIntervals(true);
});
NRF.on("disconnect", () => {
clearInterval(serviceInterval);
serviceInterval = undefined;
setIntervals(false);
});