From c36a6a5fbb070cbc3d5d69eaf074f17345500db4 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 22 Jan 2023 21:59:27 +0000 Subject: [PATCH] btadv: handle remaining sensors --- apps/btadv/app.js | 240 +++++++++++++++++--------- apps/btadv/app.ts | 417 +++++++++++++++++++++++++++++++--------------- 2 files changed, 447 insertions(+), 210 deletions(-) diff --git a/apps/btadv/app.js b/apps/btadv/app.js index 432fd2391..43fd4d062 100644 --- a/apps/btadv/app.js +++ b/apps/btadv/app.js @@ -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); }); diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts index c0547c225..08212cc16 100644 --- a/apps/btadv/app.ts +++ b/apps/btadv/app.ts @@ -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 + // + 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 + // + LocationAndSpeed = "0x2a67", + + // org.bluetooth.characteristic.magnetic_flux_density_3d + // s16: x, y, z, tesla (10^-7) + MagneticFlux3D = "0x2aa1", +} + +type BleCharAdvert = { + value: Array, + 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 = (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>; + + 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); });