diff options
| author | Michele Bini <michele.bini@gmail.com> | 2022-03-30 20:50:42 (GMT) |
|---|---|---|
| committer | Michele Bini <michele.bini@gmail.com> | 2022-03-31 00:12:29 (GMT) |
| commit | 12e938959eb1e2146fdec3fee7b9a304aa6889d2 (patch) | |
| tree | f57f54615ad00327e8dc33c958ec67773c49f655 | |
| parent | ff68f91be9cce2fb27d3b3aea9ab26fa224ea0eb (diff) | |
with heart
36 files changed, 1407 insertions, 3 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c1bb32f..7ad520a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -397,6 +397,7 @@ list(APPEND SOURCE_FILES displayapp/screens/ApplicationList.cpp displayapp/screens/Notifications.cpp displayapp/screens/FlashLight.cpp + displayapp/screens/HeartRate.cpp displayapp/screens/List.cpp displayapp/screens/Error.cpp displayapp/screens/Styles.cpp @@ -443,6 +444,7 @@ list(APPEND SOURCE_FILES components/ble/BatteryInformationService.cpp components/ble/ImmediateAlertService.cpp components/ble/ServiceDiscovery.cpp + components/ble/HeartRateService.cpp components/firmwarevalidator/FirmwareValidator.cpp components/motor/MotorController.cpp components/settings/Settings.cpp @@ -460,6 +462,12 @@ list(APPEND SOURCE_FILES systemtask/SystemMonitor.cpp drivers/TwiMaster.cpp + heartratetask/HeartRateTask.cpp + components/heartrate/Ppg.cpp + components/heartrate/Biquad.cpp + components/heartrate/Ptagc.cpp + components/heartrate/HeartRateController.cpp + buttonhandler/ButtonHandler.cpp touchhandler/TouchHandler.cpp ) @@ -497,6 +505,7 @@ list(APPEND RECOVERY_SOURCE_FILES components/ble/BatteryInformationService.cpp components/ble/ImmediateAlertService.cpp components/ble/ServiceDiscovery.cpp + components/ble/HeartRateService.cpp components/firmwarevalidator/FirmwareValidator.cpp components/settings/Settings.cpp drivers/Cst816s.cpp @@ -509,6 +518,11 @@ list(APPEND RECOVERY_SOURCE_FILES drivers/TwiMaster.cpp components/gfx/Gfx.cpp components/rle/RleDecoder.cpp + components/heartrate/HeartRateController.cpp + heartratetask/HeartRateTask.cpp + components/heartrate/Ppg.cpp + components/heartrate/Biquad.cpp + components/heartrate/Ptagc.cpp components/motor/MotorController.cpp buttonhandler/ButtonHandler.cpp touchhandler/TouchHandler.cpp @@ -559,6 +573,7 @@ set(INCLUDE_FILES displayapp/screens/ApplicationList.h displayapp/Apps.h displayapp/screens/Notifications.h + displayapp/screens/HeartRate.h displayapp/Colors.h drivers/St7789.h drivers/SpiNorFlash.h @@ -591,6 +606,7 @@ set(INCLUDE_FILES components/ble/ImmediateAlertService.h components/ble/ServiceDiscovery.h components/ble/BleClient.h + components/ble/HeartRateService.h components/settings/Settings.h drivers/Cst816s.h FreeRTOS/portmacro.h @@ -609,6 +625,11 @@ set(INCLUDE_FILES systemtask/SystemMonitor.h displayapp/screens/Symbols.h drivers/TwiMaster.h + heartratetask/HeartRateTask.h + components/heartrate/Ppg.h + components/heartrate/Biquad.h + components/heartrate/Ptagc.h + components/heartrate/HeartRateController.h components/motor/MotorController.h buttonhandler/ButtonHandler.h touchhandler/TouchHandler.h diff --git a/src/components/ble/HeartRateService.cpp b/src/components/ble/HeartRateService.cpp new file mode 100644 index 0000000..4824a6b --- /dev/null +++ b/src/components/ble/HeartRateService.cpp @@ -0,0 +1,82 @@ +#include "components/ble/HeartRateService.h" +#include "components/heartrate/HeartRateController.h" +#include "systemtask/SystemTask.h" +#include <nrf_log.h> + +using namespace Pinetime::Controllers; + +constexpr ble_uuid16_t HeartRateService::heartRateServiceUuid; +constexpr ble_uuid16_t HeartRateService::heartRateMeasurementUuid; + +namespace { + int HeartRateServiceCallback(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 = &heartRateMeasurementUuid.u, + .access_cb = HeartRateServiceCallback, + .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 = &heartRateServiceUuid.u, + .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("HEARTRATE : handle = %d", heartRateMeasurementHandle); + 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) { + if(!heartRateMeasurementNotificationEnable) return; + + 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); +} + +void HeartRateService::SubscribeNotification(uint16_t connectionHandle, uint16_t attributeHandle) { + if(attributeHandle == heartRateMeasurementHandle) + heartRateMeasurementNotificationEnable = true; +} + +void HeartRateService::UnsubscribeNotification(uint16_t connectionHandle, uint16_t attributeHandle) { + if(attributeHandle == heartRateMeasurementHandle) + heartRateMeasurementNotificationEnable = false; +}
\ No newline at end of file diff --git a/src/components/ble/HeartRateService.h b/src/components/ble/HeartRateService.h new file mode 100644 index 0000000..4e4a5a4 --- /dev/null +++ b/src/components/ble/HeartRateService.h @@ -0,0 +1,42 @@ +#pragma once +#define min // workaround: nimble's min/max macros conflict with libstdc++ +#define max +#include <host/ble_gap.h> +#include <atomic> +#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); + + void SubscribeNotification(uint16_t connectionHandle, uint16_t attributeHandle); + void UnsubscribeNotification(uint16_t connectionHandle, uint16_t attributeHandle); + + 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[2]; + struct ble_gatt_svc_def serviceDefinition[2]; + + uint16_t heartRateMeasurementHandle; + std::atomic_bool heartRateMeasurementNotificationEnable {false}; + }; + } +} diff --git a/src/components/ble/NimbleController.cpp b/src/components/ble/NimbleController.cpp index f63d98c..bc712f4 100644 --- a/src/components/ble/NimbleController.cpp +++ b/src/components/ble/NimbleController.cpp @@ -27,7 +27,8 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask, DateTime& dateTimeController, NotificationManager& notificationManager, Battery& batteryController, - Pinetime::Drivers::SpiNorFlash& spiNorFlash + Pinetime::Drivers::SpiNorFlash& spiNorFlash, + HeartRateController& heartRateController ) : systemTask {systemTask}, bleController {bleController}, @@ -42,6 +43,7 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask, currentTimeService {dateTimeController}, batteryInformationService {batteryController}, immediateAlertService {systemTask, notificationManager}, + heartRateService {systemTask, heartRateController}, serviceDiscovery({¤tTimeClient, &alertNotificationClient}) { } @@ -84,6 +86,7 @@ void NimbleController::Init() { dfuService.Init(); batteryInformationService.Init(); immediateAlertService.Init(); + heartRateService.Init(); int rc; rc = ble_hs_util_ensure_addr(0); @@ -240,6 +243,14 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { event->subscribe.prev_notify, event->subscribe.cur_notify, event->subscribe.prev_indicate); + + if (event->subscribe.reason == BLE_GAP_SUBSCRIBE_REASON_TERM) { + heartRateService.UnsubscribeNotification(event->subscribe.conn_handle, event->subscribe.attr_handle); + } else if (event->subscribe.prev_notify == 0 && event->subscribe.cur_notify == 1) { + heartRateService.SubscribeNotification(event->subscribe.conn_handle, event->subscribe.attr_handle); + } else if (event->subscribe.prev_notify == 1 && event->subscribe.cur_notify == 0) { + heartRateService.UnsubscribeNotification(event->subscribe.conn_handle, event->subscribe.attr_handle); + } break; case BLE_GAP_EVENT_MTU: diff --git a/src/components/ble/NimbleController.h b/src/components/ble/NimbleController.h index c9d6420..0facca6 100644 --- a/src/components/ble/NimbleController.h +++ b/src/components/ble/NimbleController.h @@ -14,6 +14,7 @@ #include "components/ble/CurrentTimeService.h" #include "components/ble/DeviceInformationService.h" #include "components/ble/DfuService.h" +#include "components/ble/HeartRateService.h" #include "components/ble/ImmediateAlertService.h" #include "components/ble/ServiceDiscovery.h" @@ -39,7 +40,8 @@ namespace Pinetime { DateTime& dateTimeController, NotificationManager& notificationManager, Battery& batteryController, - Pinetime::Drivers::SpiNorFlash& spiNorFlash); + Pinetime::Drivers::SpiNorFlash& spiNorFlash, + HeartRateController& heartRateController); void Init(); void StartAdvertising(); int OnGAPEvent(ble_gap_event* event); @@ -72,6 +74,7 @@ namespace Pinetime { CurrentTimeService currentTimeService; BatteryInformationService batteryInformationService; ImmediateAlertService immediateAlertService; + HeartRateService heartRateService; ServiceDiscovery serviceDiscovery; uint8_t addrType; diff --git a/src/components/heartrate/Biquad.cpp b/src/components/heartrate/Biquad.cpp new file mode 100644 index 0000000..b7edd40 --- /dev/null +++ b/src/components/heartrate/Biquad.cpp @@ -0,0 +1,26 @@ +/* + 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 "components/heartrate/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; +} diff --git a/src/components/heartrate/Biquad.h b/src/components/heartrate/Biquad.h new file mode 100644 index 0000000..7c8ca58 --- /dev/null +++ b/src/components/heartrate/Biquad.h @@ -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; + }; + } +} diff --git a/src/components/heartrate/HeartRateController.cpp b/src/components/heartrate/HeartRateController.cpp new file mode 100644 index 0000000..e0d6927 --- /dev/null +++ b/src/components/heartrate/HeartRateController.cpp @@ -0,0 +1,35 @@ +#include "components/heartrate/HeartRateController.h" +#include <heartratetask/HeartRateTask.h> +#include <systemtask/SystemTask.h> + +using namespace Pinetime::Controllers; + +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; +} diff --git a/src/components/heartrate/HeartRateController.h b/src/components/heartrate/HeartRateController.h new file mode 100644 index 0000000..a63f1a7 --- /dev/null +++ b/src/components/heartrate/HeartRateController.h @@ -0,0 +1,40 @@ +#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 }; + + HeartRateController() = default; + 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: + Applications::HeartRateTask* task = nullptr; + States state = States::Stopped; + uint8_t heartRate = 0; + Pinetime::Controllers::HeartRateService* service = nullptr; + }; + } +}
\ No newline at end of file diff --git a/src/components/heartrate/Ppg.cpp b/src/components/heartrate/Ppg.cpp new file mode 100644 index 0000000..a5d8369 --- /dev/null +++ b/src/components/heartrate/Ppg.cpp @@ -0,0 +1,99 @@ +/* + 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 "components/heartrate/Ppg.h" +#include <vector> +#include <nrf_log.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(int8_t* d1, int8_t* d2, size_t count) { + int e = 0; + for (size_t i = 0; i < count; i++) { + auto d = d1[i] - d2[i]; + e += d * d; + } + return e; + } + + int CompareShift(int8_t* d, int shift, size_t count) { + return Compare(d + shift, d, count - shift); + } + + int Trough(int8_t* d, size_t size, uint8_t mn, uint8_t 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() + : 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} { +} + +int8_t Ppg::Preprocess(float spl) { + spl -= offset; + spl = hpf.Step(spl); + spl = agc.Step(spl); + spl = lpf.Step(spl); + + auto spl_int = static_cast<int8_t>(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; +} +float Ppg::ProcessHeartRate() { + 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; +} diff --git a/src/components/heartrate/Ppg.h b/src/components/heartrate/Ppg.h new file mode 100644 index 0000000..7000c87 --- /dev/null +++ b/src/components/heartrate/Ppg.h @@ -0,0 +1,31 @@ +#pragma once + +#include <array> +#include <cstddef> +#include <cstdint> +#include "components/heartrate/Biquad.h" +#include "components/heartrate/Ptagc.h" + +namespace Pinetime { + namespace Controllers { + class Ppg { + public: + Ppg(); + int8_t Preprocess(float spl); + float HeartRate(); + + void SetOffset(uint16_t i); + void Reset(); + + private: + std::array<int8_t, 200> data; + size_t dataIndex = 0; + float offset; + Biquad hpf; + Ptagc agc; + Biquad lpf; + + float ProcessHeartRate(); + }; + } +} diff --git a/src/components/heartrate/Ptagc.cpp b/src/components/heartrate/Ptagc.cpp new file mode 100644 index 0000000..1c60bc2 --- /dev/null +++ b/src/components/heartrate/Ptagc.cpp @@ -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 "components/heartrate/Ptagc.h" +#include <cmath> + +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; +} diff --git a/src/components/heartrate/Ptagc.h b/src/components/heartrate/Ptagc.h new file mode 100644 index 0000000..3476636 --- /dev/null +++ b/src/components/heartrate/Ptagc.h @@ -0,0 +1,17 @@ +#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; + }; + } +} diff --git a/src/displayapp/Apps.h b/src/displayapp/Apps.h index 3cbaa9e..ff3f4eb 100644 --- a/src/displayapp/Apps.h +++ b/src/displayapp/Apps.h @@ -11,6 +11,7 @@ namespace Pinetime { NotificationsPreview, Notifications, FlashLight, + HeartRate, QuickSettings, Settings, SettingTimeFormat, diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 9219472..47b0f06 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -1,5 +1,6 @@ #include "displayapp/DisplayApp.h" #include <libraries/log/nrf_log.h> +#include "displayapp/screens/HeartRate.h" #include "components/battery/BatteryController.h" #include "components/ble/BleController.h" #include "components/datetime/DateTimeController.h" @@ -70,6 +71,7 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, Controllers::DateTime& dateTimeController, Drivers::WatchdogView& watchdog, Pinetime::Controllers::NotificationManager& notificationManager, + Pinetime::Controllers::HeartRateController& heartRateController, Controllers::Settings& settingsController, Pinetime::Controllers::MotorController& motorController, Pinetime::Controllers::MotionController& motionController, @@ -83,6 +85,7 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, dateTimeController {dateTimeController}, watchdog {watchdog}, notificationManager {notificationManager}, + heartRateController {heartRateController}, settingsController {settingsController}, motorController {motorController}, motionController {motionController}, @@ -298,6 +301,7 @@ void DisplayApp::LoadApp(Apps app, DisplayApp::FullRefreshDirections direction) bleController, notificationManager, settingsController, + heartRateController, motionController, *systemTask); break; @@ -355,6 +359,9 @@ void DisplayApp::LoadApp(Apps app, DisplayApp::FullRefreshDirections direction) currentScreen = std::make_unique<Screens::FlashLight>(this, *systemTask, brightnessController); ReturnApp(Apps::QuickSettings, FullRefreshDirections::Down, TouchEvents::SwipeDown); break; + case Apps::HeartRate: + currentScreen = std::make_unique<Screens::HeartRate>(this, heartRateController, *systemTask); + break; } currentApp = app; } diff --git a/src/displayapp/DisplayApp.h b/src/displayapp/DisplayApp.h index b79952d..60db552 100644 --- a/src/displayapp/DisplayApp.h +++ b/src/displayapp/DisplayApp.h @@ -31,6 +31,7 @@ namespace Pinetime { class Ble; class DateTime; class NotificationManager; + class HeartRateController; class MotionController; class TouchHandler; } @@ -52,6 +53,7 @@ namespace Pinetime { Controllers::DateTime& dateTimeController, Drivers::WatchdogView& watchdog, Pinetime::Controllers::NotificationManager& notificationManager, + Pinetime::Controllers::HeartRateController& heartRateController, Controllers::Settings& settingsController, Pinetime::Controllers::MotorController& motorController, Pinetime::Controllers::MotionController& motionController, @@ -76,6 +78,7 @@ namespace Pinetime { Pinetime::Drivers::WatchdogView& watchdog; Pinetime::System::SystemTask* systemTask = nullptr; Pinetime::Controllers::NotificationManager& notificationManager; + Pinetime::Controllers::HeartRateController& heartRateController; Pinetime::Controllers::Settings& settingsController; Pinetime::Controllers::MotorController& motorController; Pinetime::Controllers::MotionController& motionController; diff --git a/src/displayapp/DisplayAppRecovery.cpp b/src/displayapp/DisplayAppRecovery.cpp index a33308f..f577679 100644 --- a/src/displayapp/DisplayAppRecovery.cpp +++ b/src/displayapp/DisplayAppRecovery.cpp @@ -17,6 +17,7 @@ DisplayApp::DisplayApp(Drivers::St7789& lcd, Controllers::DateTime& dateTimeController, Drivers::WatchdogView& watchdog, Pinetime::Controllers::NotificationManager& notificationManager, + Pinetime::Controllers::HeartRateController& heartRateController, Controllers::Settings& settingsController, Pinetime::Controllers::MotorController& motorController, Pinetime::Controllers::MotionController& motionController, diff --git a/src/displayapp/DisplayAppRecovery.h b/src/displayapp/DisplayAppRecovery.h index 8f25cbe..4e383e2 100644 --- a/src/displayapp/DisplayAppRecovery.h +++ b/src/displayapp/DisplayAppRecovery.h @@ -28,6 +28,7 @@ namespace Pinetime { class Ble; class DateTime; class NotificationManager; + class HeartRateController; class MotionController; class TouchHandler; class MotorController; @@ -50,6 +51,7 @@ namespace Pinetime { Controllers::DateTime& dateTimeController, Drivers::WatchdogView& watchdog, Pinetime::Controllers::NotificationManager& notificationManager, + Pinetime::Controllers::HeartRateController& heartRateController, Controllers::Settings& settingsController, Pinetime::Controllers::MotorController& motorController, Pinetime::Controllers::MotionController& motionController, diff --git a/src/displayapp/screens/ApplicationList.cpp b/src/displayapp/screens/ApplicationList.cpp index b7c8ebd..c8d608a 100644 --- a/src/displayapp/screens/ApplicationList.cpp +++ b/src/displayapp/screens/ApplicationList.cpp @@ -37,6 +37,7 @@ bool ApplicationList::OnTouchEvent(Pinetime::Applications::TouchEvents event) { std::unique_ptr<Screen> ApplicationList::CreateScreen1() { std::array<Screens::Tile::Applications, 6> applications {{ {Symbols::clock}, + {Symbols::heartBeat, Apps::HeartRate}, }}; return std::make_unique<Screens::Tile>(0, 1, app, settingsController, batteryController, dateTimeController, applications); diff --git a/src/displayapp/screens/Clock.cpp b/src/displayapp/screens/Clock.cpp index ab4cafd..1893856 100644 --- a/src/displayapp/screens/Clock.cpp +++ b/src/displayapp/screens/Clock.cpp @@ -19,6 +19,7 @@ Clock::Clock(DisplayApp* app, Controllers::Ble& bleController, Controllers::NotificationManager& notificatioManager, Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, Controllers::MotionController& motionController, System::SystemTask& systemTask) : Screen(app), @@ -28,6 +29,7 @@ Clock::Clock(DisplayApp* app, bleController {bleController}, notificatioManager {notificatioManager}, settingsController {settingsController}, + heartRateController {heartRateController}, motionController {motionController}, screen {[this, &settingsController]() { switch (settingsController.GetClockFace()) { @@ -59,6 +61,7 @@ std::unique_ptr<Screen> Clock::WatchFaceDigitalScreen() { bleController, notificatioManager, settingsController, + heartRateController, motionController, systemTask); } diff --git a/src/displayapp/screens/Clock.h b/src/displayapp/screens/Clock.h index 5001dce..86793e9 100644 --- a/src/displayapp/screens/Clock.h +++ b/src/displayapp/screens/Clock.h @@ -4,6 +4,7 @@ #include <chrono> #include <cstdint> #include <memory> +#include <components/heartrate/HeartRateController.h> #include "displayapp/screens/Screen.h" #include "components/datetime/DateTimeController.h" #include "systemtask/SystemTask.h" @@ -27,6 +28,7 @@ namespace Pinetime { Controllers::Ble& bleController, Controllers::NotificationManager& notificatioManager, Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, Controllers::MotionController& motionController, System::SystemTask& systemTask); ~Clock() override; @@ -41,6 +43,7 @@ namespace Pinetime { Controllers::Ble& bleController; Controllers::NotificationManager& notificatioManager; Controllers::Settings& settingsController; + Controllers::HeartRateController& heartRateController; Controllers::MotionController& motionController; std::unique_ptr<Screen> screen; diff --git a/src/displayapp/screens/HeartRate.cpp b/src/displayapp/screens/HeartRate.cpp new file mode 100644 index 0000000..6f9154b --- /dev/null +++ b/src/displayapp/screens/HeartRate.cpp @@ -0,0 +1,115 @@ +#include "displayapp/screens/HeartRate.h" +#include <lvgl/lvgl.h> +#include <components/heartrate/HeartRateController.h> + +#include "displayapp/DisplayApp.h" + +using namespace Pinetime::Applications::Screens; + +namespace { + const char* ToString(Pinetime::Controllers::HeartRateController::States s) { + switch (s) { + case Pinetime::Controllers::HeartRateController::States::NotEnoughData: + return "Not enough data,\nplease wait..."; + case Pinetime::Controllers::HeartRateController::States::NoTouch: + return "No touch detected"; + case Pinetime::Controllers::HeartRateController::States::Running: + return "Measuring..."; + case Pinetime::Controllers::HeartRateController::States::Stopped: + return "Stopped"; + } + return ""; + } + + static void btnStartStopEventHandler(lv_obj_t* obj, lv_event_t event) { + HeartRate* screen = static_cast<HeartRate*>(obj->user_data); + screen->OnStartStopEvent(event); + } +} + +HeartRate::HeartRate(Pinetime::Applications::DisplayApp* app, + Controllers::HeartRateController& heartRateController, + System::SystemTask& systemTask) + : Screen(app), heartRateController {heartRateController}, systemTask {systemTask} { + bool isHrRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped; + label_hr = lv_label_create(lv_scr_act(), nullptr); + + lv_obj_set_style_local_text_font(label_hr, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &neofont3); + + if (isHrRunning) + lv_obj_set_style_local_text_color(label_hr, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN); + else + lv_obj_set_style_local_text_color(label_hr, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GRAY); + + lv_label_set_text(label_hr, "000"); + lv_obj_align(label_hr, nullptr, LV_ALIGN_CENTER, 0, -40); + + label_bpm = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text(label_bpm, "Heart rate BPM"); + lv_obj_align(label_bpm, label_hr, LV_ALIGN_OUT_TOP_MID, 0, -20); + + label_status = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(label_status, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x222222)); + lv_label_set_text(label_status, ToString(Pinetime::Controllers::HeartRateController::States::NotEnoughData)); + + lv_obj_align(label_status, label_hr, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); + + btn_startStop = lv_btn_create(lv_scr_act(), nullptr); + btn_startStop->user_data = this; + lv_obj_set_height(btn_startStop, 50); + lv_obj_set_event_cb(btn_startStop, btnStartStopEventHandler); + lv_obj_align(btn_startStop, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, 0); + + label_startStop = lv_label_create(btn_startStop, nullptr); + UpdateStartStopButton(isHrRunning); + if (isHrRunning) + systemTask.PushMessage(Pinetime::System::Messages::DisableSleeping); + + taskRefresh = lv_task_create(RefreshTaskCallback, 100, LV_TASK_PRIO_MID, this); +} + +HeartRate::~HeartRate() { + lv_task_del(taskRefresh); + lv_obj_clean(lv_scr_act()); + systemTask.PushMessage(Pinetime::System::Messages::EnableSleeping); +} + +void HeartRate::Refresh() { + + auto state = heartRateController.State(); + switch (state) { + case Controllers::HeartRateController::States::NoTouch: + case Controllers::HeartRateController::States::NotEnoughData: + // case Controllers::HeartRateController::States::Stopped: + lv_label_set_text(label_hr, "000"); + break; + default: + lv_label_set_text_fmt(label_hr, "%03d", heartRateController.HeartRate()); + } + + lv_label_set_text(label_status, ToString(state)); + lv_obj_align(label_status, label_hr, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); +} + +void HeartRate::OnStartStopEvent(lv_event_t event) { + if (event == LV_EVENT_CLICKED) { + if (heartRateController.State() == Controllers::HeartRateController::States::Stopped) { + heartRateController.Start(); + UpdateStartStopButton(heartRateController.State() != Controllers::HeartRateController::States::Stopped); + systemTask.PushMessage(Pinetime::System::Messages::DisableSleeping); + lv_obj_set_style_local_text_color(label_hr, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN); + } else { + heartRateController.Stop(); + UpdateStartStopButton(heartRateController.State() != Controllers::HeartRateController::States::Stopped); + systemTask.PushMessage(Pinetime::System::Messages::EnableSleeping); + lv_obj_set_style_local_text_color(label_hr, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GRAY); + } + } +} + +void HeartRate::UpdateStartStopButton(bool isRunning) { + if (isRunning) + lv_label_set_text(label_startStop, "Stop"); + else + lv_label_set_text(label_startStop, "Start"); +} diff --git a/src/displayapp/screens/HeartRate.h b/src/displayapp/screens/HeartRate.h new file mode 100644 index 0000000..2ad0035 --- /dev/null +++ b/src/displayapp/screens/HeartRate.h @@ -0,0 +1,40 @@ +#pragma once + +#include <cstdint> +#include <chrono> +#include "displayapp/screens/Screen.h" +#include "systemtask/SystemTask.h" +#include <lvgl/src/lv_core/lv_style.h> +#include <lvgl/src/lv_core/lv_obj.h> + +namespace Pinetime { + namespace Controllers { + class HeartRateController; + } + namespace Applications { + namespace Screens { + + class HeartRate : public Screen { + public: + HeartRate(DisplayApp* app, Controllers::HeartRateController& HeartRateController, System::SystemTask& systemTask); + ~HeartRate() override; + + void Refresh() override; + + void OnStartStopEvent(lv_event_t event); + + private: + Controllers::HeartRateController& heartRateController; + Pinetime::System::SystemTask& systemTask; + void UpdateStartStopButton(bool isRunning); + lv_obj_t* label_hr; + lv_obj_t* label_bpm; + lv_obj_t* label_status; + lv_obj_t* btn_startStop; + lv_obj_t* label_startStop; + + lv_task_t* taskRefresh; + }; + } + } +} diff --git a/src/displayapp/screens/WatchFaceDigital.cpp b/src/displayapp/screens/WatchFaceDigital.cpp index 963bfe4..44421ff 100644 --- a/src/displayapp/screens/WatchFaceDigital.cpp +++ b/src/displayapp/screens/WatchFaceDigital.cpp @@ -10,6 +10,7 @@ #include "components/battery/BatteryController.h" #include "components/ble/BleController.h" #include "components/ble/NotificationManager.h" +#include "components/heartrate/HeartRateController.h" #include "components/motion/MotionController.h" #include "components/settings/Settings.h" #include "displayapp/fonts/neofont.h" @@ -21,6 +22,7 @@ WatchFaceDigital::WatchFaceDigital(DisplayApp* app, Controllers::Ble& bleController, Controllers::NotificationManager& notificatioManager, Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, Controllers::MotionController& motionController, System::SystemTask& systemTask) : Screen(app), @@ -31,6 +33,7 @@ WatchFaceDigital::WatchFaceDigital(DisplayApp* app, bleController {bleController}, notificatioManager {notificatioManager}, settingsController {settingsController}, + heartRateController {heartRateController}, motionController {motionController} { if (1) { @@ -254,6 +257,16 @@ WatchFaceDigital::WatchFaceDigital(DisplayApp* app, lv_obj_set_pos(backgroundLabel, 0, 0); lv_label_set_text_static(backgroundLabel, ""); + heartbeatIcon = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(heartbeatIcon, Symbols::heartBeat); + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCE1B1B)); + lv_obj_align(heartbeatIcon, lv_scr_act(), LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); + + heartbeatValue = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_color(heartbeatValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCE1B1B)); + lv_label_set_text_static(heartbeatValue, ""); + lv_obj_align(heartbeatValue, heartbeatIcon, LV_ALIGN_OUT_RIGHT_MID, 5, 0); + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); Refresh(); } @@ -380,4 +393,18 @@ void WatchFaceDigital::Refresh() { } } + heartbeat = heartRateController.HeartRate(); + heartbeatRunning = heartRateController.State() != Controllers::HeartRateController::States::Stopped; + if (heartbeat.IsUpdated() || heartbeatRunning.IsUpdated()) { + if (heartbeatRunning.Get()) { + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0xCE1B1B)); + lv_label_set_text_fmt(heartbeatValue, "%d", heartbeat.Get()); + } else { + lv_obj_set_style_local_text_color(heartbeatIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x1B1B1B)); + lv_label_set_text_static(heartbeatValue, ""); + } + + lv_obj_realign(heartbeatIcon); + lv_obj_realign(heartbeatValue); + } } diff --git a/src/displayapp/screens/WatchFaceDigital.h b/src/displayapp/screens/WatchFaceDigital.h index 8382996..93719c1 100644 --- a/src/displayapp/screens/WatchFaceDigital.h +++ b/src/displayapp/screens/WatchFaceDigital.h @@ -15,6 +15,7 @@ namespace Pinetime { class Battery; class Ble; class NotificationManager; + class HeartRateController; class MotionController; } @@ -29,6 +30,7 @@ namespace Pinetime { Controllers::Ble& bleController, Controllers::NotificationManager& notificatioManager, Controllers::Settings& settingsController, + Controllers::HeartRateController& heartRateController, Controllers::MotionController& motionController, System::SystemTask& systemTask); ~WatchFaceDigital() override; @@ -54,6 +56,8 @@ namespace Pinetime { DirtyValue<std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds>> currentDateTime {}; DirtyValue<bool> motionSensorOk {}; DirtyValue<uint32_t> stepCount {}; + DirtyValue<uint8_t> heartbeat {}; + DirtyValue<bool> heartbeatRunning {}; DirtyValue<bool> notificationState {}; lv_obj_t* label_temp; @@ -68,6 +72,8 @@ namespace Pinetime { lv_obj_t* batteryIcon; lv_obj_t* bleIcon; lv_obj_t* batteryPlug; + lv_obj_t* heartbeatIcon; + lv_obj_t* heartbeatValue; lv_obj_t* stepIcon; lv_obj_t* stepValue; lv_obj_t* notificationIcon; @@ -77,6 +83,7 @@ namespace Pinetime { Controllers::Ble& bleController; Controllers::NotificationManager& notificatioManager; Controllers::Settings& settingsController; + Controllers::HeartRateController& heartRateController; Controllers::MotionController& motionController; lv_task_t* taskRefresh; diff --git a/src/heartratetask/HeartRateTask.cpp b/src/heartratetask/HeartRateTask.cpp new file mode 100644 index 0000000..2f689b9 --- /dev/null +++ b/src/heartratetask/HeartRateTask.cpp @@ -0,0 +1,100 @@ +#include "heartratetask/HeartRateTask.h" +#include <drivers/Hrs3300.h> +#include <components/heartrate/HeartRateController.h> +#include <nrf_log.h> + +using namespace Pinetime::Applications; + +HeartRateTask::HeartRateTask(Drivers::Hrs3300& heartRateSensor, Controllers::HeartRateController& controller) + : heartRateSensor {heartRateSensor}, controller {controller}, ppg{} { +} + +void HeartRateTask::Start() { + messageQueue = xQueueCreate(10, 1); + controller.SetHeartRateTask(this); + + if (pdPASS != xTaskCreate(HeartRateTask::Process, "Heartrate", 500, this, 0, &taskHandle)) + APP_ERROR_HANDLER(NRF_ERROR_NO_MEM); +} + +void HeartRateTask::Process(void* instance) { + auto* app = static_cast<HeartRateTask*>(instance); + app->Work(); +} + +void HeartRateTask::Work() { + int lastBpm = 0; + while (true) { + Messages msg; + uint32_t delay; + if (state == States::Running) { + if (measurementStarted) + delay = 40; + else + delay = 100; + } else + delay = portMAX_DELAY; + + if (xQueueReceive(messageQueue, &msg, delay)) { + switch (msg) { + case Messages::GoToSleep: + StopMeasurement(); + state = States::Idle; + break; + case Messages::WakeUp: + state = States::Running; + if (measurementStarted) { + lastBpm = 0; + StartMeasurement(); + } + break; + case Messages::StartMeasurement: + if (measurementStarted) + break; + lastBpm = 0; + StartMeasurement(); + measurementStarted = true; + break; + case Messages::StopMeasurement: + if (!measurementStarted) + break; + StopMeasurement(); + measurementStarted = false; + break; + } + } + + if (measurementStarted) { + ppg.Preprocess(static_cast<float>(heartRateSensor.ReadHrs())); + auto bpm = ppg.HeartRate(); + + if (lastBpm == 0 && bpm == 0) + controller.Update(Controllers::HeartRateController::States::NotEnoughData, 0); + if (bpm != 0) { + lastBpm = bpm; + controller.Update(Controllers::HeartRateController::States::Running, lastBpm); + } + } + } +} + +void HeartRateTask::PushMessage(HeartRateTask::Messages msg) { + BaseType_t xHigherPriorityTaskWoken; + xHigherPriorityTaskWoken = pdFALSE; + xQueueSendFromISR(messageQueue, &msg, &xHigherPriorityTaskWoken); + if (xHigherPriorityTaskWoken) { + /* Actual macro used here is port specific. */ + // TODO : should I do something here? + } +} + +void HeartRateTask::StartMeasurement() { + heartRateSensor.Enable(); + vTaskDelay(100); + ppg.SetOffset(static_cast<float>(heartRateSensor.ReadHrs())); +} + +void HeartRateTask::StopMeasurement() { + heartRateSensor.Disable(); + vTaskDelay(100); +} diff --git a/src/heartratetask/HeartRateTask.h b/src/heartratetask/HeartRateTask.h new file mode 100644 index 0000000..0796dc7 --- /dev/null +++ b/src/heartratetask/HeartRateTask.h @@ -0,0 +1,40 @@ +#pragma once +#include <FreeRTOS.h> +#include <task.h> +#include <queue.h> +#include <components/heartrate/Ppg.h> + +namespace Pinetime { + namespace Drivers { + class Hrs3300; + } + namespace Controllers { + class HeartRateController; + } + namespace Applications { + class HeartRateTask { + public: + enum class Messages : uint8_t { GoToSleep, WakeUp, StartMeasurement, StopMeasurement }; + enum class States { Idle, Running }; + + explicit HeartRateTask(Drivers::Hrs3300& heartRateSensor, Controllers::HeartRateController& controller); + void Start(); + void Work(); + void PushMessage(Messages msg); + + private: + static void Process(void* instance); + void StartMeasurement(); + void StopMeasurement(); + + TaskHandle_t taskHandle; + QueueHandle_t messageQueue; + States state = States::Running; + Drivers::Hrs3300& heartRateSensor; + Controllers::HeartRateController& controller; + Controllers::Ppg ppg; + bool measurementStarted = false; + }; + + } +} diff --git a/src/libs/mynewt-nimble/apps/blehr/README.md b/src/libs/mynewt-nimble/apps/blehr/README.md new file mode 100644 index 0000000..d06d95d --- /dev/null +++ b/src/libs/mynewt-nimble/apps/blehr/README.md @@ -0,0 +1,9 @@ +# BLE Heart Rate peripheral app. + +The source files are located in the src/ directory. + +pkg.yml contains the base definition of the app. + +syscfg.yml contains setting definitions and overrides. + + diff --git a/src/libs/mynewt-nimble/apps/blehr/pkg.yml b/src/libs/mynewt-nimble/apps/blehr/pkg.yml new file mode 100644 index 0000000..b91ba37 --- /dev/null +++ b/src/libs/mynewt-nimble/apps/blehr/pkg.yml @@ -0,0 +1,40 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +pkg.name: apps/blehr +pkg.type: app +pkg.description: BLE peripheral heartrate sensor. +pkg.author: "Szymon Czapracki" +pkg.email: "szymon.czapracki@codecoup.pl" +pkg.homepage: "http://mynewt.apache.org/" +pkg.keywords: + +pkg.deps: + - "@apache-mynewt-core/kernel/os" + - "@apache-mynewt-core/sys/console/full" + - "@apache-mynewt-core/sys/log/full" + - "@apache-mynewt-core/sys/log/modlog" + - "@apache-mynewt-core/sys/stats/full" + - "@apache-mynewt-core/sys/sysinit" + - "@apache-mynewt-core/sys/id" + - nimble/controller + - nimble/host + - nimble/host/services/gap + - nimble/host/services/gatt + - nimble/host/store/config + - nimble/transport/ram diff --git a/src/libs/mynewt-nimble/apps/blehr/src/blehr_sens.h b/src/libs/mynewt-nimble/apps/blehr/src/blehr_sens.h new file mode 100644 index 0000000..a3f59ca --- /dev/null +++ b/src/libs/mynewt-nimble/apps/blehr/src/blehr_sens.h @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef H_BLEHR_SENSOR_ +#define H_BLEHR_SENSOR_ + +#include "nimble/ble.h" +#include "modlog/modlog.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Heart-rate configuration */ +#define GATT_HRS_UUID 0x180D +#define GATT_HRS_MEASUREMENT_UUID 0x2A37 +#define GATT_HRS_BODY_SENSOR_LOC_UUID 0x2A38 +#define GATT_DEVICE_INFO_UUID 0x180A +#define GATT_MANUFACTURER_NAME_UUID 0x2A29 +#define GATT_MODEL_NUMBER_UUID 0x2A24 + +extern uint16_t hrs_hrm_handle; + +int gatt_svr_init(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/libs/mynewt-nimble/apps/blehr/src/gatt_svr.c b/src/libs/mynewt-nimble/apps/blehr/src/gatt_svr.c new file mode 100644 index 0000000..b2e9b4e --- /dev/null +++ b/src/libs/mynewt-nimble/apps/blehr/src/gatt_svr.c @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include <assert.h> +#include <stdio.h> +#include <string.h> +#include "host/ble_hs.h" +#include "host/ble_uuid.h" +#include "blehr_sens.h" + +static const char *manuf_name = "Apache Mynewt"; +static const char *model_num = "Mynewt HR Sensor"; +uint16_t hrs_hrm_handle; + +static int +gatt_svr_chr_access_heart_rate(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg); + +static int +gatt_svr_chr_access_device_info(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg); + +static const struct ble_gatt_svc_def gatt_svr_svcs[] = { + { + /* Service: Heart-rate */ + .type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = BLE_UUID16_DECLARE(GATT_HRS_UUID), + .characteristics = (struct ble_gatt_chr_def[]) { { + /* Characteristic: Heart-rate measurement */ + .uuid = BLE_UUID16_DECLARE(GATT_HRS_MEASUREMENT_UUID), + .access_cb = gatt_svr_chr_access_heart_rate, + .val_handle = &hrs_hrm_handle, + .flags = BLE_GATT_CHR_F_NOTIFY, + }, { + /* Characteristic: Body sensor location */ + .uuid = BLE_UUID16_DECLARE(GATT_HRS_BODY_SENSOR_LOC_UUID), + .access_cb = gatt_svr_chr_access_heart_rate, + .flags = BLE_GATT_CHR_F_READ, + }, { + 0, /* No more characteristics in this service */ + }, } + }, + + { + /* Service: Device Information */ + .type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = BLE_UUID16_DECLARE(GATT_DEVICE_INFO_UUID), + .characteristics = (struct ble_gatt_chr_def[]) { { + /* Characteristic: * Manufacturer name */ + .uuid = BLE_UUID16_DECLARE(GATT_MANUFACTURER_NAME_UUID), + .access_cb = gatt_svr_chr_access_device_info, + .flags = BLE_GATT_CHR_F_READ, + }, { + /* Characteristic: Model number string */ + .uuid = BLE_UUID16_DECLARE(GATT_MODEL_NUMBER_UUID), + .access_cb = gatt_svr_chr_access_device_info, + .flags = BLE_GATT_CHR_F_READ, + }, { + 0, /* No more characteristics in this service */ + }, } + }, + + { + 0, /* No more services */ + }, +}; + +static int +gatt_svr_chr_access_heart_rate(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + /* Sensor location, set to "Chest" */ + static uint8_t body_sens_loc = 0x01; + uint16_t uuid; + int rc; + + uuid = ble_uuid_u16(ctxt->chr->uuid); + + if (uuid == GATT_HRS_BODY_SENSOR_LOC_UUID) { + rc = os_mbuf_append(ctxt->om, &body_sens_loc, sizeof(body_sens_loc)); + + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + assert(0); + return BLE_ATT_ERR_UNLIKELY; +} + +static int +gatt_svr_chr_access_device_info(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + uint16_t uuid; + int rc; + + uuid = ble_uuid_u16(ctxt->chr->uuid); + + if (uuid == GATT_MODEL_NUMBER_UUID) { + rc = os_mbuf_append(ctxt->om, model_num, strlen(model_num)); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + if (uuid == GATT_MANUFACTURER_NAME_UUID) { + rc = os_mbuf_append(ctxt->om, manuf_name, strlen(manuf_name)); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + assert(0); + return BLE_ATT_ERR_UNLIKELY; +} + +void +gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg) +{ + char buf[BLE_UUID_STR_LEN]; + + switch (ctxt->op) { + case BLE_GATT_REGISTER_OP_SVC: + MODLOG_DFLT(DEBUG, "registered service %s with handle=%d\n", + ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf), + ctxt->svc.handle); + break; + + case BLE_GATT_REGISTER_OP_CHR: + MODLOG_DFLT(DEBUG, "registering characteristic %s with " + "def_handle=%d val_handle=%d\n", + ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf), + ctxt->chr.def_handle, + ctxt->chr.val_handle); + break; + + case BLE_GATT_REGISTER_OP_DSC: + MODLOG_DFLT(DEBUG, "registering descriptor %s with handle=%d\n", + ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf), + ctxt->dsc.handle); + break; + + default: + assert(0); + break; + } +} + +int +gatt_svr_init(void) +{ + int rc; + + rc = ble_gatts_count_cfg(gatt_svr_svcs); + if (rc != 0) { + return rc; + } + + rc = ble_gatts_add_svcs(gatt_svr_svcs); + if (rc != 0) { + return rc; + } + + return 0; +} + diff --git a/src/libs/mynewt-nimble/apps/blehr/src/main.c b/src/libs/mynewt-nimble/apps/blehr/src/main.c new file mode 100644 index 0000000..bf0f3f3 --- /dev/null +++ b/src/libs/mynewt-nimble/apps/blehr/src/main.c @@ -0,0 +1,261 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include <assert.h> +#include <string.h> +#include <stdio.h> +#include <errno.h> + +#include "os/mynewt.h" +#include "console/console.h" +#include "config/config.h" +#include "nimble/ble.h" +#include "host/ble_hs.h" +#include "services/gap/ble_svc_gap.h" +#include "blehr_sens.h" + +static bool notify_state; + +/* Connection handle */ +static uint16_t conn_handle; + +static const char *device_name = "blehr_sensor"; + +static int blehr_gap_event(struct ble_gap_event *event, void *arg); + +static uint8_t blehr_addr_type; + +/* Sending notify data timer */ +static struct os_callout blehr_tx_timer; + +/* Variable to simulate heart beats */ +static uint8_t heartrate = 90; + +/* + * Enables advertising with parameters: + * o General discoverable mode + * o Undirected connectable mode + */ +static void +blehr_advertise(void) +{ + struct ble_gap_adv_params adv_params; + struct ble_hs_adv_fields fields; + int rc; + + /* + * Set the advertisement data included in our advertisements: + * o Flags (indicates advertisement type and other general info) + * o Advertising tx power + * o Device name + */ + memset(&fields, 0, sizeof(fields)); + + /* + * Advertise two flags: + * o Discoverability in forthcoming advertisement (general) + * o BLE-only (BR/EDR unsupported) + */ + fields.flags = BLE_HS_ADV_F_DISC_GEN | + BLE_HS_ADV_F_BREDR_UNSUP; + + /* + * Indicate that the TX power level field should be included; have the + * stack fill this value automatically. This is done by assigning the + * special value BLE_HS_ADV_TX_PWR_LVL_AUTO. + */ + fields.tx_pwr_lvl_is_present = 1; + fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; + + fields.name = (uint8_t *)device_name; + fields.name_len = strlen(device_name); + fields.name_is_complete = 1; + + rc = ble_gap_adv_set_fields(&fields); + if (rc != 0) { + MODLOG_DFLT(ERROR, "error setting advertisement data; rc=%d\n", rc); + return; + } + + /* Begin advertising */ + memset(&adv_params, 0, sizeof(adv_params)); + adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; + adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; + rc = ble_gap_adv_start(blehr_addr_type, NULL, BLE_HS_FOREVER, + &adv_params, blehr_gap_event, NULL); + if (rc != 0) { + MODLOG_DFLT(ERROR, "error enabling advertisement; rc=%d\n", rc); + return; + } +} + +static void +blehr_tx_hrate_stop(void) +{ + os_callout_stop(&blehr_tx_timer); +} + +/* Reset heartrate measurment */ +static void +blehr_tx_hrate_reset(void) +{ + int rc; + + rc = os_callout_reset(&blehr_tx_timer, OS_TICKS_PER_SEC); + assert(rc == 0); +} + +/* This functions simulates heart beat and notifies it to the client */ +static void +blehr_tx_hrate(struct os_event *ev) +{ + static uint8_t hrm[2]; + int rc; + struct os_mbuf *om; + + if (!notify_state) { + blehr_tx_hrate_stop(); + heartrate = 90; + return; + } + + hrm[0] = 0x06; /* contact of a sensor */ + hrm[1] = heartrate; /* storing dummy data */ + + /* Simulation of heart beats */ + heartrate++; + if (heartrate == 160) { + heartrate = 90; + } + + om = ble_hs_mbuf_from_flat(hrm, sizeof(hrm)); + + rc = ble_gattc_notify_custom(conn_handle, hrs_hrm_handle, om); + + assert(rc == 0); + blehr_tx_hrate_reset(); +} + +static int +blehr_gap_event(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + /* A new connection was established or a connection attempt failed */ + MODLOG_DFLT(INFO, "connection %s; status=%d\n", + event->connect.status == 0 ? "established" : "failed", + event->connect.status); + + if (event->connect.status != 0) { + /* Connection failed; resume advertising */ + blehr_advertise(); + conn_handle = 0; + } + else { + conn_handle = event->connect.conn_handle; + } + + break; + + case BLE_GAP_EVENT_DISCONNECT: + MODLOG_DFLT(INFO, "disconnect; reason=%d\n", event->disconnect.reason); + conn_handle = BLE_HS_CONN_HANDLE_NONE; /* reset conn_handle */ + + /* Connection terminated; resume advertising */ + blehr_advertise(); + break; + + case BLE_GAP_EVENT_ADV_COMPLETE: + MODLOG_DFLT(INFO, "adv complete\n"); + blehr_advertise(); + break; + + case BLE_GAP_EVENT_SUBSCRIBE: + MODLOG_DFLT(INFO, "subscribe event; cur_notify=%d\n value handle; " + "val_handle=%d\n", + event->subscribe.cur_notify, hrs_hrm_handle); + if (event->subscribe.attr_handle == hrs_hrm_handle) { + notify_state = event->subscribe.cur_notify; + blehr_tx_hrate_reset(); + } else if (event->subscribe.attr_handle != hrs_hrm_handle) { + notify_state = event->subscribe.cur_notify; + blehr_tx_hrate_stop(); + } + break; + + case BLE_GAP_EVENT_MTU: + MODLOG_DFLT(INFO, "mtu update event; conn_handle=%d mtu=%d\n", + event->mtu.conn_handle, + event->mtu.value); + break; + + } + + return 0; +} + +static void +blehr_on_sync(void) +{ + int rc; + + /* Use privacy */ + rc = ble_hs_id_infer_auto(0, &blehr_addr_type); + assert(rc == 0); + + /* Begin advertising */ + blehr_advertise(); +} + +/* + * main + * + * The main task for the project. This function initializes the packages, + * then starts serving events from default event queue. + * + * @return int NOTE: this function should never return! + */ +int +main(void) +{ + int rc; + + /* Initialize OS */ + sysinit(); + + /* Initialize the NimBLE host configuration */ + ble_hs_cfg.sync_cb = blehr_on_sync; + + os_callout_init(&blehr_tx_timer, os_eventq_dflt_get(), + blehr_tx_hrate, NULL); + + rc = gatt_svr_init(); + assert(rc == 0); + + /* Set the default device name */ + rc = ble_svc_gap_device_name_set(device_name); + assert(rc == 0); + + /* As the last thing, process events from default event queue */ + while (1) { + os_eventq_run(os_eventq_dflt_get()); + } + return 0; +} + diff --git a/src/libs/mynewt-nimble/apps/blehr/syscfg.yml b/src/libs/mynewt-nimble/apps/blehr/syscfg.yml new file mode 100644 index 0000000..a6be2e2 --- /dev/null +++ b/src/libs/mynewt-nimble/apps/blehr/syscfg.yml @@ -0,0 +1,35 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +syscfg.vals: + # Disable central and observer roles. + BLE_ROLE_BROADCASTER: 1 + BLE_ROLE_CENTRAL: 0 + BLE_ROLE_OBSERVER: 0 + BLE_ROLE_PERIPHERAL: 1 + + # Disable unused eddystone feature. + BLE_EDDYSTONE: 0 + + # Log reboot messages to a flash circular buffer. + REBOOT_LOG_FCB: 1 + LOG_FCB: 1 + CONFIG_FCB: 1 + + # Set public device address. + BLE_PUBLIC_DEV_ADDR: ((uint8_t[6]){0xcc, 0xbb, 0xaa, 0x33, 0x22, 0x11}) diff --git a/src/main.cpp b/src/main.cpp index 8022595..0f68ca8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -36,6 +36,7 @@ #include "components/brightness/BrightnessController.h" #include "components/motor/MotorController.h" #include "components/datetime/DateTimeController.h" +#include "components/heartrate/HeartRateController.h" #include "drivers/Spi.h" #include "drivers/SpiMaster.h" #include "drivers/SpiNorFlash.h" @@ -58,6 +59,7 @@ Pinetime::Logging::DummyLogger logger; static constexpr uint8_t touchPanelTwiAddress = 0x15; static constexpr uint8_t motionSensorTwiAddress = 0x18; +static constexpr uint8_t heartRateSensorTwiAddress = 0x44; Pinetime::Drivers::SpiMaster spi {Pinetime::Drivers::SpiMaster::SpiModule::SPI0, {Pinetime::Drivers::SpiMaster::BitOrder::Msb_Lsb, @@ -89,12 +91,16 @@ Pinetime::Drivers::Cst816S touchPanel {twiMaster, touchPanelTwiAddress}; Pinetime::Components::LittleVgl lvgl {lcd, touchPanel}; Pinetime::Drivers::Bma421 motionSensor {twiMaster, motionSensorTwiAddress}; +Pinetime::Drivers::Hrs3300 heartRateSensor {twiMaster, heartRateSensorTwiAddress}; TimerHandle_t debounceTimer; TimerHandle_t debounceChargeTimer; Pinetime::Controllers::Battery batteryController; Pinetime::Controllers::Ble bleController; +Pinetime::Controllers::HeartRateController heartRateController; +Pinetime::Applications::HeartRateTask heartRateApp(heartRateSensor, heartRateController); + Pinetime::Controllers::Settings settingsController {}; Pinetime::Controllers::MotorController motorController {}; @@ -115,6 +121,7 @@ Pinetime::Applications::DisplayApp displayApp(lcd, dateTimeController, watchdogView, notificationManager, + heartRateController, settingsController, motorController, motionController, @@ -133,10 +140,13 @@ Pinetime::System::SystemTask systemTask(spi, watchdog, notificationManager, motorController, + heartRateSensor, motionController, motionSensor, settingsController, + heartRateController, displayApp, + heartRateApp, touchHandler, buttonHandler); diff --git a/src/systemtask/SystemTask.cpp b/src/systemtask/SystemTask.cpp index 7a0d151..4e7f3c0 100644 --- a/src/systemtask/SystemTask.cpp +++ b/src/systemtask/SystemTask.cpp @@ -59,10 +59,13 @@ SystemTask::SystemTask(Drivers::SpiMaster& spi, Drivers::Watchdog& watchdog, Pinetime::Controllers::NotificationManager& notificationManager, Pinetime::Controllers::MotorController& motorController, + Pinetime::Drivers::Hrs3300& heartRateSensor, Pinetime::Controllers::MotionController& motionController, Pinetime::Drivers::Bma421& motionSensor, Controllers::Settings& settingsController, + Pinetime::Controllers::HeartRateController& heartRateController, Pinetime::Applications::DisplayApp& displayApp, + Pinetime::Applications::HeartRateTask& heartRateApp, Pinetime::Controllers::TouchHandler& touchHandler, Pinetime::Controllers::ButtonHandler& buttonHandler) : spi {spi}, @@ -77,10 +80,13 @@ SystemTask::SystemTask(Drivers::SpiMaster& spi, watchdog {watchdog}, notificationManager {notificationManager}, motorController {motorController}, + heartRateSensor {heartRateSensor}, motionSensor {motionSensor}, settingsController {settingsController}, + heartRateController {heartRateController}, motionController {motionController}, displayApp {displayApp}, + heartRateApp(heartRateApp), touchHandler {touchHandler}, buttonHandler {buttonHandler}, nimbleController(*this, @@ -88,7 +94,8 @@ SystemTask::SystemTask(Drivers::SpiMaster& spi, dateTimeController, notificationManager, batteryController, - spiNorFlash) { + spiNorFlash, + heartRateController) { } void SystemTask::Start() { @@ -145,6 +152,10 @@ void SystemTask::Work() { displayApp.Register(this); displayApp.Start(bootError); + heartRateSensor.Init(); + heartRateSensor.Disable(); + heartRateApp.Start(); + buttonHandler.Init(this); // Button @@ -227,6 +238,7 @@ void SystemTask::Work() { lcd.Wakeup(); displayApp.PushMessage(Pinetime::Applications::Display::Messages::GoToRunning); + heartRateApp.PushMessage(Pinetime::Applications::HeartRateTask::Messages::WakeUp); if (!bleController.IsConnected()) { nimbleController.RestartFastAdv(); @@ -257,6 +269,7 @@ void SystemTask::Work() { xTimerStop(idleTimer, 0); xTimerStop(dimTimer, 0); displayApp.PushMessage(Pinetime::Applications::Display::Messages::GoToSleep); + heartRateApp.PushMessage(Pinetime::Applications::HeartRateTask::Messages::GoToSleep); break; case Messages::OnNewTime: ReloadIdleTimer(); diff --git a/src/systemtask/SystemTask.h b/src/systemtask/SystemTask.h index 7104115..b36c36d 100644 --- a/src/systemtask/SystemTask.h +++ b/src/systemtask/SystemTask.h @@ -6,6 +6,7 @@ #include <queue.h> #include <task.h> #include <timers.h> +#include <heartratetask/HeartRateTask.h> #include <components/settings/Settings.h> #include <drivers/Bma421.h> #include <drivers/PinMap.h> @@ -60,10 +61,13 @@ namespace Pinetime { Drivers::Watchdog& watchdog, Pinetime::Controllers::NotificationManager& notificationManager, Pinetime::Controllers::MotorController& motorController, + Pinetime::Drivers::Hrs3300& heartRateSensor, Pinetime::Controllers::MotionController& motionController, Pinetime::Drivers::Bma421& motionSensor, Controllers::Settings& settingsController, + Pinetime::Controllers::HeartRateController& heartRateController, Pinetime::Applications::DisplayApp& displayApp, + Pinetime::Applications::HeartRateTask& heartRateApp, Pinetime::Controllers::TouchHandler& touchHandler, Pinetime::Controllers::ButtonHandler& buttonHandler); @@ -105,12 +109,15 @@ namespace Pinetime { Pinetime::Controllers::NotificationManager& notificationManager; Pinetime::Controllers::MotorController& motorController; public: + Pinetime::Drivers::Hrs3300& heartRateSensor; Pinetime::Drivers::Bma421& motionSensor; private: Pinetime::Controllers::Settings& settingsController; + Pinetime::Controllers::HeartRateController& heartRateController; Pinetime::Controllers::MotionController& motionController; Pinetime::Applications::DisplayApp& displayApp; + Pinetime::Applications::HeartRateTask& heartRateApp; Pinetime::Controllers::TouchHandler& touchHandler; Pinetime::Controllers::ButtonHandler& buttonHandler; Pinetime::Controllers::NimbleController nimbleController; |
