2023-06-29 20:16:25 +00:00
|
|
|
{
|
2024-05-04 21:21:43 +00:00
|
|
|
// @ts-expect-error helper
|
2024-06-25 21:04:31 +00:00
|
|
|
|
2023-02-22 22:19:20 +00:00
|
|
|
const __assign = Object.assign;
|
|
|
|
|
2023-03-09 20:35:00 +00:00
|
|
|
const Layout = require("Layout");
|
2023-01-26 23:00:24 +00:00
|
|
|
|
2023-01-27 23:07:47 +00:00
|
|
|
Bangle.loadWidgets();
|
|
|
|
Bangle.drawWidgets();
|
|
|
|
|
2023-01-20 22:28:14 +00:00
|
|
|
const enum Intervals {
|
2023-01-27 23:07:47 +00:00
|
|
|
// BLE_ADVERT = 60 * 1000,
|
2023-01-29 22:31:41 +00:00
|
|
|
BLE = 1000, // info screen
|
|
|
|
BLE_BACKGROUND = 5000, // button screen
|
|
|
|
UI_INFO = 5 * 1000, // info refresh, wake
|
|
|
|
UI_INFO_SLEEP = 15 * 1000, // info refresh, asleep
|
2023-01-20 22:28:14 +00:00
|
|
|
}
|
2023-01-20 21:04:11 +00:00
|
|
|
|
2023-01-22 21:59:27 +00:00
|
|
|
type Hrm = { bpm: number, confidence: number };
|
|
|
|
|
2023-02-11 08:40:06 +00:00
|
|
|
const HRM_MIN_CONFIDENCE = 75;
|
|
|
|
|
2023-01-22 21:59:27 +00:00
|
|
|
// 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",
|
|
|
|
|
2023-07-26 06:15:31 +00:00
|
|
|
// org.microbit.service.accelerometer
|
|
|
|
// contains: Acc
|
2023-07-31 20:28:30 +00:00
|
|
|
Acc = "E95D0753251D470AA062FA1922DFA9A8",
|
2023-01-22 21:59:27 +00:00
|
|
|
}
|
|
|
|
|
2023-07-26 06:15:31 +00:00
|
|
|
const services = [
|
|
|
|
BleServ.HRM,
|
|
|
|
BleServ.EnvSensing,
|
|
|
|
BleServ.LocationAndNavigation,
|
|
|
|
BleServ.Acc,
|
|
|
|
];
|
2023-01-22 21:59:27 +00:00
|
|
|
|
|
|
|
const enum BleChar {
|
|
|
|
// org.bluetooth.characteristic.heart_rate_measurement
|
|
|
|
// <see encode function>
|
|
|
|
HRM = "0x2a37",
|
|
|
|
|
2023-07-26 06:00:31 +00:00
|
|
|
// org.bluetooth.characteristic.body_sensor_location
|
|
|
|
// u8
|
|
|
|
SensorLocation = "0x2a38",
|
|
|
|
|
2023-01-22 21:59:27 +00:00
|
|
|
// 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",
|
2023-07-26 06:15:31 +00:00
|
|
|
|
|
|
|
// org.microbit.characteristic.accelerometer_data
|
|
|
|
// s16 x3, -1024 .. 1024
|
|
|
|
// docs: https://lancaster-university.github.io/microbit-docs/ble/accelerometer-service/
|
2023-07-31 20:35:11 +00:00
|
|
|
Acc = "E95DCA4B251D470AA062FA1922DFA9A8",
|
2023-01-22 21:59:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type BleCharAdvert = {
|
2023-01-26 23:03:05 +00:00
|
|
|
value?: Array<number>,
|
2023-01-22 21:59:27 +00:00
|
|
|
readable?: true,
|
|
|
|
notify?: true,
|
|
|
|
indicate?: true, // notify + ACK
|
2023-01-26 23:03:05 +00:00
|
|
|
maxLen?: number,
|
2023-01-22 21:59:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
type BleServAdvert = {
|
|
|
|
[key in BleChar]?: BleCharAdvert;
|
|
|
|
};
|
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
type LenFunc<T> = {
|
|
|
|
(_: T): Array<number>,
|
|
|
|
maxLen: number,
|
|
|
|
}
|
|
|
|
|
2023-07-26 06:15:31 +00:00
|
|
|
const enum SensorLocations {
|
2023-07-26 06:00:31 +00:00
|
|
|
Other = 0,
|
|
|
|
Chest = 1,
|
|
|
|
Wrist = 2,
|
|
|
|
Finger = 3,
|
|
|
|
Hand = 4,
|
|
|
|
EarLobe = 5,
|
|
|
|
Foot = 6,
|
|
|
|
}
|
|
|
|
|
2023-01-20 21:04:11 +00:00
|
|
|
let acc: undefined | AccelData;
|
|
|
|
let bar: undefined | PressureData;
|
|
|
|
let gps: undefined | GPSFix;
|
2023-01-22 21:59:27 +00:00
|
|
|
let hrm: undefined | Hrm;
|
2023-02-11 08:40:06 +00:00
|
|
|
let hrmAny: undefined | Hrm;
|
2023-01-20 21:04:11 +00:00
|
|
|
let mag: undefined | CompassData;
|
2023-01-27 22:08:28 +00:00
|
|
|
let btnsShown = false;
|
|
|
|
let prevBtnsShown: boolean | undefined = undefined;
|
2023-02-19 21:17:05 +00:00
|
|
|
let hrmAnyClear: undefined | number;
|
2023-01-20 21:04:11 +00:00
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
type BtAdvType<IncludeAcc = false> = "bar" | "gps" | "hrm" | "mag" | (IncludeAcc extends true ? "acc" : never);
|
|
|
|
type BtAdvMap<T, IncludeAcc = false> = { [key in BtAdvType<IncludeAcc>]: T };
|
2023-01-20 21:04:11 +00:00
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
const settings: BtAdvMap<boolean> = {
|
|
|
|
bar: false,
|
|
|
|
gps: false,
|
|
|
|
hrm: false,
|
|
|
|
mag: false,
|
|
|
|
};
|
2023-01-20 21:04:11 +00:00
|
|
|
|
2023-08-08 07:14:48 +00:00
|
|
|
const idToName: BtAdvMap<string> = {
|
2023-01-27 22:08:28 +00:00
|
|
|
bar: "Barometer",
|
|
|
|
gps: "GPS",
|
|
|
|
hrm: "HRM",
|
|
|
|
mag: "Magnetometer",
|
|
|
|
};
|
2023-01-26 23:03:21 +00:00
|
|
|
|
2023-02-11 08:44:24 +00:00
|
|
|
// 15 characters per line
|
2023-01-27 23:07:47 +00:00
|
|
|
const infoFont: FontNameWithScaleFactor = "6x8:2";
|
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
const colour = {
|
|
|
|
on: "#0f0",
|
|
|
|
off: "#fff",
|
|
|
|
} as const;
|
2023-01-26 23:03:21 +00:00
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
const makeToggle = (id: BtAdvType) => () => {
|
|
|
|
settings[id] = !settings[id];
|
2023-01-20 21:04:11 +00:00
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
const entry = btnLayout[id]!;
|
|
|
|
const col = settings[id] ? colour.on : colour.off;
|
2023-01-20 21:04:11 +00:00
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
entry.btnBorder = entry.col = col;
|
2023-01-20 21:04:11 +00:00
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
btnLayout.update();
|
|
|
|
btnLayout.render();
|
2023-01-20 21:04:11 +00:00
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
//require('Storage').writeJSON(SETTINGS_FILENAME, settings);
|
|
|
|
enableSensors();
|
2023-01-20 22:28:14 +00:00
|
|
|
};
|
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
const btnStyle: {
|
|
|
|
font: FontNameWithScaleFactor,
|
|
|
|
fillx?: 1,
|
|
|
|
filly?: 1,
|
|
|
|
col: ColorResolvable,
|
|
|
|
bgCol: ColorResolvable,
|
|
|
|
btnBorder: ColorResolvable,
|
|
|
|
} = {
|
|
|
|
font: "Vector:14",
|
|
|
|
fillx: 1,
|
|
|
|
filly: 1,
|
|
|
|
col: g.theme.fg,
|
|
|
|
bgCol: g.theme.bg,
|
|
|
|
btnBorder: "#fff",
|
2023-01-20 21:04:11 +00:00
|
|
|
};
|
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
const btnLayout = new Layout(
|
|
|
|
{
|
|
|
|
type: "v",
|
|
|
|
c: [
|
|
|
|
{
|
|
|
|
type: "h",
|
|
|
|
c: [
|
|
|
|
{
|
|
|
|
type: "btn",
|
|
|
|
label: idToName.bar,
|
|
|
|
id: "bar",
|
|
|
|
cb: makeToggle('bar'),
|
|
|
|
...btnStyle,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: "btn",
|
|
|
|
label: idToName.gps,
|
|
|
|
id: "gps",
|
|
|
|
cb: makeToggle('gps'),
|
|
|
|
...btnStyle,
|
|
|
|
},
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: "h",
|
|
|
|
c: [
|
|
|
|
// hrm, mag
|
|
|
|
{
|
|
|
|
type: "btn",
|
|
|
|
label: idToName.hrm,
|
|
|
|
id: "hrm",
|
|
|
|
cb: makeToggle('hrm'),
|
|
|
|
...btnStyle,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: "btn",
|
|
|
|
label: idToName.mag,
|
|
|
|
id: "mag",
|
|
|
|
cb: makeToggle('mag'),
|
|
|
|
...btnStyle,
|
|
|
|
},
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: "h",
|
|
|
|
c: [
|
|
|
|
{
|
|
|
|
type: "btn",
|
|
|
|
label: "Back",
|
|
|
|
cb: () => {
|
|
|
|
setBtnsShown(false);
|
|
|
|
},
|
|
|
|
...btnStyle,
|
|
|
|
},
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]
|
2023-01-20 21:04:11 +00:00
|
|
|
},
|
2023-01-27 22:08:28 +00:00
|
|
|
{
|
|
|
|
lazy: true,
|
|
|
|
back: () => {
|
|
|
|
setBtnsShown(false);
|
|
|
|
},
|
2023-01-20 21:04:11 +00:00
|
|
|
},
|
2023-01-27 22:08:28 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
const setBtnsShown = (b: boolean) => {
|
|
|
|
btnsShown = b;
|
2023-01-29 22:31:41 +00:00
|
|
|
|
|
|
|
hook(!btnsShown);
|
|
|
|
setIntervals();
|
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
redraw();
|
2023-01-20 21:04:11 +00:00
|
|
|
};
|
|
|
|
|
2023-01-29 22:31:41 +00:00
|
|
|
const drawInfo = (force?: true) => {
|
2023-01-27 23:07:47 +00:00
|
|
|
let { y, x, w } = Bangle.appRect;
|
|
|
|
const mid = x + w / 2
|
|
|
|
let drawn = false;
|
2023-01-27 22:08:28 +00:00
|
|
|
|
2023-01-29 22:31:41 +00:00
|
|
|
if (!force && !bar && !gps && !hrm && !mag)
|
|
|
|
return;
|
|
|
|
|
2023-01-27 23:07:47 +00:00
|
|
|
g.reset()
|
|
|
|
.clearRect(Bangle.appRect)
|
|
|
|
.setFont(infoFont)
|
|
|
|
.setFontAlign(0, -1);
|
2023-01-27 22:08:28 +00:00
|
|
|
|
2023-01-27 23:07:47 +00:00
|
|
|
if (bar) {
|
|
|
|
g.drawString(`${bar.altitude.toFixed(1)}m`, mid, y);
|
|
|
|
y += g.getFontHeight();
|
2023-01-20 21:04:11 +00:00
|
|
|
|
2023-02-19 21:04:06 +00:00
|
|
|
g.drawString(`${bar.pressure.toFixed(1)} hPa`, mid, y);
|
2023-01-27 23:07:47 +00:00
|
|
|
y += g.getFontHeight();
|
2023-01-20 21:04:11 +00:00
|
|
|
|
2023-01-27 23:07:47 +00:00
|
|
|
g.drawString(`${bar.temperature.toFixed(1)}C`, mid, y);
|
|
|
|
y += g.getFontHeight();
|
|
|
|
|
|
|
|
drawn = true;
|
2023-01-27 22:08:28 +00:00
|
|
|
}
|
2023-01-26 23:03:21 +00:00
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
if (gps) {
|
2023-01-27 23:07:47 +00:00
|
|
|
g.drawString(
|
|
|
|
`${gps.lat.toFixed(4)} lat, ${gps.lon.toFixed(4)} lon`,
|
|
|
|
mid,
|
|
|
|
y,
|
|
|
|
);
|
|
|
|
y += g.getFontHeight();
|
|
|
|
|
|
|
|
g.drawString(
|
|
|
|
`${gps.alt}m (${gps.satellites} sat)`,
|
|
|
|
mid,
|
|
|
|
y,
|
|
|
|
);
|
|
|
|
y += g.getFontHeight();
|
|
|
|
|
|
|
|
drawn = true;
|
2023-01-27 22:08:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (hrm) {
|
2023-01-27 23:07:47 +00:00
|
|
|
g.drawString(`${hrm.bpm} BPM (${hrm.confidence}%)`, mid, y);
|
|
|
|
y += g.getFontHeight();
|
|
|
|
|
|
|
|
drawn = true;
|
2023-02-11 08:40:06 +00:00
|
|
|
} else if (hrmAny) {
|
|
|
|
g.drawString(`~${hrmAny.bpm} BPM (${hrmAny.confidence}%)`, mid, y);
|
|
|
|
y += g.getFontHeight();
|
|
|
|
|
|
|
|
drawn = true;
|
|
|
|
|
2023-02-19 21:17:05 +00:00
|
|
|
if (!settings.hrm && !hrmAnyClear) {
|
|
|
|
// hrm is erased, but hrmAny will remain until cleared (or reset)
|
|
|
|
// if it runs via health check, we reset it here
|
|
|
|
hrmAnyClear = setTimeout(() => {
|
|
|
|
hrmAny = undefined;
|
|
|
|
hrmAnyClear = undefined;
|
|
|
|
}, 10000);
|
|
|
|
}
|
2023-01-27 22:08:28 +00:00
|
|
|
}
|
2023-01-26 23:03:21 +00:00
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
if (mag) {
|
2023-01-27 23:07:47 +00:00
|
|
|
g.drawString(
|
|
|
|
`${mag.x} ${mag.y} ${mag.z}`,
|
|
|
|
mid,
|
|
|
|
y
|
|
|
|
);
|
|
|
|
y += g.getFontHeight();
|
|
|
|
|
|
|
|
g.drawString(
|
|
|
|
`heading: ${mag.heading.toFixed(1)}`,
|
|
|
|
mid,
|
|
|
|
y
|
|
|
|
);
|
|
|
|
y += g.getFontHeight();
|
|
|
|
|
|
|
|
drawn = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!drawn) {
|
2023-01-29 22:31:41 +00:00
|
|
|
if (!force || Object.values(settings).every((x: boolean) => !x)) {
|
|
|
|
g.drawString(`swipe to enable`, mid, y);
|
|
|
|
} else {
|
2023-02-11 08:44:24 +00:00
|
|
|
g.drawString(`events pending`, mid, y);
|
2023-01-29 22:31:41 +00:00
|
|
|
}
|
2023-01-27 23:07:47 +00:00
|
|
|
y += g.getFontHeight();
|
2023-01-27 22:08:28 +00:00
|
|
|
}
|
2023-01-26 23:03:21 +00:00
|
|
|
};
|
|
|
|
|
2023-01-29 22:31:41 +00:00
|
|
|
const onTap = (/* _: { ... } */) => {
|
2023-01-27 23:07:47 +00:00
|
|
|
setBtnsShown(true);
|
|
|
|
};
|
2023-01-27 22:08:28 +00:00
|
|
|
|
2023-01-27 23:07:47 +00:00
|
|
|
const redraw = () => {
|
2023-01-27 22:08:28 +00:00
|
|
|
if (btnsShown) {
|
2023-01-27 23:07:47 +00:00
|
|
|
if (!prevBtnsShown) {
|
|
|
|
prevBtnsShown = btnsShown;
|
2023-01-27 22:08:28 +00:00
|
|
|
|
2023-01-27 23:07:47 +00:00
|
|
|
Bangle.removeListener("swipe", onTap);
|
2023-01-27 22:08:28 +00:00
|
|
|
|
2023-01-27 23:07:47 +00:00
|
|
|
btnLayout.setUI();
|
|
|
|
btnLayout.forgetLazyState();
|
|
|
|
g.clearRect(Bangle.appRect); // in case btnLayout isn't full screen
|
|
|
|
}
|
|
|
|
|
|
|
|
btnLayout.render();
|
|
|
|
} else {
|
|
|
|
if (prevBtnsShown) {
|
|
|
|
prevBtnsShown = btnsShown;
|
|
|
|
|
|
|
|
Bangle.setUI(); // remove all existing input handlers
|
|
|
|
Bangle.on("swipe", onTap);
|
2023-01-29 22:31:41 +00:00
|
|
|
|
|
|
|
drawInfo(true);
|
|
|
|
} else {
|
|
|
|
drawInfo();
|
2023-01-27 23:07:47 +00:00
|
|
|
}
|
2023-01-20 21:04:11 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
const encodeHrm: LenFunc<Hrm> = (hrm: Hrm) =>
|
2023-01-22 21:59:27 +00:00
|
|
|
// {
|
|
|
|
// flags: u8,
|
|
|
|
// bytes: [u8...]
|
2023-01-20 21:04:11 +00:00
|
|
|
// }
|
2023-01-22 21:59:27 +00:00
|
|
|
// 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
|
2023-01-20 21:04:11 +00:00
|
|
|
// }
|
2023-01-26 23:03:05 +00:00
|
|
|
[0, hrm.bpm];
|
|
|
|
encodeHrm.maxLen = 2;
|
2023-01-22 21:59:27 +00:00
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
const encodePressure: LenFunc<PressureData> = (data: PressureData) =>
|
2023-02-19 21:05:17 +00:00
|
|
|
toByteArray(Math.round(data.pressure * 10), 4, false);
|
2023-01-26 23:03:05 +00:00
|
|
|
encodePressure.maxLen = 4;
|
2023-01-22 21:59:27 +00:00
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
const encodeElevation: LenFunc<PressureData> = (data: PressureData) =>
|
2023-01-22 21:59:27 +00:00
|
|
|
toByteArray(Math.round(data.altitude * 100), 3, true);
|
2023-01-26 23:03:05 +00:00
|
|
|
encodeElevation.maxLen = 3;
|
2023-01-22 21:59:27 +00:00
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
const encodeTemp: LenFunc<PressureData> = (data: PressureData) =>
|
2023-02-19 21:05:17 +00:00
|
|
|
toByteArray(Math.round(data.temperature * 10), 2, true);
|
2023-01-26 23:03:05 +00:00
|
|
|
encodeTemp.maxLen = 2;
|
2023-01-22 21:59:27 +00:00
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
const encodeGps: LenFunc<GPSFix> = (data: GPSFix) => {
|
2023-01-22 21:59:27 +00:00
|
|
|
// 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
|
|
|
|
|
|
|
|
const speed = toByteArray(Math.round(1000 * data.speed / 36), 2, false);
|
2023-01-20 21:04:11 +00:00
|
|
|
const lat = toByteArray(Math.round(data.lat * 10000000), 4, true);
|
|
|
|
const lon = toByteArray(Math.round(data.lon * 10000000), 4, true);
|
2023-01-22 21:59:27 +00:00
|
|
|
const elevation = toByteArray(Math.round(data.alt * 100), 3, true);
|
|
|
|
const heading = toByteArray(Math.round(data.course * 100), 2, false);
|
2023-01-20 21:04:11 +00:00
|
|
|
|
|
|
|
return [
|
2023-02-19 21:06:25 +00:00
|
|
|
0b10011101, // speed, location, elevation, heading [...]
|
|
|
|
0b00000010, // position ok, 3d speed/distance
|
2023-01-22 21:59:27 +00:00
|
|
|
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]!
|
2023-01-20 21:04:11 +00:00
|
|
|
];
|
|
|
|
};
|
2023-01-26 23:03:05 +00:00
|
|
|
encodeGps.maxLen = 17;
|
2023-01-20 21:04:11 +00:00
|
|
|
|
2023-02-19 21:06:54 +00:00
|
|
|
const encodeGpsHeadingOnly: LenFunc<CompassData> = (data: CompassData) => {
|
|
|
|
// see encodeGps()
|
|
|
|
const heading = toByteArray(Math.round(data.heading * 100), 2, false);
|
|
|
|
|
|
|
|
return [
|
|
|
|
0b00010000, // heading present
|
|
|
|
0b00010000, // heading source: mag
|
|
|
|
heading[0]!, heading[1]!
|
|
|
|
];
|
|
|
|
};
|
|
|
|
encodeGpsHeadingOnly.maxLen = 17;
|
|
|
|
|
2023-07-31 20:40:06 +00:00
|
|
|
const encodeMag: LenFunc<CompassData> = (data: CompassData) => {
|
2023-01-20 21:04:11 +00:00
|
|
|
const x = toByteArray(data.x, 2, true);
|
|
|
|
const y = toByteArray(data.y, 2, true);
|
|
|
|
const z = toByteArray(data.z, 2, true);
|
|
|
|
|
2023-01-22 21:59:27 +00:00
|
|
|
return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ];
|
2023-01-20 21:04:11 +00:00
|
|
|
};
|
2023-07-31 20:40:06 +00:00
|
|
|
encodeMag.maxLen = 6;
|
|
|
|
|
|
|
|
const encodeAcc: LenFunc<AccelData> = (data: AccelData) => {
|
|
|
|
const x = toByteArray(data.x * 1000, 2, true);
|
|
|
|
const y = toByteArray(data.y * 1000, 2, true);
|
|
|
|
const z = toByteArray(data.z * 1000, 2, true);
|
2023-07-26 06:15:31 +00:00
|
|
|
|
2023-07-31 20:40:06 +00:00
|
|
|
return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ];
|
|
|
|
};
|
|
|
|
encodeAcc.maxLen = 6;
|
2023-01-20 21:04:11 +00:00
|
|
|
|
|
|
|
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 = () => {
|
2023-01-27 22:08:28 +00:00
|
|
|
Bangle.setBarometerPower(settings.bar, "btadv");
|
|
|
|
if (!settings.bar)
|
2023-01-21 00:14:02 +00:00
|
|
|
bar = undefined;
|
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
Bangle.setGPSPower(settings.gps, "btadv");
|
|
|
|
if (!settings.gps)
|
2023-01-21 00:14:02 +00:00
|
|
|
gps = undefined;
|
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
Bangle.setHRMPower(settings.hrm, "btadv");
|
|
|
|
if (!settings.hrm)
|
2023-02-11 08:44:39 +00:00
|
|
|
hrm = hrmAny = undefined;
|
2023-01-21 00:14:02 +00:00
|
|
|
|
2023-01-27 22:08:28 +00:00
|
|
|
Bangle.setCompassPower(settings.mag, "btadv");
|
|
|
|
if (!settings.mag)
|
2023-01-21 00:14:02 +00:00
|
|
|
mag = undefined;
|
2023-01-20 21:04:11 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// ----------------------------
|
|
|
|
|
2023-01-27 23:07:34 +00:00
|
|
|
const haveServiceData = (serv: BleServ): boolean => {
|
2023-01-22 21:59:27 +00:00
|
|
|
switch (serv) {
|
|
|
|
case BleServ.HRM: return !!hrm;
|
|
|
|
case BleServ.EnvSensing: return !!(bar || mag);
|
2023-02-19 21:06:54 +00:00
|
|
|
case BleServ.LocationAndNavigation: return !!(gps && gps.lat && gps.lon || mag);
|
2023-07-26 06:15:31 +00:00
|
|
|
case BleServ.Acc: return !!acc;
|
2023-01-22 21:59:27 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => {
|
2023-01-22 21:59:27 +00:00
|
|
|
switch (serv) {
|
|
|
|
case BleServ.HRM:
|
2023-01-26 23:03:05 +00:00
|
|
|
if (hrm || initial) {
|
|
|
|
const o: BleCharAdvert = {
|
|
|
|
maxLen: encodeHrm.maxLen,
|
|
|
|
readable: true,
|
|
|
|
notify: true,
|
2023-01-22 21:59:27 +00:00
|
|
|
};
|
2023-07-26 06:00:31 +00:00
|
|
|
const os: BleCharAdvert = {
|
|
|
|
maxLen: 1,
|
|
|
|
readable: true,
|
|
|
|
notify: true,
|
|
|
|
};
|
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
if (hrm) {
|
|
|
|
o.value = encodeHrm(hrm);
|
2023-07-26 06:15:31 +00:00
|
|
|
os.value = [SensorLocations.Wrist];
|
2023-01-27 23:07:34 +00:00
|
|
|
hrm = undefined;
|
2023-01-26 23:03:05 +00:00
|
|
|
}
|
|
|
|
|
2023-07-26 06:00:31 +00:00
|
|
|
return {
|
|
|
|
[BleChar.HRM]: o,
|
|
|
|
[BleChar.SensorLocation]: os,
|
|
|
|
};
|
2023-01-20 21:04:11 +00:00
|
|
|
}
|
2023-01-22 21:59:27 +00:00
|
|
|
return {};
|
|
|
|
|
|
|
|
case BleServ.LocationAndNavigation:
|
2023-01-26 23:03:05 +00:00
|
|
|
if (gps || initial) {
|
|
|
|
const o: BleCharAdvert = {
|
|
|
|
maxLen: encodeGps.maxLen,
|
|
|
|
readable: true,
|
|
|
|
notify: true,
|
2023-01-22 21:59:27 +00:00
|
|
|
};
|
2023-01-26 23:03:05 +00:00
|
|
|
if (gps) {
|
|
|
|
o.value = encodeGps(gps);
|
2023-01-27 23:07:34 +00:00
|
|
|
gps = undefined;
|
2023-01-26 23:03:05 +00:00
|
|
|
}
|
|
|
|
|
2023-02-19 21:06:54 +00:00
|
|
|
return { [BleChar.LocationAndSpeed]: o };
|
|
|
|
} else if (mag) {
|
|
|
|
const o: BleCharAdvert = {
|
|
|
|
maxLen: encodeGpsHeadingOnly.maxLen,
|
|
|
|
readable: true,
|
|
|
|
notify: true,
|
|
|
|
value: encodeGpsHeadingOnly(mag),
|
|
|
|
};
|
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
return { [BleChar.LocationAndSpeed]: o };
|
2023-01-22 21:59:27 +00:00
|
|
|
}
|
|
|
|
return {};
|
|
|
|
|
|
|
|
case BleServ.EnvSensing: {
|
|
|
|
const o: BleServAdvert = {};
|
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
if (bar || initial) {
|
2023-01-22 21:59:27 +00:00
|
|
|
o[BleChar.Elevation] = {
|
2023-01-26 23:03:05 +00:00
|
|
|
maxLen: encodeElevation.maxLen,
|
2023-01-22 21:59:27 +00:00
|
|
|
readable: true,
|
|
|
|
notify: true,
|
|
|
|
};
|
|
|
|
o[BleChar.TempCelsius] = {
|
2023-01-26 23:03:05 +00:00
|
|
|
maxLen: encodeTemp.maxLen,
|
2023-01-22 21:59:27 +00:00
|
|
|
readable: true,
|
|
|
|
notify: true,
|
|
|
|
};
|
|
|
|
o[BleChar.Pressure] = {
|
2023-01-26 23:03:05 +00:00
|
|
|
maxLen: encodePressure.maxLen,
|
2023-01-22 21:59:27 +00:00
|
|
|
readable: true,
|
|
|
|
notify: true,
|
|
|
|
};
|
2023-01-26 23:03:05 +00:00
|
|
|
|
|
|
|
if (bar) {
|
|
|
|
o[BleChar.Elevation]!.value = encodeElevation(bar);
|
|
|
|
o[BleChar.TempCelsius]!.value = encodeTemp(bar);
|
|
|
|
o[BleChar.Pressure]!.value = encodePressure(bar);
|
2023-01-27 23:07:34 +00:00
|
|
|
bar = undefined;
|
2023-01-26 23:03:05 +00:00
|
|
|
}
|
2023-01-22 21:59:27 +00:00
|
|
|
}
|
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
if (mag || initial) {
|
|
|
|
o[BleChar.MagneticFlux3D] = {
|
|
|
|
maxLen: encodeMag.maxLen,
|
|
|
|
readable: true,
|
|
|
|
notify: true,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (mag) {
|
|
|
|
o[BleChar.MagneticFlux3D]!.value = encodeMag(mag);
|
|
|
|
}
|
|
|
|
}
|
2023-01-22 21:59:27 +00:00
|
|
|
|
|
|
|
return o;
|
2023-01-20 21:04:11 +00:00
|
|
|
}
|
2023-07-26 06:15:31 +00:00
|
|
|
|
|
|
|
case BleServ.Acc: {
|
|
|
|
const o: BleServAdvert = {};
|
|
|
|
|
|
|
|
if (acc || initial) {
|
|
|
|
o[BleChar.Acc] = {
|
|
|
|
maxLen: encodeAcc.maxLen,
|
|
|
|
readable: true,
|
|
|
|
notify: true,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (acc) {
|
|
|
|
o[BleChar.Acc]!.value = encodeAcc(acc);
|
|
|
|
acc = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return o;
|
|
|
|
}
|
2023-01-22 21:59:27 +00:00
|
|
|
}
|
2023-01-20 21:04:11 +00:00
|
|
|
};
|
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
const getBleAdvert = <T>(map: (s: BleServ) => T, all = false) => {
|
2023-01-22 21:59:27 +00:00
|
|
|
const advert: { [key in BleServ]?: T } = {};
|
|
|
|
|
|
|
|
for (const serv of services) {
|
2023-01-27 23:07:34 +00:00
|
|
|
if (all || haveServiceData(serv)) {
|
2023-01-22 21:59:27 +00:00
|
|
|
advert[serv] = map(serv);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-19 21:06:54 +00:00
|
|
|
// clear mag only after both EnvSensing and LocationAndNavigation have run
|
|
|
|
mag = undefined;
|
|
|
|
|
2023-01-22 21:59:27 +00:00
|
|
|
return advert;
|
|
|
|
};
|
|
|
|
|
2023-01-29 22:31:41 +00:00
|
|
|
// done via advertise in setServices()
|
|
|
|
//const updateBleAdvert = () => {
|
2024-05-24 16:18:35 +00:00
|
|
|
// require("ble_advert").set(...)
|
|
|
|
//
|
2023-01-29 22:31:41 +00:00
|
|
|
// 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("couldn't setAdvertising():", e);
|
|
|
|
// }
|
|
|
|
//};
|
2023-01-22 21:59:27 +00:00
|
|
|
|
|
|
|
const updateServices = () => {
|
|
|
|
const newAdvert = getBleAdvert(serviceToAdvert);
|
|
|
|
|
2023-01-26 23:03:05 +00:00
|
|
|
NRF.updateServices(newAdvert);
|
2023-01-22 21:59:27 +00:00
|
|
|
};
|
|
|
|
|
2023-02-11 08:40:06 +00:00
|
|
|
const onAccel = (newAcc: NonNull<typeof acc>) => acc = newAcc;
|
|
|
|
const onPressure = (newBar: NonNull<typeof bar>) => bar = newBar;
|
|
|
|
const onGPS = (newGps: NonNull<typeof gps>) => gps = newGps;
|
|
|
|
const onHRM = (newHrm: NonNull<typeof hrm>) => {
|
|
|
|
if (newHrm.confidence >= HRM_MIN_CONFIDENCE)
|
|
|
|
hrm = newHrm;
|
|
|
|
hrmAny = newHrm;
|
|
|
|
};
|
|
|
|
const onMag = (newMag: NonNull<typeof mag>) => mag = newMag;
|
2023-01-29 22:31:41 +00:00
|
|
|
|
|
|
|
const hook = (enable: boolean) => {
|
|
|
|
// need to disable for perf reasons, when buttons are shown
|
|
|
|
if (enable) {
|
|
|
|
Bangle.on("accel", onAccel);
|
|
|
|
Bangle.on("pressure", onPressure);
|
|
|
|
Bangle.on("GPS", onGPS);
|
|
|
|
Bangle.on("HRM", onHRM);
|
|
|
|
Bangle.on("mag", onMag);
|
|
|
|
} else {
|
|
|
|
Bangle.removeListener("accel", onAccel);
|
|
|
|
Bangle.removeListener("pressure", onPressure);
|
|
|
|
Bangle.removeListener("GPS", onGPS);
|
|
|
|
Bangle.removeListener("HRM", onHRM);
|
|
|
|
Bangle.removeListener("mag", onMag);
|
|
|
|
}
|
|
|
|
}
|
2023-01-20 21:04:11 +00:00
|
|
|
|
2023-01-29 22:31:41 +00:00
|
|
|
// --- intervals ---
|
2023-01-27 22:08:28 +00:00
|
|
|
|
2023-01-29 22:31:41 +00:00
|
|
|
const setIntervals = (
|
|
|
|
locked: boolean = Bangle.isLocked(),
|
|
|
|
connected: boolean = NRF.getSecurityStatus().connected,
|
|
|
|
) => {
|
2023-01-20 22:28:14 +00:00
|
|
|
changeInterval(
|
2023-01-27 22:08:28 +00:00
|
|
|
redrawInterval,
|
2023-01-29 22:31:41 +00:00
|
|
|
locked ? Intervals.UI_INFO_SLEEP : Intervals.UI_INFO,
|
2023-01-20 22:28:14 +00:00
|
|
|
);
|
2023-01-29 22:31:41 +00:00
|
|
|
|
|
|
|
if (connected) {
|
|
|
|
const interval = btnsShown ? Intervals.BLE_BACKGROUND : Intervals.BLE;
|
|
|
|
|
|
|
|
if (bleInterval) {
|
|
|
|
changeInterval(bleInterval, interval);
|
|
|
|
} else {
|
|
|
|
bleInterval = setInterval(updateServices, interval);
|
|
|
|
}
|
|
|
|
} else if (bleInterval) {
|
|
|
|
clearInterval(bleInterval);
|
|
|
|
bleInterval = undefined;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const redrawInterval = setInterval(redraw, /*replaced*/1000);
|
|
|
|
Bangle.on("lock", locked => setIntervals(locked));
|
|
|
|
|
2023-06-13 22:21:26 +00:00
|
|
|
let bleInterval: undefined | IntervalId;
|
2023-01-29 22:31:41 +00:00
|
|
|
NRF.on("connect", () => setIntervals(undefined, true));
|
|
|
|
NRF.on("disconnect", () => setIntervals(undefined, false));
|
|
|
|
|
|
|
|
setIntervals();
|
2023-01-20 22:28:14 +00:00
|
|
|
|
2023-01-22 21:59:27 +00:00
|
|
|
// turn things on
|
2023-01-29 22:31:41 +00:00
|
|
|
setBtnsShown(true);
|
2023-01-26 23:03:05 +00:00
|
|
|
enableSensors();
|
|
|
|
|
|
|
|
// set services/advert once at startup:
|
|
|
|
{
|
|
|
|
// must have fixed services from the start:
|
|
|
|
const ad = getBleAdvert(serv => serviceToAdvert(serv, true), /*all*/true);
|
|
|
|
|
|
|
|
NRF.setServices(
|
|
|
|
ad,
|
|
|
|
{
|
2023-01-29 22:31:41 +00:00
|
|
|
uart: false,
|
2023-01-26 23:03:05 +00:00
|
|
|
},
|
|
|
|
);
|
2023-07-31 20:29:45 +00:00
|
|
|
|
|
|
|
for(const id in ad){
|
|
|
|
const serv = ad[id as BleServ];
|
|
|
|
let value;
|
|
|
|
|
|
|
|
// pick the first characteristic to advertise
|
|
|
|
for(const ch in serv){
|
|
|
|
value = serv[ch as BleChar]!.value;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2024-06-26 20:42:16 +00:00
|
|
|
require("ble_advert").set(id, value || []);
|
2023-07-31 20:29:45 +00:00
|
|
|
}
|
2023-01-26 23:03:05 +00:00
|
|
|
}
|
2023-06-29 20:16:25 +00:00
|
|
|
}
|