82
src/components/ble/HeartRateService.cpp
Normal file
82
src/components/ble/HeartRateService.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "HeartRateService.h"
|
||||
#include "components/heartrate/HeartRateController.h"
|
||||
#include "systemtask/SystemTask.h"
|
||||
|
||||
using namespace Pinetime::Controllers;
|
||||
|
||||
constexpr ble_uuid16_t HeartRateService::heartRateServiceUuid;
|
||||
constexpr ble_uuid16_t HeartRateService::heartRateMeasurementUuid;
|
||||
|
||||
namespace {
|
||||
int HeartRateServiceServiceCallback(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) {
|
||||
auto* heartRateService = static_cast<HeartRateService*>(arg);
|
||||
return heartRateService->OnHeartRateRequested(conn_handle, attr_handle, ctxt);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Refactoring - remove dependency to SystemTask
|
||||
HeartRateService::HeartRateService(Pinetime::System::SystemTask &system, Controllers::HeartRateController& heartRateController) :
|
||||
system{system},
|
||||
heartRateController{heartRateController},
|
||||
characteristicDefinition{
|
||||
{
|
||||
.uuid = (ble_uuid_t *) &heartRateMeasurementUuid,
|
||||
.access_cb = HeartRateServiceServiceCallback,
|
||||
.arg = this,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY,
|
||||
.val_handle = &heartRateMeasurementHandle
|
||||
},
|
||||
{
|
||||
0
|
||||
}
|
||||
},
|
||||
serviceDefinition{
|
||||
{
|
||||
/* Device Information Service */
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = (ble_uuid_t *) &heartRateServiceUuid,
|
||||
.characteristics = characteristicDefinition
|
||||
},
|
||||
{
|
||||
0
|
||||
},
|
||||
}{
|
||||
// TODO refactor to prevent this loop dependency (service depends on controller and controller depends on service)
|
||||
heartRateController.SetService(this);
|
||||
}
|
||||
|
||||
void HeartRateService::Init() {
|
||||
int res = 0;
|
||||
res = ble_gatts_count_cfg(serviceDefinition);
|
||||
ASSERT(res == 0);
|
||||
|
||||
res = ble_gatts_add_svcs(serviceDefinition);
|
||||
ASSERT(res == 0);
|
||||
}
|
||||
|
||||
int HeartRateService::OnHeartRateRequested(uint16_t connectionHandle, uint16_t attributeHandle,
|
||||
ble_gatt_access_ctxt *context) {
|
||||
if(attributeHandle == heartRateMeasurementHandle) {
|
||||
NRF_LOG_INFO("BATTERY : handle = %d", heartRateMeasurementHandle);
|
||||
static uint8_t batteryValue = heartRateController.HeartRate();
|
||||
|
||||
uint8_t buffer[2] = {0, heartRateController.HeartRate()}; // [0] = flags, [1] = hr value
|
||||
|
||||
int res = os_mbuf_append(context->om, buffer, 2);
|
||||
return (res == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void HeartRateService::OnNewHeartRateValue(uint8_t heartRateValue) {
|
||||
uint8_t buffer[2] = {0, heartRateController.HeartRate()}; // [0] = flags, [1] = hr value
|
||||
auto *om = ble_hs_mbuf_from_flat(buffer, 2);
|
||||
|
||||
uint16_t connectionHandle = system.nimble().connHandle();
|
||||
|
||||
if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
ble_gattc_notify_custom(connectionHandle, heartRateMeasurementHandle, om);
|
||||
}
|
44
src/components/ble/HeartRateService.h
Normal file
44
src/components/ble/HeartRateService.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
#define min // workaround: nimble's min/max macros conflict with libstdc++
|
||||
#define max
|
||||
#include <host/ble_gap.h>
|
||||
#undef max
|
||||
#undef min
|
||||
|
||||
namespace Pinetime {
|
||||
namespace System {
|
||||
class SystemTask;
|
||||
}
|
||||
namespace Controllers {
|
||||
class HeartRateController;
|
||||
class HeartRateService {
|
||||
public:
|
||||
HeartRateService(Pinetime::System::SystemTask &system, Controllers::HeartRateController& heartRateController);
|
||||
void Init();
|
||||
int OnHeartRateRequested(uint16_t connectionHandle, uint16_t attributeHandle, ble_gatt_access_ctxt *context);
|
||||
void OnNewHeartRateValue(uint8_t hearRateValue);
|
||||
|
||||
private:
|
||||
Pinetime::System::SystemTask &system;
|
||||
Controllers::HeartRateController& heartRateController;
|
||||
static constexpr uint16_t heartRateServiceId {0x180D};
|
||||
static constexpr uint16_t heartRateMeasurementId {0x2A37};
|
||||
|
||||
static constexpr ble_uuid16_t heartRateServiceUuid {
|
||||
.u {.type = BLE_UUID_TYPE_16},
|
||||
.value = heartRateServiceId
|
||||
};
|
||||
|
||||
static constexpr ble_uuid16_t heartRateMeasurementUuid {
|
||||
.u {.type = BLE_UUID_TYPE_16},
|
||||
.value = heartRateMeasurementId
|
||||
};
|
||||
|
||||
struct ble_gatt_chr_def characteristicDefinition[3];
|
||||
struct ble_gatt_svc_def serviceDefinition[2];
|
||||
|
||||
uint16_t heartRateMeasurementHandle;
|
||||
|
||||
};
|
||||
}
|
||||
}
|
@@ -22,7 +22,8 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask,
|
||||
DateTime& dateTimeController,
|
||||
Pinetime::Controllers::NotificationManager& notificationManager,
|
||||
Controllers::Battery& batteryController,
|
||||
Pinetime::Drivers::SpiNorFlash& spiNorFlash) :
|
||||
Pinetime::Drivers::SpiNorFlash& spiNorFlash,
|
||||
Controllers::HeartRateController& heartRateController) :
|
||||
systemTask{systemTask},
|
||||
bleController{bleController},
|
||||
dateTimeController{dateTimeController},
|
||||
@@ -36,7 +37,8 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask,
|
||||
musicService{systemTask},
|
||||
batteryInformationService{batteryController},
|
||||
immediateAlertService{systemTask, notificationManager},
|
||||
serviceDiscovery({¤tTimeClient, &alertNotificationClient}) {
|
||||
serviceDiscovery({¤tTimeClient, &alertNotificationClient}),
|
||||
heartRateService{systemTask, heartRateController} {
|
||||
}
|
||||
|
||||
int GAPEventCallback(struct ble_gap_event *event, void *arg) {
|
||||
@@ -58,6 +60,7 @@ void NimbleController::Init() {
|
||||
dfuService.Init();
|
||||
batteryInformationService.Init();
|
||||
immediateAlertService.Init();
|
||||
heartRateService.Init();
|
||||
int res;
|
||||
res = ble_hs_util_ensure_addr(0);
|
||||
ASSERT(res == 0);
|
||||
|
@@ -17,6 +17,7 @@
|
||||
#include "ImmediateAlertService.h"
|
||||
#include "MusicService.h"
|
||||
#include "ServiceDiscovery.h"
|
||||
#include "HeartRateService.h"
|
||||
|
||||
namespace Pinetime {
|
||||
namespace Drivers {
|
||||
@@ -37,7 +38,8 @@ namespace Pinetime {
|
||||
public:
|
||||
NimbleController(Pinetime::System::SystemTask& systemTask, Pinetime::Controllers::Ble& bleController,
|
||||
DateTime& dateTimeController, Pinetime::Controllers::NotificationManager& notificationManager,
|
||||
Controllers::Battery& batteryController, Pinetime::Drivers::SpiNorFlash& spiNorFlash);
|
||||
Controllers::Battery& batteryController, Pinetime::Drivers::SpiNorFlash& spiNorFlash,
|
||||
Controllers::HeartRateController& heartRateController);
|
||||
void Init();
|
||||
void StartAdvertising();
|
||||
int OnGAPEvent(ble_gap_event *event);
|
||||
@@ -74,6 +76,7 @@ namespace Pinetime {
|
||||
MusicService musicService;
|
||||
BatteryInformationService batteryInformationService;
|
||||
ImmediateAlertService immediateAlertService;
|
||||
HeartRateService heartRateService;
|
||||
|
||||
uint8_t addrType; // 1 = Random, 0 = PUBLIC
|
||||
uint16_t connectionHandle = 0;
|
||||
|
27
src/components/heartrate/Biquad.cpp
Normal file
27
src/components/heartrate/Biquad.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
Original work Copyright (C) 2020 Daniel Thompson
|
||||
C++ port Copyright (C) 2021 Jean-François Milants
|
||||
*/
|
||||
|
||||
#include "Biquad.h"
|
||||
|
||||
using namespace Pinetime::Controllers;
|
||||
|
||||
/** Original implementation from wasp-os : https://github.com/daniel-thompson/wasp-os/blob/master/wasp/ppg.py */
|
||||
Biquad::Biquad(float b0, float b1, float b2, float a1, float a2) : b0{b0}, b1{b1}, b2{b2}, a1{a1}, a2{a2} {
|
||||
|
||||
}
|
||||
|
||||
float Biquad::Step(float x) {
|
||||
auto v1 = this->v1;
|
||||
auto v2 = this->v2;
|
||||
|
||||
auto v = x - (a1 * v1) - (a2 * v2);
|
||||
auto y = (b0 * v) + (b1 * v1) + (b2 * v2);
|
||||
|
||||
this->v2 = v1;
|
||||
this->v1 = v;
|
||||
|
||||
return y;
|
||||
}
|
22
src/components/heartrate/Biquad.h
Normal file
22
src/components/heartrate/Biquad.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
namespace Pinetime {
|
||||
namespace Controllers {
|
||||
/// Direct Form II Biquad Filter
|
||||
class Biquad {
|
||||
public:
|
||||
Biquad(float b0, float b1, float b2, float a1, float a2);
|
||||
float Step(float x);
|
||||
|
||||
private:
|
||||
float b0;
|
||||
float b1;
|
||||
float b2;
|
||||
float a1;
|
||||
float a2;
|
||||
|
||||
float v1 = 0.0f;
|
||||
float v2 = 0.0f;
|
||||
};
|
||||
}
|
||||
}
|
41
src/components/heartrate/HeartRateController.cpp
Normal file
41
src/components/heartrate/HeartRateController.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#include "HeartRateController.h"
|
||||
#include <heartratetask/HeartRateTask.h>
|
||||
#include <systemtask/SystemTask.h>
|
||||
|
||||
using namespace Pinetime::Controllers;
|
||||
|
||||
HeartRateController::HeartRateController(Pinetime::System::SystemTask &systemTask) : systemTask{systemTask} {
|
||||
|
||||
}
|
||||
|
||||
|
||||
void HeartRateController::Update(HeartRateController::States newState, uint8_t heartRate) {
|
||||
this->state = newState;
|
||||
if(this->heartRate != heartRate) {
|
||||
this->heartRate = heartRate;
|
||||
service->OnNewHeartRateValue(heartRate);
|
||||
}
|
||||
}
|
||||
|
||||
void HeartRateController::Start() {
|
||||
if(task != nullptr) {
|
||||
state = States::NotEnoughData;
|
||||
task->PushMessage(Pinetime::Applications::HeartRateTask::Messages::StartMeasurement);
|
||||
}
|
||||
}
|
||||
|
||||
void HeartRateController::Stop() {
|
||||
if(task != nullptr) {
|
||||
state = States::Stopped;
|
||||
task->PushMessage(Pinetime::Applications::HeartRateTask::Messages::StopMeasurement);
|
||||
}
|
||||
}
|
||||
|
||||
void HeartRateController::SetHeartRateTask(Pinetime::Applications::HeartRateTask *task) {
|
||||
this->task = task;
|
||||
}
|
||||
|
||||
void HeartRateController::SetService(Pinetime::Controllers::HeartRateService *service) {
|
||||
this->service = service;
|
||||
}
|
||||
|
38
src/components/heartrate/HeartRateController.h
Normal file
38
src/components/heartrate/HeartRateController.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <components/ble/HeartRateService.h>
|
||||
|
||||
namespace Pinetime {
|
||||
namespace Applications {
|
||||
class HeartRateTask;
|
||||
}
|
||||
namespace System {
|
||||
class SystemTask;
|
||||
}
|
||||
namespace Controllers {
|
||||
class HeartRateController {
|
||||
public:
|
||||
enum class States { Stopped, NotEnoughData, NoTouch, Running};
|
||||
|
||||
explicit HeartRateController(System::SystemTask& systemTask);
|
||||
|
||||
void Start();
|
||||
void Stop();
|
||||
void Update(States newState, uint8_t heartRate);
|
||||
|
||||
void SetHeartRateTask(Applications::HeartRateTask* task);
|
||||
States State() const { return state; }
|
||||
uint8_t HeartRate() const { return heartRate; }
|
||||
|
||||
void SetService(Pinetime::Controllers::HeartRateService *service);
|
||||
|
||||
private:
|
||||
System::SystemTask& systemTask;
|
||||
Applications::HeartRateTask* task = nullptr;
|
||||
States state = States::Stopped;
|
||||
uint8_t heartRate = 0;
|
||||
Pinetime::Controllers::HeartRateService* service = nullptr;
|
||||
};
|
||||
}
|
||||
}
|
106
src/components/heartrate/Ppg.cpp
Normal file
106
src/components/heartrate/Ppg.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
Original work Copyright (C) 2020 Daniel Thompson
|
||||
C++ port Copyright (C) 2021 Jean-François Milants
|
||||
*/
|
||||
|
||||
#include <vector>
|
||||
#include <nrf_log.h>
|
||||
#include "Ppg.h"
|
||||
using namespace Pinetime::Controllers;
|
||||
|
||||
/** Original implementation from wasp-os : https://github.com/daniel-thompson/wasp-os/blob/master/wasp/ppg.py */
|
||||
namespace {
|
||||
int Compare(int* d1, int* d2, size_t count) {
|
||||
int e = 0;
|
||||
for(int i = 0; i < count; i++) {
|
||||
auto d = d1[i] - d2[i];
|
||||
e += d * d;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
int CompareShift(int* d, int shift, size_t count) {
|
||||
return Compare(d +shift, d, count - shift);
|
||||
}
|
||||
|
||||
int Trough(int* d, size_t size, float mn, float mx) {
|
||||
auto z2 = CompareShift(d, mn-2, size);
|
||||
auto z1 = CompareShift(d, mn-1, size);
|
||||
for(int i = mn; i < mx + 1; i++) {
|
||||
auto z = CompareShift(d, i, size);
|
||||
if(z2 > z1 && z1 < z)
|
||||
return i;
|
||||
z2 = z1;
|
||||
z1 = z;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
Ppg::Ppg(float spl) : offset{spl},
|
||||
hpf{0.87033078, -1.74066156, 0.87033078,-1.72377617, 0.75754694},
|
||||
agc{20, 0.971, 2},
|
||||
lpf{0.11595249, 0.23190498, 0.11595249,-0.72168143, 0.18549138} {
|
||||
|
||||
}
|
||||
|
||||
int Ppg::Preprocess(float spl) {
|
||||
spl -= offset;
|
||||
spl = hpf.Step(spl);
|
||||
spl = agc.Step(spl);
|
||||
spl = lpf.Step(spl);
|
||||
|
||||
auto spl_int = static_cast<int>(spl);
|
||||
|
||||
if(dataIndex < 200)
|
||||
data[dataIndex++] = spl_int;
|
||||
return spl_int;
|
||||
}
|
||||
|
||||
float Ppg::HeartRate() {
|
||||
if(dataIndex < 200)
|
||||
return 0;
|
||||
|
||||
NRF_LOG_INFO("PREPROCESS, offset = %d", offset);
|
||||
auto hr = ProcessHeartRate();
|
||||
dataIndex = 0;
|
||||
return hr;
|
||||
}
|
||||
|
||||
int cccount = 0;
|
||||
float Ppg::ProcessHeartRate() {
|
||||
|
||||
if(cccount > 2)
|
||||
asm("nop");
|
||||
cccount ++;
|
||||
auto t0 = Trough(data.data(), dataIndex, 7, 48);
|
||||
if(t0 < 0)
|
||||
return 0;
|
||||
|
||||
float t1 = t0 * 2;
|
||||
t1 = Trough(data.data(), dataIndex, t1-5, t1+5);
|
||||
if(t1 < 0)
|
||||
return 0;
|
||||
|
||||
float t2 = static_cast<int>(t1 * 3) / 2;
|
||||
t2 = Trough(data.data(), dataIndex, t2 - 5, t2 + 5);
|
||||
if(t2 < 0)
|
||||
return 0;
|
||||
|
||||
float t3 = static_cast<int>(t2 * 4) / 3;
|
||||
t3 = Trough(data.data(), dataIndex, t3 - 4, t3 + 4);
|
||||
if(t3 < 0)
|
||||
return static_cast<int>(60 * 24 * 3) / static_cast<int>(t2);
|
||||
|
||||
return static_cast<int>(60 * 24 * 4) / static_cast<int>(t3);
|
||||
}
|
||||
|
||||
void Ppg::SetOffset(uint16_t offset) {
|
||||
this->offset = offset;
|
||||
dataIndex = 0;
|
||||
}
|
||||
|
||||
void Ppg::Reset() {
|
||||
dataIndex = 0;
|
||||
}
|
31
src/components/heartrate/Ppg.h
Normal file
31
src/components/heartrate/Ppg.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include "Biquad.h"
|
||||
#include "Ptagc.h"
|
||||
|
||||
namespace Pinetime {
|
||||
namespace Controllers {
|
||||
class Ppg {
|
||||
public:
|
||||
explicit Ppg(float spl);
|
||||
|
||||
int Preprocess(float spl);
|
||||
float HeartRate();
|
||||
|
||||
void SetOffset(uint16_t i);
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
std::array<int, 200> data;
|
||||
size_t dataIndex = 0;
|
||||
float offset;
|
||||
Biquad hpf;
|
||||
Ptagc agc;
|
||||
Biquad lpf;
|
||||
|
||||
|
||||
float ProcessHeartRate();
|
||||
};
|
||||
}
|
||||
}
|
28
src/components/heartrate/Ptagc.cpp
Normal file
28
src/components/heartrate/Ptagc.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
Original work Copyright (C) 2020 Daniel Thompson
|
||||
C++ port Copyright (C) 2021 Jean-François Milants
|
||||
*/
|
||||
|
||||
#include <cmath>
|
||||
#include "Ptagc.h"
|
||||
|
||||
using namespace Pinetime::Controllers;
|
||||
|
||||
/** Original implementation from wasp-os : https://github.com/daniel-thompson/wasp-os/blob/master/wasp/ppg.py */
|
||||
Ptagc::Ptagc(float start, float decay, float threshold) : peak{start}, decay{decay}, boost{1.0f/decay}, threshold{threshold} {
|
||||
|
||||
}
|
||||
|
||||
float Ptagc::Step(float spl) {
|
||||
if(std::abs(spl) > peak)
|
||||
peak *= boost;
|
||||
else
|
||||
peak *= decay;
|
||||
|
||||
if((spl > (peak * threshold)) || (spl < (peak * -threshold)))
|
||||
return 0.0f;
|
||||
|
||||
spl = 100.0f * spl / (2.0f * peak);
|
||||
return spl;
|
||||
}
|
18
src/components/heartrate/Ptagc.h
Normal file
18
src/components/heartrate/Ptagc.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
namespace Pinetime {
|
||||
namespace Controllers {
|
||||
class Ptagc {
|
||||
public:
|
||||
Ptagc(float start, float decay, float threshold);
|
||||
float Step(float spl);
|
||||
|
||||
private:
|
||||
float peak;
|
||||
float decay;
|
||||
float boost;
|
||||
float threshold;
|
||||
|
||||
};
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user