diff options
Diffstat (limited to 'src/components')
41 files changed, 1322 insertions, 1898 deletions
diff --git a/src/components/alarm/AlarmController.cpp b/src/components/alarm/AlarmController.cpp index c4eb8ed0..4ae42c08 100644 --- a/src/components/alarm/AlarmController.cpp +++ b/src/components/alarm/AlarmController.cpp @@ -19,11 +19,13 @@ #include "systemtask/SystemTask.h" #include "task.h" #include <chrono> +#include <libraries/log/nrf_log.h> using namespace Pinetime::Controllers; using namespace std::chrono_literals; -AlarmController::AlarmController(Controllers::DateTime& dateTimeController) : dateTimeController {dateTimeController} { +AlarmController::AlarmController(Controllers::DateTime& dateTimeController, Controllers::FS& fs) + : dateTimeController {dateTimeController}, fs {fs} { } namespace { @@ -36,11 +38,28 @@ namespace { void AlarmController::Init(System::SystemTask* systemTask) { this->systemTask = systemTask; alarmTimer = xTimerCreate("Alarm", 1, pdFALSE, this, SetOffAlarm); + LoadSettingsFromFile(); + if (alarm.isEnabled) { + NRF_LOG_INFO("[AlarmController] Loaded alarm was enabled, scheduling"); + ScheduleAlarm(); + } +} + +void AlarmController::SaveAlarm() { + // verify if it is necessary to save + if (alarmChanged) { + SaveSettingsToFile(); + } + alarmChanged = false; } void AlarmController::SetAlarmTime(uint8_t alarmHr, uint8_t alarmMin) { - hours = alarmHr; - minutes = alarmMin; + if (alarm.hours == alarmHr && alarm.minutes == alarmMin) { + return; + } + alarm.hours = alarmHr; + alarm.minutes = alarmMin; + alarmChanged = true; } void AlarmController::ScheduleAlarm() { @@ -53,18 +72,19 @@ void AlarmController::ScheduleAlarm() { tm* tmAlarmTime = std::localtime(&ttAlarmTime); // If the time being set has already passed today,the alarm should be set for tomorrow - if (hours < dateTimeController.Hours() || (hours == dateTimeController.Hours() && minutes <= dateTimeController.Minutes())) { + if (alarm.hours < dateTimeController.Hours() || + (alarm.hours == dateTimeController.Hours() && alarm.minutes <= dateTimeController.Minutes())) { tmAlarmTime->tm_mday += 1; // tm_wday doesn't update automatically tmAlarmTime->tm_wday = (tmAlarmTime->tm_wday + 1) % 7; } - tmAlarmTime->tm_hour = hours; - tmAlarmTime->tm_min = minutes; + tmAlarmTime->tm_hour = alarm.hours; + tmAlarmTime->tm_min = alarm.minutes; tmAlarmTime->tm_sec = 0; // if alarm is in weekday-only mode, make sure it shifts to the next weekday - if (recurrence == RecurType::Weekdays) { + if (alarm.recurrence == RecurType::Weekdays) { if (tmAlarmTime->tm_wday == 0) { // Sunday, shift 1 day tmAlarmTime->tm_mday += 1; } else if (tmAlarmTime->tm_wday == 6) { // Saturday, shift 2 days @@ -79,7 +99,10 @@ void AlarmController::ScheduleAlarm() { xTimerChangePeriod(alarmTimer, secondsToAlarm * configTICK_RATE_HZ, 0); xTimerStart(alarmTimer, 0); - state = AlarmState::Set; + if (!alarm.isEnabled) { + alarm.isEnabled = true; + alarmChanged = true; + } } uint32_t AlarmController::SecondsToAlarm() const { @@ -88,20 +111,71 @@ uint32_t AlarmController::SecondsToAlarm() const { void AlarmController::DisableAlarm() { xTimerStop(alarmTimer, 0); - state = AlarmState::Not_Set; + if (alarm.isEnabled) { + alarm.isEnabled = false; + alarmChanged = true; + } } void AlarmController::SetOffAlarmNow() { - state = AlarmState::Alerting; + isAlerting = true; systemTask->PushMessage(System::Messages::SetOffAlarm); } void AlarmController::StopAlerting() { - // Alarm state is off unless this is a recurring alarm - if (recurrence == RecurType::None) { - state = AlarmState::Not_Set; + isAlerting = false; + // Disable alarm unless it is recurring + if (alarm.recurrence == RecurType::None) { + alarm.isEnabled = false; + alarmChanged = true; } else { // set next instance ScheduleAlarm(); } } + +void AlarmController::SetRecurrence(RecurType recurrence) { + if (alarm.recurrence != recurrence) { + alarm.recurrence = recurrence; + alarmChanged = true; + } +} + +void AlarmController::LoadSettingsFromFile() { + lfs_file_t alarmFile; + AlarmSettings alarmBuffer; + + if (fs.FileOpen(&alarmFile, "/.system/alarm.dat", LFS_O_RDONLY) != LFS_ERR_OK) { + NRF_LOG_WARNING("[AlarmController] Failed to open alarm data file"); + return; + } + + fs.FileRead(&alarmFile, reinterpret_cast<uint8_t*>(&alarmBuffer), sizeof(alarmBuffer)); + fs.FileClose(&alarmFile); + if (alarmBuffer.version != alarmFormatVersion) { + NRF_LOG_WARNING("[AlarmController] Loaded alarm settings has version %u instead of %u, discarding", + alarmBuffer.version, + alarmFormatVersion); + return; + } + + alarm = alarmBuffer; + NRF_LOG_INFO("[AlarmController] Loaded alarm settings from file"); +} + +void AlarmController::SaveSettingsToFile() const { + lfs_dir systemDir; + if (fs.DirOpen("/.system", &systemDir) != LFS_ERR_OK) { + fs.DirCreate("/.system"); + } + fs.DirClose(&systemDir); + lfs_file_t alarmFile; + if (fs.FileOpen(&alarmFile, "/.system/alarm.dat", LFS_O_WRONLY | LFS_O_CREAT) != LFS_ERR_OK) { + NRF_LOG_WARNING("[AlarmController] Failed to open alarm data file for saving"); + return; + } + + fs.FileWrite(&alarmFile, reinterpret_cast<const uint8_t*>(&alarm), sizeof(alarm)); + fs.FileClose(&alarmFile); + NRF_LOG_INFO("[AlarmController] Saved alarm settings with format version %u to file", alarm.version); +} diff --git a/src/components/alarm/AlarmController.h b/src/components/alarm/AlarmController.h index 8ac0de9a..278e9cdb 100644 --- a/src/components/alarm/AlarmController.h +++ b/src/components/alarm/AlarmController.h @@ -30,47 +30,65 @@ namespace Pinetime { namespace Controllers { class AlarmController { public: - AlarmController(Controllers::DateTime& dateTimeController); + AlarmController(Controllers::DateTime& dateTimeController, Controllers::FS& fs); void Init(System::SystemTask* systemTask); + void SaveAlarm(); void SetAlarmTime(uint8_t alarmHr, uint8_t alarmMin); void ScheduleAlarm(); void DisableAlarm(); void SetOffAlarmNow(); uint32_t SecondsToAlarm() const; void StopAlerting(); - enum class AlarmState { Not_Set, Set, Alerting }; enum class RecurType { None, Daily, Weekdays }; uint8_t Hours() const { - return hours; + return alarm.hours; } uint8_t Minutes() const { - return minutes; + return alarm.minutes; } - AlarmState State() const { - return state; + bool IsAlerting() const { + return isAlerting; } - RecurType Recurrence() const { - return recurrence; + bool IsEnabled() const { + return alarm.isEnabled; } - void SetRecurrence(RecurType recurType) { - recurrence = recurType; + RecurType Recurrence() const { + return alarm.recurrence; } + void SetRecurrence(RecurType recurrence); + private: + // Versions 255 is reserved for now, so the version field can be made + // bigger, should it ever be needed. + static constexpr uint8_t alarmFormatVersion = 1; + + struct AlarmSettings { + uint8_t version = alarmFormatVersion; + uint8_t hours = 7; + uint8_t minutes = 0; + RecurType recurrence = RecurType::None; + bool isEnabled = false; + }; + + bool isAlerting = false; + bool alarmChanged = false; + Controllers::DateTime& dateTimeController; + Controllers::FS& fs; System::SystemTask* systemTask = nullptr; TimerHandle_t alarmTimer; - uint8_t hours = 7; - uint8_t minutes = 0; + AlarmSettings alarm; std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> alarmTime; - AlarmState state = AlarmState::Not_Set; - RecurType recurrence = RecurType::None; + + void LoadSettingsFromFile(); + void SaveSettingsToFile() const; }; } } diff --git a/src/components/battery/BatteryController.cpp b/src/components/battery/BatteryController.cpp index d9e198c5..fdb461f8 100644 --- a/src/components/battery/BatteryController.cpp +++ b/src/components/battery/BatteryController.cpp @@ -1,5 +1,5 @@ #include "components/battery/BatteryController.h" -#include "components/utility/LinearApproximation.h" +#include "utility/LinearApproximation.h" #include "drivers/PinMap.h" #include <hal/nrf_gpio.h> #include <nrfx_saadc.h> diff --git a/src/components/ble/DfuService.cpp b/src/components/ble/DfuService.cpp index 1f06b69e..2427513d 100644 --- a/src/components/ble/DfuService.cpp +++ b/src/components/ble/DfuService.cpp @@ -124,9 +124,11 @@ int DfuService::WritePacketHandler(uint16_t connectionHandle, os_mbuf* om) { bootloaderSize, applicationSize); - // wait until SystemTask has finished waking up all devices - while (systemTask.IsSleeping()) { - vTaskDelay(50); // 50ms + // Wait until SystemTask has disabled sleeping + // This isn't quite correct, as we don't actually know + // if BleFirmwareUpdateStarted has been received yet + while (!systemTask.IsSleepDisabled()) { + vTaskDelay(pdMS_TO_TICKS(5)); } dfuImage.Erase(); @@ -357,6 +359,8 @@ void DfuService::DfuImage::Init(size_t chunkSize, size_t totalSize, uint16_t exp this->totalSize = totalSize; this->expectedCrc = expectedCrc; this->ready = true; + totalWriteIndex = 0; + bufferWriteIndex = 0; } void DfuService::DfuImage::Append(uint8_t* data, size_t size) { diff --git a/src/components/ble/DfuService.h b/src/components/ble/DfuService.h index b56911b9..6652cdc1 100644 --- a/src/components/ble/DfuService.h +++ b/src/components/ble/DfuService.h @@ -77,6 +77,10 @@ namespace Pinetime { uint16_t ComputeCrc(uint8_t const* p_data, uint32_t size, uint16_t const* p_crc); }; + static constexpr ble_uuid128_t serviceUuid { + .u {.type = BLE_UUID_TYPE_128}, + .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x30, 0x15, 0x00, 0x00}}; + private: Pinetime::System::SystemTask& systemTask; Pinetime::Controllers::Ble& bleController; @@ -90,10 +94,6 @@ namespace Pinetime { uint16_t revision {0x0008}; - static constexpr ble_uuid128_t serviceUuid { - .u {.type = BLE_UUID_TYPE_128}, - .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x30, 0x15, 0x00, 0x00}}; - static constexpr ble_uuid128_t packetCharacteristicUuid { .u {.type = BLE_UUID_TYPE_128}, .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x32, 0x15, 0x00, 0x00}}; diff --git a/src/components/ble/HeartRateService.cpp b/src/components/ble/HeartRateService.cpp index c522e67e..d34dbf83 100644 --- a/src/components/ble/HeartRateService.cpp +++ b/src/components/ble/HeartRateService.cpp @@ -1,6 +1,6 @@ #include "components/ble/HeartRateService.h" #include "components/heartrate/HeartRateController.h" -#include "systemtask/SystemTask.h" +#include "components/ble/NimbleController.h" #include <nrf_log.h> using namespace Pinetime::Controllers; @@ -16,8 +16,8 @@ namespace { } // TODO Refactoring - remove dependency to SystemTask -HeartRateService::HeartRateService(Pinetime::System::SystemTask& system, Controllers::HeartRateController& heartRateController) - : system {system}, +HeartRateService::HeartRateService(NimbleController& nimble, Controllers::HeartRateController& heartRateController) + : nimble {nimble}, heartRateController {heartRateController}, characteristicDefinition {{.uuid = &heartRateMeasurementUuid.u, .access_cb = HeartRateServiceCallback, @@ -63,7 +63,7 @@ void HeartRateService::OnNewHeartRateValue(uint8_t heartRateValue) { uint8_t buffer[2] = {0, heartRateValue}; // [0] = flags, [1] = hr value auto* om = ble_hs_mbuf_from_flat(buffer, 2); - uint16_t connectionHandle = system.nimble().connHandle(); + uint16_t connectionHandle = nimble.connHandle(); if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) { return; diff --git a/src/components/ble/HeartRateService.h b/src/components/ble/HeartRateService.h index 003bdbd1..ca8f10fb 100644 --- a/src/components/ble/HeartRateService.h +++ b/src/components/ble/HeartRateService.h @@ -2,21 +2,18 @@ #define min // workaround: nimble's min/max macros conflict with libstdc++ #define max #include <host/ble_gap.h> -#include <atomic> #undef max #undef min +#include <atomic> namespace Pinetime { - namespace System { - class SystemTask; - } - namespace Controllers { class HeartRateController; + class NimbleController; class HeartRateService { public: - HeartRateService(Pinetime::System::SystemTask& system, Controllers::HeartRateController& heartRateController); + HeartRateService(NimbleController& nimble, Controllers::HeartRateController& heartRateController); void Init(); int OnHeartRateRequested(uint16_t attributeHandle, ble_gatt_access_ctxt* context); void OnNewHeartRateValue(uint8_t hearRateValue); @@ -24,14 +21,14 @@ namespace Pinetime { void SubscribeNotification(uint16_t attributeHandle); void UnsubscribeNotification(uint16_t attributeHandle); + static constexpr uint16_t heartRateServiceId {0x180D}; + static constexpr ble_uuid16_t heartRateServiceUuid {.u {.type = BLE_UUID_TYPE_16}, .value = heartRateServiceId}; + private: - Pinetime::System::SystemTask& system; + NimbleController& nimble; 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]; diff --git a/src/components/ble/MotionService.cpp b/src/components/ble/MotionService.cpp index 604f22d5..1626a5bf 100644 --- a/src/components/ble/MotionService.cpp +++ b/src/components/ble/MotionService.cpp @@ -1,6 +1,6 @@ #include "components/ble/MotionService.h" #include "components/motion/MotionController.h" -#include "systemtask/SystemTask.h" +#include "components/ble/NimbleController.h" #include <nrf_log.h> using namespace Pinetime::Controllers; @@ -28,8 +28,8 @@ namespace { } // TODO Refactoring - remove dependency to SystemTask -MotionService::MotionService(Pinetime::System::SystemTask& system, Controllers::MotionController& motionController) - : system {system}, +MotionService::MotionService(NimbleController& nimble, Controllers::MotionController& motionController) + : nimble {nimble}, motionController {motionController}, characteristicDefinition {{.uuid = &stepCountCharUuid.u, .access_cb = MotionServiceCallback, @@ -82,7 +82,7 @@ void MotionService::OnNewStepCountValue(uint32_t stepCount) { uint32_t buffer = stepCount; auto* om = ble_hs_mbuf_from_flat(&buffer, 4); - uint16_t connectionHandle = system.nimble().connHandle(); + uint16_t connectionHandle = nimble.connHandle(); if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) { return; @@ -98,7 +98,7 @@ void MotionService::OnNewMotionValues(int16_t x, int16_t y, int16_t z) { int16_t buffer[3] = {x, y, z}; auto* om = ble_hs_mbuf_from_flat(buffer, 3 * sizeof(int16_t)); - uint16_t connectionHandle = system.nimble().connHandle(); + uint16_t connectionHandle = nimble.connHandle(); if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) { return; @@ -120,3 +120,7 @@ void MotionService::UnsubscribeNotification(uint16_t attributeHandle) { else if (attributeHandle == motionValuesHandle) motionValuesNoficationEnabled = false; } + +bool MotionService::IsMotionNotificationSubscribed() const { + return motionValuesNoficationEnabled; +} diff --git a/src/components/ble/MotionService.h b/src/components/ble/MotionService.h index 1b172528..acc91e8d 100644 --- a/src/components/ble/MotionService.h +++ b/src/components/ble/MotionService.h @@ -7,16 +7,13 @@ #undef min namespace Pinetime { - namespace System { - class SystemTask; - } - namespace Controllers { + class NimbleController; class MotionController; class MotionService { public: - MotionService(Pinetime::System::SystemTask& system, Controllers::MotionController& motionController); + MotionService(NimbleController& nimble, Controllers::MotionController& motionController); void Init(); int OnStepCountRequested(uint16_t attributeHandle, ble_gatt_access_ctxt* context); void OnNewStepCountValue(uint32_t stepCount); @@ -24,9 +21,10 @@ namespace Pinetime { void SubscribeNotification(uint16_t attributeHandle); void UnsubscribeNotification(uint16_t attributeHandle); + bool IsMotionNotificationSubscribed() const; private: - Pinetime::System::SystemTask& system; + NimbleController& nimble; Controllers::MotionController& motionController; struct ble_gatt_chr_def characteristicDefinition[3]; diff --git a/src/components/ble/MusicService.cpp b/src/components/ble/MusicService.cpp index 403c957b..43cbec70 100644 --- a/src/components/ble/MusicService.cpp +++ b/src/components/ble/MusicService.cpp @@ -16,8 +16,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ #include "components/ble/MusicService.h" -#include "systemtask/SystemTask.h" +#include "components/ble/NimbleController.h" #include <cstring> +#include <FreeRTOS.h> +#include <task.h> namespace { // 0000yyxx-78fc-48fe-8e23-433b3a1942d0 @@ -53,7 +55,7 @@ namespace { } } -Pinetime::Controllers::MusicService::MusicService(Pinetime::System::SystemTask& system) : m_system(system) { +Pinetime::Controllers::MusicService::MusicService(Pinetime::Controllers::NimbleController& nimble) : nimble(nimble) { characteristicDefinition[0] = {.uuid = &msEventCharUuid.u, .access_cb = MusicCallback, .arg = this, @@ -212,7 +214,7 @@ int Pinetime::Controllers::MusicService::getTrackLength() const { void Pinetime::Controllers::MusicService::event(char event) { auto* om = ble_hs_mbuf_from_flat(&event, 1); - uint16_t connectionHandle = m_system.nimble().connHandle(); + uint16_t connectionHandle = nimble.connHandle(); if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) { return; diff --git a/src/components/ble/MusicService.h b/src/components/ble/MusicService.h index 6aebc3c5..93d94a34 100644 --- a/src/components/ble/MusicService.h +++ b/src/components/ble/MusicService.h @@ -25,16 +25,15 @@ #include <host/ble_uuid.h> #undef max #undef min +#include <FreeRTOS.h> namespace Pinetime { - namespace System { - class SystemTask; - } - namespace Controllers { + class NimbleController; + class MusicService { public: - explicit MusicService(Pinetime::System::SystemTask& system); + explicit MusicService(NimbleController& nimble); void Init(); @@ -89,7 +88,7 @@ namespace Pinetime { bool repeat {false}; bool shuffle {false}; - Pinetime::System::SystemTask& m_system; + NimbleController& nimble; }; } } diff --git a/src/components/ble/NavigationService.cpp b/src/components/ble/NavigationService.cpp index 5508d08c..4922237c 100644 --- a/src/components/ble/NavigationService.cpp +++ b/src/components/ble/NavigationService.cpp @@ -18,8 +18,6 @@ #include "components/ble/NavigationService.h" -#include "systemtask/SystemTask.h" - namespace { // 0001yyxx-78fc-48fe-8e23-433b3a1942d0 constexpr ble_uuid128_t CharUuid(uint8_t x, uint8_t y) { @@ -45,7 +43,7 @@ namespace { } } // namespace -Pinetime::Controllers::NavigationService::NavigationService(Pinetime::System::SystemTask& system) : m_system(system) { +Pinetime::Controllers::NavigationService::NavigationService() { characteristicDefinition[0] = {.uuid = &navFlagCharUuid.u, .access_cb = NAVCallback, .arg = this, diff --git a/src/components/ble/NavigationService.h b/src/components/ble/NavigationService.h index 1c1739ba..03e79ac5 100644 --- a/src/components/ble/NavigationService.h +++ b/src/components/ble/NavigationService.h @@ -27,15 +27,11 @@ #undef min namespace Pinetime { - namespace System { - class SystemTask; - } - namespace Controllers { class NavigationService { public: - explicit NavigationService(Pinetime::System::SystemTask& system); + NavigationService(); void Init(); @@ -57,8 +53,6 @@ namespace Pinetime { std::string m_narrative; std::string m_manDist; int m_progress; - - Pinetime::System::SystemTask& m_system; }; } } diff --git a/src/components/ble/NimbleController.cpp b/src/components/ble/NimbleController.cpp index 74c8f926..5059007a 100644 --- a/src/components/ble/NimbleController.cpp +++ b/src/components/ble/NimbleController.cpp @@ -42,13 +42,12 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask, anService {systemTask, notificationManager}, alertNotificationClient {systemTask, notificationManager}, currentTimeService {dateTimeController}, - musicService {systemTask}, - weatherService {systemTask, dateTimeController}, - navService {systemTask}, + musicService {*this}, + weatherService {dateTimeController}, batteryInformationService {batteryController}, immediateAlertService {systemTask, notificationManager}, - heartRateService {systemTask, heartRateController}, - motionService {systemTask, motionController}, + heartRateService {*this, heartRateController}, + motionService {*this, motionController}, fsService {systemTask, fs}, serviceDiscovery({¤tTimeClient, &alertNotificationClient}) { } @@ -159,7 +158,10 @@ void NimbleController::StartAdvertising() { } fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; - fields.uuids128 = &dfuServiceUuid; + fields.uuids16 = &HeartRateService::heartRateServiceUuid; + fields.num_uuids16 = 1; + fields.uuids16_is_complete = 1; + fields.uuids128 = &DfuService::serviceUuid; fields.num_uuids128 = 1; fields.uuids128_is_complete = 1; fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; @@ -452,9 +454,15 @@ void NimbleController::PersistBond(struct ble_gap_conn_desc& desc) { /* Wakeup Spi and SpiNorFlash before accessing the file system * This should be fixed in the FS driver */ - systemTask.PushMessage(Pinetime::System::Messages::GoToRunning); systemTask.PushMessage(Pinetime::System::Messages::DisableSleeping); - vTaskDelay(10); + + // This isn't quite correct + // SystemTask could receive EnableSleeping right after passing this check + // We need some guarantee that the SystemTask has processed the above message + // before we can continue + while (!systemTask.IsSleepDisabled()) { + vTaskDelay(pdMS_TO_TICKS(5)); + } lfs_file_t file_p; diff --git a/src/components/ble/NimbleController.h b/src/components/ble/NimbleController.h index 8f1dfed7..597ef0cc 100644 --- a/src/components/ble/NimbleController.h +++ b/src/components/ble/NimbleController.h @@ -21,7 +21,7 @@ #include "components/ble/NavigationService.h" #include "components/ble/ServiceDiscovery.h" #include "components/ble/MotionService.h" -#include "components/ble/weather/WeatherService.h" +#include "components/ble/SimpleWeatherService.h" #include "components/fs/FS.h" namespace Pinetime { @@ -67,7 +67,7 @@ namespace Pinetime { return anService; }; - Pinetime::Controllers::WeatherService& weather() { + Pinetime::Controllers::SimpleWeatherService& weather() { return weatherService; }; @@ -99,7 +99,7 @@ namespace Pinetime { AlertNotificationClient alertNotificationClient; CurrentTimeService currentTimeService; MusicService musicService; - WeatherService weatherService; + SimpleWeatherService weatherService; NavigationService navService; BatteryInformationService batteryInformationService; ImmediateAlertService immediateAlertService; @@ -112,10 +112,6 @@ namespace Pinetime { uint16_t connectionHandle = BLE_HS_CONN_HANDLE_NONE; uint8_t fastAdvCount = 0; uint8_t bondId[16] = {0}; - - ble_uuid128_t dfuServiceUuid { - .u {.type = BLE_UUID_TYPE_128}, - .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x30, 0x15, 0x00, 0x00}}; }; static NimbleController* nptr; diff --git a/src/components/ble/NotificationManager.h b/src/components/ble/NotificationManager.h index f68a68af..57a9c715 100644 --- a/src/components/ble/NotificationManager.h +++ b/src/components/ble/NotificationManager.h @@ -27,11 +27,12 @@ namespace Pinetime { struct Notification { using Id = uint8_t; using Idx = uint8_t; - Id id = 0; - bool valid = false; + + std::array<char, MessageSize + 1> message{}; uint8_t size; - std::array<char, MessageSize + 1> message {}; Categories category = Categories::Unknown; + Id id = 0; + bool valid = false; const char* Message() const; const char* Title() const; diff --git a/src/components/ble/SimpleWeatherService.cpp b/src/components/ble/SimpleWeatherService.cpp new file mode 100644 index 00000000..51baf543 --- /dev/null +++ b/src/components/ble/SimpleWeatherService.cpp @@ -0,0 +1,173 @@ +/* Copyright (C) 2023 Jean-François Milants + + This file is part of InfiniTime. + + InfiniTime is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + InfiniTime is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include "components/ble/SimpleWeatherService.h" + +#include <algorithm> +#include <array> +#include <cstring> +#include <nrf_log.h> + +using namespace Pinetime::Controllers; + +namespace { + enum class MessageType : uint8_t { CurrentWeather, Forecast, Unknown }; + + uint64_t ToUInt64(const uint8_t* data) { + return data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24) + (static_cast<uint64_t>(data[4]) << 32) + + (static_cast<uint64_t>(data[5]) << 40) + (static_cast<uint64_t>(data[6]) << 48) + (static_cast<uint64_t>(data[7]) << 56); + } + + int16_t ToInt16(const uint8_t* data) { + return data[0] + (data[1] << 8); + } + + SimpleWeatherService::CurrentWeather CreateCurrentWeather(const uint8_t* dataBuffer) { + SimpleWeatherService::Location cityName; + std::memcpy(cityName.data(), &dataBuffer[16], 32); + cityName[32] = '\0'; + return SimpleWeatherService::CurrentWeather(ToUInt64(&dataBuffer[2]), + SimpleWeatherService::Temperature(ToInt16(&dataBuffer[10])), + SimpleWeatherService::Temperature(ToInt16(&dataBuffer[12])), + SimpleWeatherService::Temperature(ToInt16(&dataBuffer[14])), + SimpleWeatherService::Icons {dataBuffer[16 + 32]}, + std::move(cityName)); + } + + SimpleWeatherService::Forecast CreateForecast(const uint8_t* dataBuffer) { + auto timestamp = static_cast<uint64_t>(ToUInt64(&dataBuffer[2])); + + std::array<std::optional<SimpleWeatherService::Forecast::Day>, SimpleWeatherService::MaxNbForecastDays> days; + const uint8_t nbDaysInBuffer = dataBuffer[10]; + const uint8_t nbDays = std::min(SimpleWeatherService::MaxNbForecastDays, nbDaysInBuffer); + for (int i = 0; i < nbDays; i++) { + days[i] = SimpleWeatherService::Forecast::Day {SimpleWeatherService::Temperature(ToInt16(&dataBuffer[11 + (i * 5)])), + SimpleWeatherService::Temperature(ToInt16(&dataBuffer[13 + (i * 5)])), + SimpleWeatherService::Icons {dataBuffer[15 + (i * 5)]}}; + } + return SimpleWeatherService::Forecast {timestamp, nbDays, days}; + } + + MessageType GetMessageType(const uint8_t* data) { + auto messageType = static_cast<MessageType>(*data); + if (messageType > MessageType::Unknown) { + return MessageType::Unknown; + } + return messageType; + } + + uint8_t GetVersion(const uint8_t* dataBuffer) { + return dataBuffer[1]; + } +} + +int WeatherCallback(uint16_t /*connHandle*/, uint16_t /*attrHandle*/, struct ble_gatt_access_ctxt* ctxt, void* arg) { + return static_cast<Pinetime::Controllers::SimpleWeatherService*>(arg)->OnCommand(ctxt); +} + +SimpleWeatherService::SimpleWeatherService(DateTime& dateTimeController) : dateTimeController(dateTimeController) { +} + +void SimpleWeatherService::Init() { + ble_gatts_count_cfg(serviceDefinition); + ble_gatts_add_svcs(serviceDefinition); +} + +int SimpleWeatherService::OnCommand(struct ble_gatt_access_ctxt* ctxt) { + const auto* buffer = ctxt->om; + const auto* dataBuffer = buffer->om_data; + + switch (GetMessageType(dataBuffer)) { + case MessageType::CurrentWeather: + if (GetVersion(dataBuffer) == 0) { + currentWeather = CreateCurrentWeather(dataBuffer); + NRF_LOG_INFO("Current weather :\n\tTimestamp : %d\n\tTemperature:%d\n\tMin:%d\n\tMax:%d\n\tIcon:%d\n\tLocation:%s", + currentWeather->timestamp, + currentWeather->temperature.PreciseCelsius(), + currentWeather->minTemperature.PreciseCelsius(), + currentWeather->maxTemperature.PreciseCelsius(), + currentWeather->iconId, + currentWeather->location.data()); + } + break; + case MessageType::Forecast: + if (GetVersion(dataBuffer) == 0) { + forecast = CreateForecast(dataBuffer); + NRF_LOG_INFO("Forecast : Timestamp : %d", forecast->timestamp); + for (int i = 0; i < 5; i++) { + NRF_LOG_INFO("\t[%d] Min: %d - Max : %d - Icon : %d", + i, + forecast->days[i]->minTemperature.PreciseCelsius(), + forecast->days[i]->maxTemperature.PreciseCelsius(), + forecast->days[i]->iconId); + } + } + break; + default: + break; + } + + return 0; +} + +std::optional<SimpleWeatherService::CurrentWeather> SimpleWeatherService::Current() const { + if (currentWeather) { + auto currentTime = dateTimeController.CurrentDateTime().time_since_epoch(); + auto weatherTpSecond = std::chrono::seconds {currentWeather->timestamp}; + auto weatherTp = std::chrono::duration_cast<std::chrono::seconds>(weatherTpSecond); + auto delta = currentTime - weatherTp; + + if (delta < std::chrono::hours {24}) { + return currentWeather; + } + } + return {}; +} + +std::optional<SimpleWeatherService::Forecast> SimpleWeatherService::GetForecast() const { + if (forecast) { + auto currentTime = dateTimeController.CurrentDateTime().time_since_epoch(); + auto weatherTpSecond = std::chrono::seconds {forecast->timestamp}; + auto weatherTp = std::chrono::duration_cast<std::chrono::seconds>(weatherTpSecond); + auto delta = currentTime - weatherTp; + + if (delta < std::chrono::hours {24}) { + return this->forecast; + } + } + return {}; +} + +bool SimpleWeatherService::CurrentWeather::operator==(const SimpleWeatherService::CurrentWeather& other) const { + return this->iconId == other.iconId && this->temperature == other.temperature && this->timestamp == other.timestamp && + this->maxTemperature == other.maxTemperature && this->minTemperature == other.maxTemperature && + std::strcmp(this->location.data(), other.location.data()) == 0; +} + +bool SimpleWeatherService::Forecast::Day::operator==(const SimpleWeatherService::Forecast::Day& other) const { + return this->iconId == other.iconId && this->maxTemperature == other.maxTemperature && this->minTemperature == other.maxTemperature; +} + +bool SimpleWeatherService::Forecast::operator==(const SimpleWeatherService::Forecast& other) const { + for (int i = 0; i < this->nbDays; i++) { + if (this->days[i] != other.days[i]) { + return false; + } + } + return this->timestamp == other.timestamp && this->nbDays == other.nbDays; +} diff --git a/src/components/ble/SimpleWeatherService.h b/src/components/ble/SimpleWeatherService.h new file mode 100644 index 00000000..0f8c181b --- /dev/null +++ b/src/components/ble/SimpleWeatherService.h @@ -0,0 +1,174 @@ +/* Copyright (C) 2023 Jean-François Milants + + This file is part of InfiniTime. + + InfiniTime is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + InfiniTime is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <cstdint> +#include <string> +#include <array> +#include <memory> + +#define min // workaround: nimble's min/max macros conflict with libstdc++ +#define max +#include <host/ble_gap.h> +#include <host/ble_uuid.h> +#include <optional> +#include <cstring> +#undef max +#undef min + +#include "components/datetime/DateTimeController.h" + +int WeatherCallback(uint16_t connHandle, uint16_t attrHandle, struct ble_gatt_access_ctxt* ctxt, void* arg); + +namespace Pinetime { + namespace Controllers { + + class SimpleWeatherService { + public: + explicit SimpleWeatherService(DateTime& dateTimeController); + + void Init(); + + int OnCommand(struct ble_gatt_access_ctxt* ctxt); + + static constexpr uint8_t MaxNbForecastDays = 5; + + enum class Icons : uint8_t { + Sun = 0, // ClearSky + CloudsSun = 1, // FewClouds + Clouds = 2, // Scattered clouds + BrokenClouds = 3, + CloudShowerHeavy = 4, // shower rain + CloudSunRain = 5, // rain + Thunderstorm = 6, + Snow = 7, + Smog = 8, // Mist + Unknown = 255 + }; + + class Temperature { + public: + explicit Temperature(int16_t raw) : raw {raw} { + } + + [[nodiscard]] int16_t PreciseCelsius() const { + return raw; + } + + [[nodiscard]] int16_t PreciseFahrenheit() const { + return raw * 9 / 5 + 3200; + } + + [[nodiscard]] int16_t Celsius() const { + return (PreciseCelsius() + 50) / 100; + } + + [[nodiscard]] int16_t Fahrenheit() const { + return (PreciseFahrenheit() + 50) / 100; + } + + bool operator==(const Temperature& other) const { + return raw == other.raw; + } + + private: + int16_t raw; + }; + + using Location = std::array<char, 33>; // 32 char + \0 (end of string) + + struct CurrentWeather { + CurrentWeather(uint64_t timestamp, + Temperature temperature, + Temperature minTemperature, + Temperature maxTemperature, + Icons iconId, + Location&& location) + : timestamp {timestamp}, + temperature {temperature}, + minTemperature {minTemperature}, + maxTemperature {maxTemperature}, + iconId {iconId}, + location {std::move(location)} { + } + + uint64_t timestamp; + Temperature temperature; + Temperature minTemperature; + Temperature maxTemperature; + Icons iconId; + Location location; + + bool operator==(const CurrentWeather& other) const; + }; + + struct Forecast { + uint64_t timestamp; + uint8_t nbDays; + + struct Day { + Temperature minTemperature; + Temperature maxTemperature; + Icons iconId; + + bool operator==(const Day& other) const; + }; + + std::array<std::optional<Day>, MaxNbForecastDays> days; + + bool operator==(const Forecast& other) const; + }; + + std::optional<CurrentWeather> Current() const; + std::optional<Forecast> GetForecast() const; + + private: + // 00050000-78fc-48fe-8e23-433b3a1942d0 + static constexpr ble_uuid128_t BaseUuid() { + return CharUuid(0x00, 0x00); + } + + // 0005yyxx-78fc-48fe-8e23-433b3a1942d0 + static constexpr ble_uuid128_t CharUuid(uint8_t x, uint8_t y) { + return ble_uuid128_t {.u = {.type = BLE_UUID_TYPE_128}, + .value = {0xd0, 0x42, 0x19, 0x3a, 0x3b, 0x43, 0x23, 0x8e, 0xfe, 0x48, 0xfc, 0x78, y, x, 0x05, 0x00}}; + } + + ble_uuid128_t weatherUuid {BaseUuid()}; + + ble_uuid128_t weatherDataCharUuid {CharUuid(0x00, 0x01)}; + + const struct ble_gatt_chr_def characteristicDefinition[2] = {{.uuid = &weatherDataCharUuid.u, + .access_cb = WeatherCallback, + .arg = this, + .flags = BLE_GATT_CHR_F_WRITE, + .val_handle = &eventHandle}, + {0}}; + const struct ble_gatt_svc_def serviceDefinition[2] = { + {.type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &weatherUuid.u, .characteristics = characteristicDefinition}, + {0}}; + + uint16_t eventHandle {}; + + Pinetime::Controllers::DateTime& dateTimeController; + + std::optional<CurrentWeather> currentWeather; + std::optional<Forecast> forecast; + }; + } +} diff --git a/src/components/ble/weather/WeatherData.h b/src/components/ble/weather/WeatherData.h deleted file mode 100644 index 1a995eb9..00000000 --- a/src/components/ble/weather/WeatherData.h +++ /dev/null @@ -1,385 +0,0 @@ -/* Copyright (C) 2021 Avamander - - This file is part of InfiniTime. - - InfiniTime is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - InfiniTime is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ -#pragma once - -/** - * Different weather events, weather data structures used by {@link WeatherService.h} - * - * How to upload events to the timeline? - * - * All timeline write payloads are simply CBOR-encoded payloads of the structs described below. - * - * All payloads have a mandatory header part and the dynamic part that - * depends on the event type specified in the header. If you don't, - * you'll get an error returned. Data is relatively well-validated, - * so keep in the bounds of the data types given. - * - * Write all struct members (CamelCase keys) into a single finite-sized map, and write it to the characteristic. - * Mind the MTU. - * - * How to debug? - * - * There's a Screen that you can compile into your firmware that shows currently valid events. - * You can adapt that to display something else. That part right now is very much work in progress - * because the exact requirements are not yet known. - * - * - * Implemented based on and other material: - * https://en.wikipedia.org/wiki/METAR - * https://www.weather.gov/jetstream/obscurationtypes - * http://www.faraim.org/aim/aim-4-03-14-493.html - */ - -namespace Pinetime { - namespace Controllers { - class WeatherData { - public: - /** - * Visibility obscuration types - */ - enum class obscurationtype { - /** No obscuration */ - None = 0, - /** Water particles suspended in the air; low visibility; does not fall */ - Fog = 1, - /** Tiny, dry particles in the air; invisible to the eye; opalescent */ - Haze = 2, - /** Small fire-created particles suspended in the air */ - Smoke = 3, - /** Fine rock powder, from for example volcanoes */ - Ash = 4, - /** Fine particles of earth suspended in the air by the wind */ - Dust = 5, - /** Fine particles of sand suspended in the air by the wind */ - Sand = 6, - /** Water particles suspended in the air; low-ish visibility; temperature is near dewpoint */ - Mist = 7, - /** This is SPECIAL in the sense that the thing raining down is doing the obscuration */ - Precipitation = 8, - Length - }; - - /** - * Types of precipitation - */ - enum class precipitationtype { - /** - * No precipitation - * - * Theoretically we could just _not_ send the event, but then - * how do we differentiate between no precipitation and - * no information about precipitation - */ - None = 0, - /** Drops larger than a drizzle; also widely separated drizzle */ - Rain = 1, - /** Fairly uniform rain consisting of fine drops */ - Drizzle = 2, - /** Rain that freezes upon contact with objects and ground */ - FreezingRain = 3, - /** Rain + hail; ice pellets; small translucent frozen raindrops */ - Sleet = 4, - /** Larger ice pellets; falling separately or in irregular clumps */ - Hail = 5, - /** Hail with smaller grains of ice; mini-snowballs */ - SmallHail = 6, - /** Snow... */ - Snow = 7, - /** Frozen drizzle; very small snow crystals */ - SnowGrains = 8, - /** Needles; columns or plates of ice. Sometimes described as "diamond dust". In very cold regions */ - IceCrystals = 9, - /** It's raining down ash, e.g. from a volcano */ - Ash = 10, - Length - }; - - /** - * These are special events that can "enhance" the "experience" of existing weather events - */ - enum class specialtype { - /** Strong wind with a sudden onset that lasts at least a minute */ - Squall = 0, - /** Series of waves in a water body caused by the displacement of a large volume of water */ - Tsunami = 1, - /** Violent; rotating column of air */ - Tornado = 2, - /** Unplanned; unwanted; uncontrolled fire in an area */ - Fire = 3, - /** Thunder and/or lightning */ - Thunder = 4, - Length - }; - - /** - * These are used for weather timeline manipulation - * that isn't just adding to the stack of weather events - */ - enum class controlcodes { - /** How much is stored already */ - GetLength = 0, - /** This wipes the entire timeline */ - DelTimeline = 1, - /** There's a currently valid timeline event with the given type */ - HasValidEvent = 3, - Length - }; - - /** - * Events have types - * then they're easier to parse after sending them over the air - */ - enum class eventtype : uint8_t { - /** @see obscuration */ - Obscuration = 0, - /** @see precipitation */ - Precipitation = 1, - /** @see wind */ - Wind = 2, - /** @see temperature */ - Temperature = 3, - /** @see airquality */ - AirQuality = 4, - /** @see special */ - Special = 5, - /** @see pressure */ - Pressure = 6, - /** @see location */ - Location = 7, - /** @see cloud */ - Clouds = 8, - /** @see humidity */ - Humidity = 9, - Length - }; - - /** - * Valid event query - * - * NOTE: Not currently available, until needs are better known - */ - class ValidEventQuery { - public: - static constexpr controlcodes code = controlcodes::HasValidEvent; - eventtype eventType; - }; - - /** The header used for further parsing */ - class TimelineHeader { - public: - /** - * UNIX timestamp - * TODO: This is currently WITH A TIMEZONE OFFSET! - * Please send events with the timestamp offset by the timezone. - **/ - uint64_t timestamp; - /** - * Time in seconds until the event expires - * - * 32 bits ought to be enough for everyone - * - * If there's a newer event of the same type then it overrides this one, even if it hasn't expired - */ - uint32_t expires; - /** - * What type of weather-related event - */ - eventtype eventType; - }; - - /** Specifies how cloudiness is stored */ - class Clouds : public TimelineHeader { - public: - /** Cloud coverage in percentage, 0-100% */ - uint8_t amount; - }; - - /** Specifies how obscuration is stored */ - class Obscuration : public TimelineHeader { - public: - /** Type of precipitation */ - obscurationtype type; - /** - * Visibility distance in meters - * 65535 is reserved for unspecified - */ - uint16_t amount; - }; - - /** Specifies how precipitation is stored */ - class Precipitation : public TimelineHeader { - public: - /** Type of precipitation */ - precipitationtype type; - /** - * How much is it going to rain? In millimeters - * 255 is reserved for unspecified - **/ - uint8_t amount; - }; - - /** - * How wind speed is stored - * - * In order to represent bursts of wind instead of constant wind, - * you have minimum and maximum speeds. - * - * As direction can fluctuate wildly and some watch faces might wish to display it nicely, - * we're following the aerospace industry weather report option of specifying a range. - */ - class Wind : public TimelineHeader { - public: - /** Meters per second */ - uint8_t speedMin; - /** Meters per second */ - uint8_t speedMax; - /** Unitless direction between 0-255; approximately 1 unit per 0.71 degrees */ - uint8_t directionMin; - /** Unitless direction between 0-255; approximately 1 unit per 0.71 degrees */ - uint8_t directionMax; - }; - - /** - * How temperature is stored - * - * As it's annoying to figure out the dewpoint on the watch, - * please send it from the companion - * - * We don't do floats, picodegrees are not useful. Make sure to multiply. - */ - class Temperature : public TimelineHeader { - public: - /** - * Temperature °C but multiplied by 100 (e.g. -12.50°C becomes -1250) - * -32768 is reserved for "no data" - */ - int16_t temperature; - /** - * Dewpoint °C but multiplied by 100 (e.g. -12.50°C becomes -1250) - * -32768 is reserved for "no data" - */ - int16_t dewPoint; - }; - - /** - * How location info is stored - * - * This can be mostly static with long expiration, - * as it usually is, but it could change during a trip for ex. - * so we allow changing it dynamically. - * - * Location info can be for some kind of map watch face - * or daylight calculations, should those be required. - * - */ - class Location : public TimelineHeader { - public: - /** Location name */ - std::string location; - /** Altitude relative to sea level in meters */ - int16_t altitude; - /** Latitude, EPSG:3857 (Google Maps, Openstreetmaps datum) */ - int32_t latitude; - /** Longitude, EPSG:3857 (Google Maps, Openstreetmaps datum) */ - int32_t longitude; - }; - - /** - * How humidity is stored - */ - class Humidity : public TimelineHeader { - public: - /** Relative humidity, 0-100% */ - uint8_t humidity; - }; - - /** - * How air pressure is stored - */ - class Pressure : public TimelineHeader { - public: - /** Air pressure in hectopascals (hPa) */ - int16_t pressure; - }; - - /** - * How special events are stored - */ - class Special : public TimelineHeader { - public: - /** Special event's type */ - specialtype type; - }; - - /** - * How air quality is stored - * - * These events are a bit more complex because the topic is not simple, - * the intention is to heavy-lift the annoying preprocessing from the watch - * this allows watch face or watchapp makers to generate accurate alerts and graphics - * - * If this needs further enforced standardization, pull requests are welcome - */ - class AirQuality : public TimelineHeader { - public: - /** - * The name of the pollution - * - * for the sake of better compatibility with watchapps - * that might want to use this data for say visuals - * don't localize the name. - * - * Ideally watchapp itself localizes the name, if it's at all needed. - * - * E.g. - * For generic ones use "PM0.1", "PM5", "PM10" - * For chemical compounds use the molecular formula e.g. "NO2", "CO2", "O3" - * For pollen use the genus, e.g. "Betula" for birch or "Alternaria" for that mold's spores - */ - std::string polluter; - /** - * Amount of the pollution in SI units, - * otherwise it's going to be difficult to create UI, alerts - * and so on and for. - * - * See more: - * https://ec.europa.eu/environment/air/quality/standards.htm - * http://www.ourair.org/wp-content/uploads/2012-aaqs2.pdf - * - * Example units: - * count/m³ for pollen - * µgC/m³ for micrograms of organic carbon - * µg/m³ sulfates, PM0.1, PM1, PM2, PM10 and so on, dust - * mg/m³ CO2, CO - * ng/m³ for heavy metals - * - * List is not comprehensive, should be improved. - * The current ones are what watchapps assume! - * - * Note: ppb and ppm to concentration should be calculated on the companion, using - * the correct formula (taking into account temperature and air pressure) - * - * Note2: The amount is off by times 100, for two decimal places of precision. - * E.g. 54.32µg/m³ is 5432 - * - */ - uint32_t amount; - }; - }; - } -} diff --git a/src/components/ble/weather/WeatherService.cpp b/src/components/ble/weather/WeatherService.cpp deleted file mode 100644 index fd13f819..00000000 --- a/src/components/ble/weather/WeatherService.cpp +++ /dev/null @@ -1,607 +0,0 @@ -/* Copyright (C) 2021 Avamander - - This file is part of InfiniTime. - - InfiniTime is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - InfiniTime is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ -#include <algorithm> -#include <qcbor/qcbor_spiffy_decode.h> -#include "WeatherService.h" -#include "libs/QCBOR/inc/qcbor/qcbor.h" -#include "systemtask/SystemTask.h" - -int WeatherCallback(uint16_t /*connHandle*/, uint16_t /*attrHandle*/, struct ble_gatt_access_ctxt* ctxt, void* arg) { - return static_cast<Pinetime::Controllers::WeatherService*>(arg)->OnCommand(ctxt); -} - -namespace Pinetime { - namespace Controllers { - WeatherService::WeatherService(System::SystemTask& system, DateTime& dateTimeController) - : system(system), dateTimeController(dateTimeController) { - nullHeader = &nullTimelineheader; - nullTimelineheader->timestamp = 0; - } - - void WeatherService::Init() { - uint8_t res = 0; - res = ble_gatts_count_cfg(serviceDefinition); - ASSERT(res == 0); - - res = ble_gatts_add_svcs(serviceDefinition); - ASSERT(res == 0); - } - - int WeatherService::OnCommand(struct ble_gatt_access_ctxt* ctxt) { - if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { - const uint8_t packetLen = OS_MBUF_PKTLEN(ctxt->om); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) - if (packetLen <= 0) { - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - // Decode - QCBORDecodeContext decodeContext; - UsefulBufC encodedCbor = {ctxt->om->om_data, OS_MBUF_PKTLEN(ctxt->om)}; // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) - - QCBORDecode_Init(&decodeContext, encodedCbor, QCBOR_DECODE_MODE_NORMAL); - // KINDLY provide us a fixed-length map - QCBORDecode_EnterMap(&decodeContext, nullptr); - // Always encodes to the smallest number of bytes based on the value - int64_t tmpTimestamp = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Timestamp", &tmpTimestamp); - if (QCBORDecode_GetError(&decodeContext) != QCBOR_SUCCESS) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - int64_t tmpExpires = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Expires", &tmpExpires); - if (QCBORDecode_GetError(&decodeContext) != QCBOR_SUCCESS || tmpExpires < 0 || tmpExpires > 4294967295) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - int64_t tmpEventType = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "EventType", &tmpEventType); - if (QCBORDecode_GetError(&decodeContext) != QCBOR_SUCCESS || tmpEventType < 0 || - tmpEventType >= static_cast<int64_t>(WeatherData::eventtype::Length)) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - - switch (static_cast<WeatherData::eventtype>(tmpEventType)) { - case WeatherData::eventtype::AirQuality: { - std::unique_ptr<WeatherData::AirQuality> airquality = std::make_unique<WeatherData::AirQuality>(); - airquality->timestamp = tmpTimestamp; - airquality->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - airquality->expires = tmpExpires; - - UsefulBufC stringBuf; // TODO: Everything ok with lifecycle here? - QCBORDecode_GetTextStringInMapSZ(&decodeContext, "Polluter", &stringBuf); - if (UsefulBuf_IsNULLOrEmptyC(stringBuf) != 0) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - airquality->polluter = std::string(static_cast<const char*>(stringBuf.ptr), stringBuf.len); - - int64_t tmpAmount = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount); - if (tmpAmount < 0 || tmpAmount > 4294967295) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - airquality->amount = tmpAmount; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - if (!AddEventToTimeline(std::move(airquality))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Obscuration: { - std::unique_ptr<WeatherData::Obscuration> obscuration = std::make_unique<WeatherData::Obscuration>(); - obscuration->timestamp = tmpTimestamp; - obscuration->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - obscuration->expires = tmpExpires; - - int64_t tmpType = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Type", &tmpType); - if (tmpType < 0 || tmpType >= static_cast<int64_t>(WeatherData::obscurationtype::Length)) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - obscuration->type = static_cast<WeatherData::obscurationtype>(tmpType); - - int64_t tmpAmount = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount); - if (tmpAmount < 0 || tmpAmount > 65535) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - obscuration->amount = tmpAmount; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - if (!AddEventToTimeline(std::move(obscuration))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Precipitation: { - std::unique_ptr<WeatherData::Precipitation> precipitation = std::make_unique<WeatherData::Precipitation>(); - precipitation->timestamp = tmpTimestamp; - precipitation->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - precipitation->expires = tmpExpires; - - int64_t tmpType = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Type", &tmpType); - if (tmpType < 0 || tmpType >= static_cast<int64_t>(WeatherData::precipitationtype::Length)) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - precipitation->type = static_cast<WeatherData::precipitationtype>(tmpType); - - int64_t tmpAmount = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount); - if (tmpAmount < 0 || tmpAmount > 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - precipitation->amount = tmpAmount; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - if (!AddEventToTimeline(std::move(precipitation))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Wind: { - std::unique_ptr<WeatherData::Wind> wind = std::make_unique<WeatherData::Wind>(); - wind->timestamp = tmpTimestamp; - wind->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - wind->expires = tmpExpires; - - int64_t tmpMin = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "SpeedMin", &tmpMin); - if (tmpMin < 0 || tmpMin > 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - wind->speedMin = tmpMin; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - int64_t tmpMax = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "SpeedMin", &tmpMax); - if (tmpMax < 0 || tmpMax > 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - wind->speedMax = tmpMax; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - int64_t tmpDMin = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "DirectionMin", &tmpDMin); - if (tmpDMin < 0 || tmpDMin > 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - wind->directionMin = tmpDMin; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - int64_t tmpDMax = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "DirectionMax", &tmpDMax); - if (tmpDMax < 0 || tmpDMax > 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - wind->directionMax = tmpDMax; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - if (!AddEventToTimeline(std::move(wind))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Temperature: { - std::unique_ptr<WeatherData::Temperature> temperature = std::make_unique<WeatherData::Temperature>(); - temperature->timestamp = tmpTimestamp; - temperature->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - temperature->expires = tmpExpires; - - int64_t tmpTemperature = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Temperature", &tmpTemperature); - if (tmpTemperature < -32768 || tmpTemperature > 32767) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - temperature->temperature = - static_cast<int16_t>(tmpTemperature); // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - int64_t tmpDewPoint = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "DewPoint", &tmpDewPoint); - if (tmpDewPoint < -32768 || tmpDewPoint > 32767) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - temperature->dewPoint = - static_cast<int16_t>(tmpDewPoint); // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - if (!AddEventToTimeline(std::move(temperature))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Special: { - std::unique_ptr<WeatherData::Special> special = std::make_unique<WeatherData::Special>(); - special->timestamp = tmpTimestamp; - special->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - special->expires = tmpExpires; - - int64_t tmpType = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Type", &tmpType); - if (tmpType < 0 || tmpType >= static_cast<int64_t>(WeatherData::specialtype::Length)) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - special->type = static_cast<WeatherData::specialtype>(tmpType); - - if (!AddEventToTimeline(std::move(special))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Pressure: { - std::unique_ptr<WeatherData::Pressure> pressure = std::make_unique<WeatherData::Pressure>(); - pressure->timestamp = tmpTimestamp; - pressure->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - pressure->expires = tmpExpires; - - int64_t tmpPressure = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Pressure", &tmpPressure); - if (tmpPressure < 0 || tmpPressure >= 65535) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - pressure->pressure = tmpPressure; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - - if (!AddEventToTimeline(std::move(pressure))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Location: { - std::unique_ptr<WeatherData::Location> location = std::make_unique<WeatherData::Location>(); - location->timestamp = tmpTimestamp; - location->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - location->expires = tmpExpires; - - UsefulBufC stringBuf; // TODO: Everything ok with lifecycle here? - QCBORDecode_GetTextStringInMapSZ(&decodeContext, "Location", &stringBuf); - if (UsefulBuf_IsNULLOrEmptyC(stringBuf) != 0) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - location->location = std::string(static_cast<const char*>(stringBuf.ptr), stringBuf.len); - - int64_t tmpAltitude = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Altitude", &tmpAltitude); - if (tmpAltitude < -32768 || tmpAltitude >= 32767) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - location->altitude = static_cast<int16_t>(tmpAltitude); - - int64_t tmpLatitude = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Latitude", &tmpLatitude); - if (tmpLatitude < -2147483648 || tmpLatitude >= 2147483647) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - location->latitude = static_cast<int32_t>(tmpLatitude); - - int64_t tmpLongitude = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Longitude", &tmpLongitude); - if (tmpLongitude < -2147483648 || tmpLongitude >= 2147483647) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - location->latitude = static_cast<int32_t>(tmpLongitude); - - if (!AddEventToTimeline(std::move(location))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Clouds: { - std::unique_ptr<WeatherData::Clouds> clouds = std::make_unique<WeatherData::Clouds>(); - clouds->timestamp = tmpTimestamp; - clouds->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - clouds->expires = tmpExpires; - - int64_t tmpAmount = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount); - if (tmpAmount < 0 || tmpAmount > 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - clouds->amount = static_cast<uint8_t>(tmpAmount); - - if (!AddEventToTimeline(std::move(clouds))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - case WeatherData::eventtype::Humidity: { - std::unique_ptr<WeatherData::Humidity> humidity = std::make_unique<WeatherData::Humidity>(); - humidity->timestamp = tmpTimestamp; - humidity->eventType = static_cast<WeatherData::eventtype>(tmpEventType); - humidity->expires = tmpExpires; - - int64_t tmpType = 0; - QCBORDecode_GetInt64InMapSZ(&decodeContext, "Humidity", &tmpType); - if (tmpType < 0 || tmpType >= 255) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - humidity->humidity = static_cast<uint8_t>(tmpType); - - if (!AddEventToTimeline(std::move(humidity))) { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - break; - } - default: { - CleanUpQcbor(&decodeContext); - return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; - } - } - - QCBORDecode_ExitMap(&decodeContext); - GetTimelineLength(); - TidyTimeline(); - - if (QCBORDecode_Finish(&decodeContext) != QCBOR_SUCCESS) { - return BLE_ATT_ERR_INSUFFICIENT_RES; - } - } else if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { - // Encode - uint8_t buffer[64]; - QCBOREncodeContext encodeContext; - /* TODO: This is very much still a test endpoint - * it needs a characteristic UUID check - * and actual implementations that show - * what actually has to be read. - * WARN: Consider commands not part of the API for now! - */ - QCBOREncode_Init(&encodeContext, UsefulBuf_FROM_BYTE_ARRAY(buffer)); - QCBOREncode_OpenMap(&encodeContext); - QCBOREncode_AddTextToMap(&encodeContext, "test", UsefulBuf_FROM_SZ_LITERAL("test")); - QCBOREncode_AddInt64ToMap(&encodeContext, "test", 1ul); - QCBOREncode_CloseMap(&encodeContext); - - UsefulBufC encodedEvent; - auto uErr = QCBOREncode_Finish(&encodeContext, &encodedEvent); - if (uErr != 0) { - return BLE_ATT_ERR_INSUFFICIENT_RES; - } - auto res = os_mbuf_append(ctxt->om, &buffer, sizeof(buffer)); - if (res == 0) { - return BLE_ATT_ERR_INSUFFICIENT_RES; - } - - return 0; - } - return 0; - } - - std::unique_ptr<WeatherData::Clouds>& WeatherService::GetCurrentClouds() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Clouds && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Clouds>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Clouds>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Obscuration>& WeatherService::GetCurrentObscuration() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Obscuration && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Obscuration>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Obscuration>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Precipitation>& WeatherService::GetCurrentPrecipitation() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Precipitation && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Precipitation>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Precipitation>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Wind>& WeatherService::GetCurrentWind() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Wind && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Wind>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Wind>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Temperature>& WeatherService::GetCurrentTemperature() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Temperature && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Temperature>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Temperature>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Humidity>& WeatherService::GetCurrentHumidity() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Humidity && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Humidity>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Humidity>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Pressure>& WeatherService::GetCurrentPressure() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Pressure && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Pressure>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Pressure>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::Location>& WeatherService::GetCurrentLocation() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Location && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::Location>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::Location>&>(*this->nullHeader); - } - - std::unique_ptr<WeatherData::AirQuality>& WeatherService::GetCurrentQuality() { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::AirQuality && IsEventStillValid(header, currentTimestamp)) { - return reinterpret_cast<std::unique_ptr<WeatherData::AirQuality>&>(header); - } - } - - return reinterpret_cast<std::unique_ptr<WeatherData::AirQuality>&>(*this->nullHeader); - } - - size_t WeatherService::GetTimelineLength() const { - return timeline.size(); - } - - bool WeatherService::AddEventToTimeline(std::unique_ptr<WeatherData::TimelineHeader> event) { - if (timeline.size() == timeline.max_size()) { - return false; - } - - timeline.push_back(std::move(event)); - return true; - } - - bool WeatherService::HasTimelineEventOfType(const WeatherData::eventtype type) const { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - for (auto&& header : timeline) { - if (header->eventType == type && IsEventStillValid(header, currentTimestamp)) { - return true; - } - } - return false; - } - - void WeatherService::TidyTimeline() { - uint64_t timeCurrent = GetCurrentUnixTimestamp(); - timeline.erase(std::remove_if(std::begin(timeline), - std::end(timeline), - [&](std::unique_ptr<WeatherData::TimelineHeader> const& header) { - return !IsEventStillValid(header, timeCurrent); - }), - std::end(timeline)); - - std::sort(std::begin(timeline), std::end(timeline), CompareTimelineEvents); - } - - bool WeatherService::CompareTimelineEvents(const std::unique_ptr<WeatherData::TimelineHeader>& first, - const std::unique_ptr<WeatherData::TimelineHeader>& second) { - return first->timestamp > second->timestamp; - } - - bool WeatherService::IsEventStillValid(const std::unique_ptr<WeatherData::TimelineHeader>& uniquePtr, const uint64_t timestamp) { - // Not getting timestamp in isEventStillValid for more speed - return uniquePtr->timestamp + uniquePtr->expires >= timestamp; - } - - uint64_t WeatherService::GetCurrentUnixTimestamp() const { - return std::chrono::duration_cast<std::chrono::seconds>(dateTimeController.CurrentDateTime().time_since_epoch()).count(); - } - - int16_t WeatherService::GetTodayMinTemp() const { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - uint64_t currentDayEnd = currentTimestamp + ((24 - dateTimeController.Hours()) * 60 * 60) + - ((60 - dateTimeController.Minutes()) * 60) + (60 - dateTimeController.Seconds()); - uint64_t currentDayStart = currentDayEnd - 86400; - int16_t result = -32768; - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Temperature && header->timestamp >= currentDayStart && - header->timestamp < currentDayEnd && - reinterpret_cast<const std::unique_ptr<WeatherData::Temperature>&>(header)->temperature != -32768) { - int16_t temperature = reinterpret_cast<const std::unique_ptr<WeatherData::Temperature>&>(header)->temperature; - if (result == -32768) { - result = temperature; - } else if (result > temperature) { - result = temperature; - } else { - // The temperature in this item is higher than the lowest we've found - } - } - } - - return result; - } - - int16_t WeatherService::GetTodayMaxTemp() const { - uint64_t currentTimestamp = GetCurrentUnixTimestamp(); - uint64_t currentDayEnd = currentTimestamp + ((24 - dateTimeController.Hours()) * 60 * 60) + - ((60 - dateTimeController.Minutes()) * 60) + (60 - dateTimeController.Seconds()); - uint64_t currentDayStart = currentDayEnd - 86400; - int16_t result = -32768; - for (auto&& header : this->timeline) { - if (header->eventType == WeatherData::eventtype::Temperature && header->timestamp >= currentDayStart && - header->timestamp < currentDayEnd && - reinterpret_cast<const std::unique_ptr<WeatherData::Temperature>&>(header)->temperature != -32768) { - int16_t temperature = reinterpret_cast<const std::unique_ptr<WeatherData::Temperature>&>(header)->temperature; - if (result == -32768) { - result = temperature; - } else if (result < temperature) { - result = temperature; - } else { - // The temperature in this item is lower than the highest we've found - } - } - } - - return result; - } - - void WeatherService::CleanUpQcbor(QCBORDecodeContext* decodeContext) { - QCBORDecode_ExitMap(decodeContext); - QCBORDecode_Finish(decodeContext); - } - } -} diff --git a/src/components/ble/weather/WeatherService.h b/src/components/ble/weather/WeatherService.h deleted file mode 100644 index 00650e90..00000000 --- a/src/components/ble/weather/WeatherService.h +++ /dev/null @@ -1,174 +0,0 @@ -/* Copyright (C) 2021 Avamander - - This file is part of InfiniTime. - - InfiniTime is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - InfiniTime is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ -#pragma once - -#include <cstdint> -#include <string> -#include <vector> -#include <memory> - -#define min // workaround: nimble's min/max macros conflict with libstdc++ -#define max -#include <host/ble_gap.h> -#include <host/ble_uuid.h> -#undef max -#undef min - -#include "WeatherData.h" -#include "libs/QCBOR/inc/qcbor/qcbor.h" -#include "components/datetime/DateTimeController.h" - -int WeatherCallback(uint16_t connHandle, uint16_t attrHandle, struct ble_gatt_access_ctxt* ctxt, void* arg); - -namespace Pinetime { - namespace System { - class SystemTask; - } - - namespace Controllers { - - class WeatherService { - public: - explicit WeatherService(System::SystemTask& system, DateTime& dateTimeController); - - void Init(); - - int OnCommand(struct ble_gatt_access_ctxt* ctxt); - - /* - * Helper functions for quick access to currently valid data - */ - std::unique_ptr<WeatherData::Location>& GetCurrentLocation(); - std::unique_ptr<WeatherData::Clouds>& GetCurrentClouds(); - std::unique_ptr<WeatherData::Obscuration>& GetCurrentObscuration(); - std::unique_ptr<WeatherData::Precipitation>& GetCurrentPrecipitation(); - std::unique_ptr<WeatherData::Wind>& GetCurrentWind(); - std::unique_ptr<WeatherData::Temperature>& GetCurrentTemperature(); - std::unique_ptr<WeatherData::Humidity>& GetCurrentHumidity(); - std::unique_ptr<WeatherData::Pressure>& GetCurrentPressure(); - std::unique_ptr<WeatherData::AirQuality>& GetCurrentQuality(); - - /** - * Searches for the current day's maximum temperature - * @return -32768 if there's no data, degrees Celsius times 100 otherwise - */ - int16_t GetTodayMaxTemp() const; - /** - * Searches for the current day's minimum temperature - * @return -32768 if there's no data, degrees Celsius times 100 otherwise - */ - int16_t GetTodayMinTemp() const; - - /* - * Management functions - */ - /** - * Adds an event to the timeline - * @return - */ - bool AddEventToTimeline(std::unique_ptr<WeatherData::TimelineHeader> event); - /** - * Gets the current timeline length - */ - size_t GetTimelineLength() const; - /** - * Checks if an event of a certain type exists in the timeline - */ - bool HasTimelineEventOfType(WeatherData::eventtype type) const; - - private: - // 00040000-78fc-48fe-8e23-433b3a1942d0 - static constexpr ble_uuid128_t BaseUuid() { - return CharUuid(0x00, 0x00); - } - - // 0004yyxx-78fc-48fe-8e23-433b3a1942d0 - static constexpr ble_uuid128_t CharUuid(uint8_t x, uint8_t y) { - return ble_uuid128_t {.u = {.type = BLE_UUID_TYPE_128}, - .value = {0xd0, 0x42, 0x19, 0x3a, 0x3b, 0x43, 0x23, 0x8e, 0xfe, 0x48, 0xfc, 0x78, y, x, 0x04, 0x00}}; - } - - ble_uuid128_t weatherUuid {BaseUuid()}; - - /** - * Just write timeline data here. - * - * See {@link WeatherData.h} for more information. - */ - ble_uuid128_t weatherDataCharUuid {CharUuid(0x00, 0x01)}; - /** - * This doesn't take timeline data, provides some control over it. - * - * NOTE: Currently not supported. Companion app implementer feedback required. - * There's very little point in solidifying an API before we know the needs. - */ - ble_uuid128_t weatherControlCharUuid {CharUuid(0x00, 0x02)}; - - const struct ble_gatt_chr_def characteristicDefinition[3] = { - {.uuid = &weatherDataCharUuid.u, - .access_cb = WeatherCallback, - .arg = this, - .flags = BLE_GATT_CHR_F_WRITE, - .val_handle = &eventHandle}, - {.uuid = &weatherControlCharUuid.u, .access_cb = WeatherCallback, .arg = this, .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_READ}, - {nullptr}}; - const struct ble_gatt_svc_def serviceDefinition[2] = { - {.type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &weatherUuid.u, .characteristics = characteristicDefinition}, - {0}}; - - uint16_t eventHandle {}; - - Pinetime::System::SystemTask& system; - Pinetime::Controllers::DateTime& dateTimeController; - - std::vector<std::unique_ptr<WeatherData::TimelineHeader>> timeline; - std::unique_ptr<WeatherData::TimelineHeader> nullTimelineheader = std::make_unique<WeatherData::TimelineHeader>(); - std::unique_ptr<WeatherData::TimelineHeader>* nullHeader; - - /** - * Cleans up the timeline of expired events - */ - void TidyTimeline(); - - /** - * Compares two timeline events - */ - static bool CompareTimelineEvents(const std::unique_ptr<WeatherData::TimelineHeader>& first, - const std::unique_ptr<WeatherData::TimelineHeader>& second); - - /** - * Returns current UNIX timestamp - */ - uint64_t GetCurrentUnixTimestamp() const; - - /** - * Checks if the event hasn't gone past and expired - * - * @param header timeline event to check - * @param currentTimestamp what's the time right now - * @return if the event is valid - */ - static bool IsEventStillValid(const std::unique_ptr<WeatherData::TimelineHeader>& uniquePtr, const uint64_t timestamp); - - /** - * This is a helper function that closes a QCBOR map and decoding context cleanly - */ - void CleanUpQcbor(QCBORDecodeContext* decodeContext); - }; - } -} diff --git a/src/components/brightness/BrightnessController.cpp b/src/components/brightness/BrightnessController.cpp index 0392158c..4d1eba6a 100644 --- a/src/components/brightness/BrightnessController.cpp +++ b/src/components/brightness/BrightnessController.cpp @@ -2,38 +2,138 @@ #include <hal/nrf_gpio.h> #include "displayapp/screens/Symbols.h" #include "drivers/PinMap.h" +#include <libraries/delay/nrf_delay.h> using namespace Pinetime::Controllers; +namespace { + // reinterpret_cast is not constexpr so this is the best we can do + static NRF_RTC_Type* const RTC = reinterpret_cast<NRF_RTC_Type*>(NRF_RTC2_BASE); +} + void BrightnessController::Init() { nrf_gpio_cfg_output(PinMap::LcdBacklightLow); nrf_gpio_cfg_output(PinMap::LcdBacklightMedium); nrf_gpio_cfg_output(PinMap::LcdBacklightHigh); + + nrf_gpio_pin_clear(PinMap::LcdBacklightLow); + nrf_gpio_pin_clear(PinMap::LcdBacklightMedium); + nrf_gpio_pin_clear(PinMap::LcdBacklightHigh); + + static_assert(timerFrequency == 32768, "Change the prescaler below"); + RTC->PRESCALER = 0; + // CC1 switches the backlight on (pin transitions from high to low) and resets the counter to 0 + RTC->CC[1] = timerPeriod; + // Enable compare events for CC0,CC1 + RTC->EVTEN = 0b0000'0000'0000'0011'0000'0000'0000'0000; + // Disable all interrupts + RTC->INTENCLR = 0b0000'0000'0000'1111'0000'0000'0000'0011; Set(level); } +void BrightnessController::ApplyBrightness(uint16_t rawBrightness) { + // The classic off, low, medium, high brightnesses are at {0, timerPeriod, timerPeriod*2, timerPeriod*3} + // These brightness levels do not use PWM: they only set/clear the corresponding pins + // Any brightness level between the above levels is achieved with efficient RTC based PWM on the next pin up + // E.g 2.5*timerPeriod corresponds to medium brightness with 50% PWM on the high pin + // Note: Raw brightness does not necessarily correspond to a linear perceived brightness + + uint8_t pin; + if (rawBrightness > 2 * timerPeriod) { + rawBrightness -= 2 * timerPeriod; + pin = PinMap::LcdBacklightHigh; + } else if (rawBrightness > timerPeriod) { + rawBrightness -= timerPeriod; + pin = PinMap::LcdBacklightMedium; + } else { + pin = PinMap::LcdBacklightLow; + } + if (rawBrightness == timerPeriod || rawBrightness == 0) { + if (lastPin != UNSET) { + RTC->TASKS_STOP = 1; + nrf_delay_us(rtcStopTime); + nrf_ppi_channel_disable(ppiBacklightOff); + nrf_ppi_channel_disable(ppiBacklightOn); + nrfx_gpiote_out_uninit(lastPin); + nrf_gpio_cfg_output(lastPin); + } + lastPin = UNSET; + if (rawBrightness == 0) { + nrf_gpio_pin_set(pin); + } else { + nrf_gpio_pin_clear(pin); + } + } else { + // If the pin on which we are doing PWM is changing + // Disable old PWM channel (if exists) and set up new one + if (lastPin != pin) { + if (lastPin != UNSET) { + RTC->TASKS_STOP = 1; + nrf_delay_us(rtcStopTime); + nrf_ppi_channel_disable(ppiBacklightOff); + nrf_ppi_channel_disable(ppiBacklightOn); + nrfx_gpiote_out_uninit(lastPin); + nrf_gpio_cfg_output(lastPin); + } + nrfx_gpiote_out_config_t gpioteCfg = {.action = NRF_GPIOTE_POLARITY_TOGGLE, + .init_state = NRF_GPIOTE_INITIAL_VALUE_LOW, + .task_pin = true}; + APP_ERROR_CHECK(nrfx_gpiote_out_init(pin, &gpioteCfg)); + nrfx_gpiote_out_task_enable(pin); + nrf_ppi_channel_endpoint_setup(ppiBacklightOff, + reinterpret_cast<uint32_t>(&RTC->EVENTS_COMPARE[0]), + nrfx_gpiote_out_task_addr_get(pin)); + nrf_ppi_channel_endpoint_setup(ppiBacklightOn, + reinterpret_cast<uint32_t>(&RTC->EVENTS_COMPARE[1]), + nrfx_gpiote_out_task_addr_get(pin)); + nrf_ppi_fork_endpoint_setup(ppiBacklightOn, reinterpret_cast<uint32_t>(&RTC->TASKS_CLEAR)); + nrf_ppi_channel_enable(ppiBacklightOff); + nrf_ppi_channel_enable(ppiBacklightOn); + } else { + // If the pin used for PWM isn't changing, we only need to set the pin state to the initial value (low) + RTC->TASKS_STOP = 1; + nrf_delay_us(rtcStopTime); + // Due to errata 20,179 and the intricacies of RTC timing, keep it simple: override the pin state + nrfx_gpiote_out_task_force(pin, false); + } + // CC0 switches the backlight off (pin transitions from low to high) + RTC->CC[0] = rawBrightness; + RTC->TASKS_CLEAR = 1; + RTC->TASKS_START = 1; + lastPin = pin; + } + switch (pin) { + case PinMap::LcdBacklightHigh: + nrf_gpio_pin_clear(PinMap::LcdBacklightLow); + nrf_gpio_pin_clear(PinMap::LcdBacklightMedium); + break; + case PinMap::LcdBacklightMedium: + nrf_gpio_pin_clear(PinMap::LcdBacklightLow); + nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + break; + case PinMap::LcdBacklightLow: + nrf_gpio_pin_set(PinMap::LcdBacklightMedium); + nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + } +} + void BrightnessController::Set(BrightnessController::Levels level) { this->level = level; switch (level) { default: case Levels::High: - nrf_gpio_pin_clear(PinMap::LcdBacklightLow); - nrf_gpio_pin_clear(PinMap::LcdBacklightMedium); - nrf_gpio_pin_clear(PinMap::LcdBacklightHigh); + ApplyBrightness(3 * timerPeriod); break; case Levels::Medium: - nrf_gpio_pin_clear(PinMap::LcdBacklightLow); - nrf_gpio_pin_clear(PinMap::LcdBacklightMedium); - nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + ApplyBrightness(2 * timerPeriod); break; case Levels::Low: - nrf_gpio_pin_clear(PinMap::LcdBacklightLow); - nrf_gpio_pin_set(PinMap::LcdBacklightMedium); - nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + ApplyBrightness(timerPeriod); + break; + case Levels::AlwaysOn: + ApplyBrightness(timerPeriod / 10); break; case Levels::Off: - nrf_gpio_pin_set(PinMap::LcdBacklightLow); - nrf_gpio_pin_set(PinMap::LcdBacklightMedium); - nrf_gpio_pin_set(PinMap::LcdBacklightHigh); + ApplyBrightness(0); break; } } diff --git a/src/components/brightness/BrightnessController.h b/src/components/brightness/BrightnessController.h index 7f86759a..650749a8 100644 --- a/src/components/brightness/BrightnessController.h +++ b/src/components/brightness/BrightnessController.h @@ -2,11 +2,14 @@ #include <cstdint> +#include "nrf_ppi.h" +#include "nrfx_gpiote.h" + namespace Pinetime { namespace Controllers { class BrightnessController { public: - enum class Levels { Off, Low, Medium, High }; + enum class Levels { Off, AlwaysOn, Low, Medium, High }; void Init(); void Set(Levels level); @@ -20,6 +23,25 @@ namespace Pinetime { private: Levels level = Levels::High; + static constexpr uint8_t UNSET = UINT8_MAX; + uint8_t lastPin = UNSET; + // Maximum time (μs) it takes for the RTC to fully stop + static constexpr uint8_t rtcStopTime = 46; + // Frequency of timer used for PWM (Hz) + static constexpr uint16_t timerFrequency = 32768; + // Backlight PWM frequency (Hz) + static constexpr uint16_t pwmFreq = 1000; + // Wraparound point in timer ticks + // Defines the number of brightness levels between each pin + static constexpr uint16_t timerPeriod = timerFrequency / pwmFreq; + // Warning: nimble reserves some PPIs + // https://github.com/InfiniTimeOrg/InfiniTime/blob/034d83fe6baf1ab3875a34f8cee387e24410a824/src/libs/mynewt-nimble/nimble/drivers/nrf52/src/ble_phy.c#L53 + // SpiMaster uses PPI 0 for an erratum workaround + // Channel 1, 2 should be free to use + static constexpr nrf_ppi_channel_t ppiBacklightOn = NRF_PPI_CHANNEL1; + static constexpr nrf_ppi_channel_t ppiBacklightOff = NRF_PPI_CHANNEL2; + + void ApplyBrightness(uint16_t val); }; } } diff --git a/src/components/datetime/DateTimeController.cpp b/src/components/datetime/DateTimeController.cpp index 9e9fb6e4..d439821b 100644 --- a/src/components/datetime/DateTimeController.cpp +++ b/src/components/datetime/DateTimeController.cpp @@ -1,22 +1,42 @@ #include "components/datetime/DateTimeController.h" #include <libraries/log/nrf_log.h> #include <systemtask/SystemTask.h> +#include <hal/nrf_rtc.h> +#include "nrf_assert.h" using namespace Pinetime::Controllers; namespace { - char const* DaysStringShort[] = {"--", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; - char const* DaysStringShortLow[] = {"--", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; - char const* MonthsString[] = {"--", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; - char const* MonthsStringLow[] = {"--", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + constexpr const char* const DaysStringShort[] = {"--", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; + constexpr const char* const DaysStringShortLow[] = {"--", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; + constexpr const char* const MonthsString[] = {"--", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; + constexpr const char* const MonthsStringLow[] = + {"--", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + + constexpr int compileTimeAtoi(const char* str) { + int result = 0; + while (*str >= '0' && *str <= '9') { + result = result * 10 + *str - '0'; + str++; + } + return result; + } } DateTime::DateTime(Controllers::Settings& settingsController) : settingsController {settingsController} { + mutex = xSemaphoreCreateMutex(); + ASSERT(mutex != nullptr); + xSemaphoreGive(mutex); + + // __DATE__ is a string of the format "MMM DD YYYY", so an offset of 7 gives the start of the year + SetTime(compileTimeAtoi(&__DATE__[7]), 1, 1, 0, 0, 0); } void DateTime::SetCurrentTime(std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> t) { + xSemaphoreTake(mutex, portMAX_DELAY); this->currentDateTime = t; - UpdateTime(previousSystickCounter); // Update internal state without updating the time + UpdateTime(previousSystickCounter, true); // Update internal state without updating the time + xSemaphoreGive(mutex); } void DateTime::SetTime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second) { @@ -29,15 +49,19 @@ void DateTime::SetTime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, /* .tm_year = */ year - 1900, }; - tm.tm_isdst = -1; // Use DST value from local time zone - currentDateTime = std::chrono::system_clock::from_time_t(std::mktime(&tm)); - NRF_LOG_INFO("%d %d %d ", day, month, year); NRF_LOG_INFO("%d %d %d ", hour, minute, second); - UpdateTime(previousSystickCounter); + tm.tm_isdst = -1; // Use DST value from local time zone + + xSemaphoreTake(mutex, portMAX_DELAY); + currentDateTime = std::chrono::system_clock::from_time_t(std::mktime(&tm)); + UpdateTime(previousSystickCounter, true); + xSemaphoreGive(mutex); - systemTask->PushMessage(System::Messages::OnNewTime); + if (systemTask != nullptr) { + systemTask->PushMessage(System::Messages::OnNewTime); + } } void DateTime::SetTimeZone(int8_t timezone, int8_t dst) { @@ -45,25 +69,34 @@ void DateTime::SetTimeZone(int8_t timezone, int8_t dst) { dstOffset = dst; } -void DateTime::UpdateTime(uint32_t systickCounter) { +std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> DateTime::CurrentDateTime() { + xSemaphoreTake(mutex, portMAX_DELAY); + UpdateTime(nrf_rtc_counter_get(portNRF_RTC_REG), false); + xSemaphoreGive(mutex); + return currentDateTime; +} + +void DateTime::UpdateTime(uint32_t systickCounter, bool forceUpdate) { // Handle systick counter overflow uint32_t systickDelta = 0; if (systickCounter < previousSystickCounter) { - systickDelta = 0xffffff - previousSystickCounter; + systickDelta = static_cast<uint32_t>(portNRF_RTC_MAXTICKS) - previousSystickCounter; systickDelta += systickCounter + 1; } else { systickDelta = systickCounter - previousSystickCounter; } - /* - * 1000 ms = 1024 ticks - */ - auto correctedDelta = systickDelta / 1024; - auto rest = systickDelta % 1024; + auto correctedDelta = systickDelta / configTICK_RATE_HZ; + // If a second hasn't passed, there is nothing to do + // If the time has been changed, set forceUpdate to trigger internal state updates + if (correctedDelta == 0 && !forceUpdate) { + return; + } + auto rest = systickDelta % configTICK_RATE_HZ; if (systickCounter >= rest) { previousSystickCounter = systickCounter - rest; } else { - previousSystickCounter = 0xffffff - (rest - systickCounter); + previousSystickCounter = static_cast<uint32_t>(portNRF_RTC_MAXTICKS) - (rest - systickCounter - 1); } currentDateTime += std::chrono::seconds(correctedDelta); @@ -115,8 +148,8 @@ const char* DateTime::MonthShortToStringLow(Months month) { return MonthsStringLow[static_cast<uint8_t>(month)]; } -const char* DateTime::DayOfWeekShortToStringLow() const { - return DaysStringShortLow[static_cast<uint8_t>(DayOfWeek())]; +const char* DateTime::DayOfWeekShortToStringLow(Days day) { + return DaysStringShortLow[static_cast<uint8_t>(day)]; } void DateTime::Register(Pinetime::System::SystemTask* systemTask) { @@ -140,9 +173,9 @@ std::string DateTime::FormattedTime() { hour12 = (hour == 12) ? 12 : hour - 12; amPmStr = "PM"; } - sprintf(buff, "%i:%02i %s", hour12, minute, amPmStr); + snprintf(buff, sizeof(buff), "%i:%02i %s", hour12, minute, amPmStr); } else { - sprintf(buff, "%02i:%02i", hour, minute); + snprintf(buff, sizeof(buff), "%02i:%02i", hour, minute); } return std::string(buff); } diff --git a/src/components/datetime/DateTimeController.h b/src/components/datetime/DateTimeController.h index 0bf6ac2a..a005f9ac 100644 --- a/src/components/datetime/DateTimeController.h +++ b/src/components/datetime/DateTimeController.h @@ -5,6 +5,8 @@ #include <ctime> #include <string> #include "components/settings/Settings.h" +#include <FreeRTOS.h> +#include <semphr.h> namespace Pinetime { namespace System { @@ -39,14 +41,12 @@ namespace Pinetime { * * used to update difference between utc and local time (see UtcOffset()) * - * parameters are in quarters of an our. Following the BLE CTS specification, + * parameters are in quarters of an hour. Following the BLE CTS specification, * timezone is expected to be constant over DST which will be reported in * dst field. */ void SetTimeZone(int8_t timezone, int8_t dst); - void UpdateTime(uint32_t systickCounter); - uint16_t Year() const { return 1900 + localTime.tm_year; } @@ -122,14 +122,12 @@ namespace Pinetime { const char* MonthShortToString() const; const char* DayOfWeekShortToString() const; static const char* MonthShortToStringLow(Months month); - const char* DayOfWeekShortToStringLow() const; + static const char* DayOfWeekShortToStringLow(Days day); - std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> CurrentDateTime() const { - return currentDateTime; - } + std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> CurrentDateTime(); - std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> UTCDateTime() const { - return currentDateTime - std::chrono::seconds((tzOffset + dstOffset) * 15 * 60); + std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> UTCDateTime() { + return CurrentDateTime() - std::chrono::seconds((tzOffset + dstOffset) * 15 * 60); } std::chrono::seconds Uptime() const { @@ -141,10 +139,14 @@ namespace Pinetime { std::string FormattedTime(); private: + void UpdateTime(uint32_t systickCounter, bool forceUpdate); + std::tm localTime; int8_t tzOffset = 0; int8_t dstOffset = 0; + SemaphoreHandle_t mutex = nullptr; + uint32_t previousSystickCounter = 0; std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> currentDateTime; std::chrono::seconds uptime {0}; diff --git a/src/components/datetime/TODO.md b/src/components/datetime/TODO.md new file mode 100644 index 00000000..e9590898 --- /dev/null +++ b/src/components/datetime/TODO.md @@ -0,0 +1,41 @@ +# Refactoring needed + +## Context + +The [PR #2041 - Continuous time updates](https://github.com/InfiniTimeOrg/InfiniTime/pull/2041) highlighted some +limitations in the design of DateTimeController: the granularity of the time returned by `DateTime::CurrentDateTime()` +is limited by the frequency at which SystemTask calls `DateTime::UpdateTime()`, which is currently set to 100ms. + +@mark9064 provided more details +in [this comment](https://github.com/InfiniTimeOrg/InfiniTime/pull/2041#issuecomment-2048528967). + +The [PR #2041 - Continuous time updates](https://github.com/InfiniTimeOrg/InfiniTime/pull/2041) provided some changes +to `DateTime` controller that improves the granularity of the time returned by `DateTime::CurrentDateTime()`. + +However, the review showed that `DateTime` cannot be `const` anymore, even when it's only used to get the current time, +since `DateTime::CurrentDateTime()` changes the internal state of the instance. + +We tried to identify alternative implementation that would have maintained the "const correctness" but we eventually +figured that this would lead to a re-design of `DateTime` which was out of scope of the initial PR (Continuous time +updates and always on display). + +So we decided to merge this PR #2041 and agree to fix/improve `DateTime` later on. + +## What needs to be done? + +Improve/redesign `DateTime` so that it + +* provides a very granular (ideally down to the millisecond) date and time via `CurrentDateTime()`. +* can be declared/passed as `const` when it's only used to **get** the time. +* limits the use of mutex as much as possible (an ideal implementation would not use any mutex, but this might not be + possible). +* improves the design of `DateTime::Seconds()`, `DateTime::Minutes()`, `DateTime::Hours()`, etc as + explained [in this comment](https://github.com/InfiniTimeOrg/InfiniTime/pull/2054#pullrequestreview-2037033105). + +Once this redesign is implemented, all instances/references to `DateTime` should be reviewed and updated to use `const` +where appropriate. + +Please check the following PR to get more context about this redesign: + +* [#2041 - Continuous time updates by @mark9064](https://github.com/InfiniTimeOrg/InfiniTime/pull/2041) +* [#2054 - Continuous time update - Alternative implementation to #2041 by @JF002](https://github.com/InfiniTimeOrg/InfiniTime/pull/2054)
\ No newline at end of file diff --git a/src/components/gfx/Gfx.cpp b/src/components/gfx/Gfx.cpp deleted file mode 100644 index 3eaaa3fe..00000000 --- a/src/components/gfx/Gfx.cpp +++ /dev/null @@ -1,196 +0,0 @@ -#include "components/gfx/Gfx.h" -#include "drivers/St7789.h" -using namespace Pinetime::Components; - -Gfx::Gfx(Pinetime::Drivers::St7789& lcd) : lcd {lcd} { -} - -void Gfx::Init() { -} - -void Gfx::ClearScreen() { - SetBackgroundColor(0x0000); - - state.remainingIterations = 240 + 1; - state.currentIteration = 0; - state.busy = true; - state.action = Action::FillRectangle; - state.taskToNotify = xTaskGetCurrentTaskHandle(); - - lcd.DrawBuffer(0, 0, width, height, reinterpret_cast<const uint8_t*>(buffer), width * 2); - WaitTransferFinished(); -} - -void Gfx::FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint16_t color) { - SetBackgroundColor(color); - - state.remainingIterations = h; - state.currentIteration = 0; - state.busy = true; - state.action = Action::FillRectangle; - state.color = color; - state.taskToNotify = xTaskGetCurrentTaskHandle(); - - lcd.DrawBuffer(x, y, w, h, reinterpret_cast<const uint8_t*>(buffer), width * 2); - - WaitTransferFinished(); -} - -void Gfx::FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t* b) { - state.remainingIterations = h; - state.currentIteration = 0; - state.busy = true; - state.action = Action::FillRectangle; - state.color = 0x00; - state.taskToNotify = xTaskGetCurrentTaskHandle(); - - lcd.DrawBuffer(x, y, w, h, reinterpret_cast<const uint8_t*>(b), width * 2); - - WaitTransferFinished(); -} - -void Gfx::DrawString(uint8_t x, uint8_t y, uint16_t color, const char* text, const FONT_INFO* p_font, bool wrap) { - if (y > (height - p_font->height)) { - // Not enough space to write even single char. - return; - } - - uint8_t current_x = x; - uint8_t current_y = y; - - for (size_t i = 0; text[i] != '\0'; i++) { - if (text[i] == '\n') { - current_x = x; - current_y += p_font->height + p_font->height / 10; - } else { - DrawChar(p_font, (uint8_t) text[i], ¤t_x, current_y, color); - } - - uint8_t char_idx = text[i] - p_font->startChar; - uint16_t char_width = text[i] == ' ' ? (p_font->height / 2) : p_font->charInfo[char_idx].widthBits; - - if (current_x > (width - char_width)) { - if (wrap) { - current_x = x; - current_y += p_font->height + p_font->height / 10; - } else { - break; - } - - if (y > (height - p_font->height)) { - break; - } - } - } -} - -void Gfx::DrawChar(const FONT_INFO* font, uint8_t c, uint8_t* x, uint8_t y, uint16_t color) { - uint8_t char_idx = c - font->startChar; - uint16_t bytes_in_line = CEIL_DIV(font->charInfo[char_idx].widthBits, 8); - uint16_t bg = 0x0000; - - if (c == ' ') { - *x += font->height / 2; - return; - } - - // Build first line - for (uint16_t j = 0; j < bytes_in_line; j++) { - for (uint8_t k = 0; k < 8; k++) { - if ((1 << (7 - k)) & font->data[font->charInfo[char_idx].offset + j]) { - buffer[(j * 8) + k] = color; - } else { - buffer[(j * 8) + k] = bg; - } - } - } - - state.remainingIterations = font->height + 0; - state.currentIteration = 0; - state.busy = true; - state.action = Action::DrawChar; - state.font = const_cast<FONT_INFO*>(font); - state.character = c; - state.color = color; - state.taskToNotify = xTaskGetCurrentTaskHandle(); - - lcd.DrawBuffer(*x, y, bytes_in_line * 8, font->height, reinterpret_cast<const uint8_t*>(&buffer), bytes_in_line * 8 * 2); - WaitTransferFinished(); - - *x += font->charInfo[char_idx].widthBits + font->spacePixels; -} - -void Gfx::pixel_draw(uint8_t x, uint8_t y, uint16_t color) { - lcd.DrawPixel(x, y, color); -} - -void Gfx::Sleep() { - lcd.Sleep(); -} - -void Gfx::Wakeup() { - lcd.Wakeup(); -} - -void Gfx::SetBackgroundColor(uint16_t color) { - for (int i = 0; i < width; i++) { - buffer[i] = color; - } -} - -bool Gfx::GetNextBuffer(uint8_t** data, size_t& size) { - if (!state.busy) - return false; - state.remainingIterations--; - if (state.remainingIterations == 0) { - state.busy = false; - NotifyEndOfTransfer(state.taskToNotify); - return false; - } - - if (state.action == Action::FillRectangle) { - *data = reinterpret_cast<uint8_t*>(buffer); - size = width * 2; - } else if (state.action == Action::DrawChar) { - uint16_t bg = 0x0000; - uint8_t char_idx = state.character - state.font->startChar; - uint16_t bytes_in_line = CEIL_DIV(state.font->charInfo[char_idx].widthBits, 8); - - for (uint16_t j = 0; j < bytes_in_line; j++) { - for (uint8_t k = 0; k < 8; k++) { - if ((1 << (7 - k)) & state.font->data[state.font->charInfo[char_idx].offset + ((state.currentIteration + 1) * bytes_in_line) + j]) { - buffer[(j * 8) + k] = state.color; - } else { - buffer[(j * 8) + k] = bg; - } - } - } - - *data = reinterpret_cast<uint8_t*>(buffer); - size = bytes_in_line * 8 * 2; - } - - state.currentIteration++; - - return true; -} - -void Gfx::NotifyEndOfTransfer(TaskHandle_t task) { - if (task != nullptr) { - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - vTaskNotifyGiveFromISR(task, &xHigherPriorityTaskWoken); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } -} - -void Gfx::WaitTransferFinished() const { - ulTaskNotifyTake(pdTRUE, 500); -} - -void Gfx::SetScrollArea(uint16_t topFixedLines, uint16_t scrollLines, uint16_t bottomFixedLines) { - lcd.VerticalScrollDefinition(topFixedLines, scrollLines, bottomFixedLines); -} - -void Gfx::SetScrollStartLine(uint16_t line) { - lcd.VerticalScrollStartAddress(line); -} diff --git a/src/components/gfx/Gfx.h b/src/components/gfx/Gfx.h deleted file mode 100644 index 17c248f7..00000000 --- a/src/components/gfx/Gfx.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once -#include <FreeRTOS.h> -#include <nrf_font.h> -#include <task.h> -#include <cstddef> -#include <cstdint> -#include "drivers/BufferProvider.h" - -namespace Pinetime { - namespace Drivers { - class St7789; - } - - namespace Components { - class Gfx : public Pinetime::Drivers::BufferProvider { - public: - explicit Gfx(Drivers::St7789& lcd); - void Init(); - void ClearScreen(); - void DrawString(uint8_t x, uint8_t y, uint16_t color, const char* text, const FONT_INFO* p_font, bool wrap); - void DrawChar(const FONT_INFO* font, uint8_t c, uint8_t* x, uint8_t y, uint16_t color); - void FillRectangle(uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint16_t color); - void FillRectangle(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t* b); - void SetScrollArea(uint16_t topFixedLines, uint16_t scrollLines, uint16_t bottomFixedLines); - void SetScrollStartLine(uint16_t line); - - void Sleep(); - void Wakeup(); - bool GetNextBuffer(uint8_t** buffer, size_t& size) override; - void pixel_draw(uint8_t x, uint8_t y, uint16_t color); - - private: - static constexpr uint8_t width = 240; - static constexpr uint8_t height = 240; - - enum class Action { None, FillRectangle, DrawChar }; - - struct State { - State() : busy {false}, action {Action::None}, remainingIterations {0}, currentIteration {0} { - } - - volatile bool busy; - volatile Action action; - volatile uint16_t remainingIterations; - volatile uint16_t currentIteration; - volatile FONT_INFO* font; - volatile uint16_t color; - volatile uint8_t character; - volatile TaskHandle_t taskToNotify = nullptr; - }; - - volatile State state; - - uint16_t buffer[width]; // 1 line buffer - Drivers::St7789& lcd; - - void SetBackgroundColor(uint16_t color); - void WaitTransferFinished() const; - void NotifyEndOfTransfer(TaskHandle_t task); - }; - } -} diff --git a/src/components/heartrate/Biquad.cpp b/src/components/heartrate/Biquad.cpp deleted file mode 100644 index b7edd403..00000000 --- a/src/components/heartrate/Biquad.cpp +++ /dev/null @@ -1,26 +0,0 @@ -/* - 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 deleted file mode 100644 index 7c8ca58f..00000000 --- a/src/components/heartrate/Biquad.h +++ /dev/null @@ -1,22 +0,0 @@ -#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/Ppg.cpp b/src/components/heartrate/Ppg.cpp index 900b1c22..efbed852 100644 --- a/src/components/heartrate/Ppg.cpp +++ b/src/components/heartrate/Ppg.cpp @@ -1,107 +1,292 @@ -/* - 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> +#include <vector> + 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; + float LinearInterpolation(const float* xValues, const float* yValues, int length, float pointX) { + if (pointX > xValues[length - 1]) { + return yValues[length - 1]; + } else if (pointX <= xValues[0]) { + return yValues[0]; + } + int index = 0; + while (pointX > xValues[index] && index < length - 1) { + index++; + } + float pointX0 = xValues[index - 1]; + float pointX1 = xValues[index]; + float pointY0 = yValues[index - 1]; + float pointY1 = yValues[index]; + float mu = (pointX - pointX0) / (pointX1 - pointX0); + + return (pointY0 * (1 - mu) + pointY1 * mu); + } + + float PeakSearch(float* xVals, float* yVals, float threshold, float& width, float start, float end, int length) { + int peaks = 0; + bool enabled = false; + float minBin = 0.0f; + float maxBin = 0.0f; + float peakCenter = 0.0f; + float prevValue = LinearInterpolation(xVals, yVals, length, start - 0.01f); + float currValue = LinearInterpolation(xVals, yVals, length, start); + float idx = start; + while (idx < end) { + float nextValue = LinearInterpolation(xVals, yVals, length, idx + 0.01f); + if (currValue < threshold) { + enabled = true; + } + if (currValue >= threshold and enabled) { + if (prevValue < threshold) { + minBin = idx; + } else if (nextValue <= threshold) { + maxBin = idx; + peaks++; + width = maxBin - minBin; + peakCenter = width / 2.0f + minBin; + } + } + prevValue = currValue; + currValue = nextValue; + idx += 0.01f; } - return e; + if (peaks != 1) { + width = 0.0f; + peakCenter = 0.0f; + } + return peakCenter; } - int CompareShift(int8_t* d, int shift, size_t count) { - return Compare(d + shift, d, count - shift); + float SpectrumMean(const std::array<float, Ppg::spectrumLength>& signal, int start, int end) { + int total = 0; + float mean = 0.0f; + for (int idx = start; idx < end; idx++) { + mean += signal.at(idx); + total++; + } + if (total > 0) { + mean /= static_cast<float>(total); + } + return mean; } - 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; + float SignalToNoise(const std::array<float, Ppg::spectrumLength>& signal, int start, int end, float max) { + float mean = SpectrumMean(signal, start, end); + return max / mean; + } + + // Simple bandpass filter using exponential moving average + void Filter30to240(std::array<float, Ppg::dataLength>& signal) { + // From: + // https://www.norwegiancreations.com/2016/03/arduino-tutorial-simple-high-pass-band-pass-and-band-stop-filtering/ + + int length = signal.size(); + // 0.268 is ~0.5Hz and 0.816 is ~4Hz cutoff at 10Hz sampling + float expAlpha = 0.816f; + float expAvg = 0.0f; + for (int loop = 0; loop < 4; loop++) { + expAvg = signal.front(); + for (int idx = 0; idx < length; idx++) { + expAvg = (expAlpha * signal.at(idx)) + ((1 - expAlpha) * expAvg); + signal[idx] = expAvg; + } + } + expAlpha = 0.268f; + for (int loop = 0; loop < 4; loop++) { + expAvg = signal.front(); + for (int idx = 0; idx < length; idx++) { + expAvg = (expAlpha * signal.at(idx)) + ((1 - expAlpha) * expAvg); + signal[idx] -= expAvg; } - 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} { -} + float SpectrumMax(const std::array<float, Ppg::spectrumLength>& data, int start, int end) { + float max = 0.0f; + for (int idx = start; idx < end; idx++) { + if (data.at(idx) > max) { + max = data.at(idx); + } + } + return max; + } -int8_t Ppg::Preprocess(float spl) { - spl -= offset; - spl = hpf.Step(spl); - spl = agc.Step(spl); - spl = lpf.Step(spl); + void Detrend(std::array<float, Ppg::dataLength>& signal) { + int size = signal.size(); + float offset = signal.front(); + float slope = (signal.at(size - 1) - offset) / static_cast<float>(size - 1); - auto spl_int = static_cast<int8_t>(spl); + for (int idx = 0; idx < size; idx++) { + signal[idx] -= (slope * static_cast<float>(idx) + offset); + } + for (int idx = 0; idx < size - 1; idx++) { + signal[idx] = signal[idx + 1] - signal[idx]; + } + } - if (dataIndex < 200) { - data[dataIndex++] = spl_int; + // Hanning Coefficients from numpy: python -c 'import numpy;print(numpy.hanning(64))' + // Note: Harcoded and must be updated if constexpr dataLength is changed. Prevents the need to + // use cosf() which results in an extra ~5KB in storage. + // This data is symetrical so just using the first half (saves 128B when dataLength is 64). + static constexpr float hanning[Ppg::dataLength >> 1] { + 0.0f, 0.00248461f, 0.00991376f, 0.0222136f, 0.03926189f, 0.06088921f, 0.08688061f, 0.11697778f, + 0.15088159f, 0.1882551f, 0.22872687f, 0.27189467f, 0.31732949f, 0.36457977f, 0.41317591f, 0.46263495f, + 0.51246535f, 0.56217185f, 0.61126047f, 0.65924333f, 0.70564355f, 0.75f, 0.79187184f, 0.83084292f, + 0.86652594f, 0.89856625f, 0.92664544f, 0.95048443f, 0.96984631f, 0.98453864f, 0.99441541f, 0.99937846f}; +} + +Ppg::Ppg() { + dataAverage.fill(0.0f); + spectrum.fill(0.0f); +} + +int8_t Ppg::Preprocess(uint16_t hrs, uint16_t als) { + if (dataIndex < dataLength) { + dataHRS[dataIndex++] = hrs; } - return spl_int; + alsValue = als; + if (alsValue > alsThreshold) { + return 1; + } + return 0; } int Ppg::HeartRate() { - if (dataIndex < 200) { + if (dataIndex < dataLength) { return 0; } - - NRF_LOG_INFO("PREPROCESS, offset = %d", offset); - auto hr = ProcessHeartRate(); - dataIndex = 0; + int hr = 0; + hr = ProcessHeartRate(resetSpectralAvg); + resetSpectralAvg = false; + // Make room for overlapWindow number of new samples + for (int idx = 0; idx < dataLength - overlapWindow; idx++) { + dataHRS[idx] = dataHRS[idx + overlapWindow]; + } + dataIndex = dataLength - overlapWindow; return hr; } -int Ppg::ProcessHeartRate() { - int t0 = Trough(data.data(), dataIndex, 7, 48); - if (t0 < 0) { - return 0; +void Ppg::Reset(bool resetDaqBuffer) { + if (resetDaqBuffer) { + dataIndex = 0; } + avgIndex = 0; + dataAverage.fill(0.0f); + lastPeakLocation = 0.0f; + alsThreshold = UINT16_MAX; + alsValue = 0; + resetSpectralAvg = true; + spectrum.fill(0.0f); +} - int t1 = t0 * 2; - t1 = Trough(data.data(), dataIndex, t1 - 5, t1 + 5); - if (t1 < 0) { - return 0; +// Pass init == true to reset spectral averaging. +// Returns -1 (Reset Acquisition), 0 (Unable to obtain HR) or HR (BPM). +int Ppg::ProcessHeartRate(bool init) { + std::copy(dataHRS.begin(), dataHRS.end(), vReal.begin()); + Detrend(vReal); + Filter30to240(vReal); + vImag.fill(0.0f); + // Apply Hanning Window + int hannIdx = 0; + for (int idx = 0; idx < dataLength; idx++) { + if (idx >= dataLength >> 1) { + hannIdx--; + } + vReal[idx] *= hanning[hannIdx]; + if (idx < dataLength >> 1) { + hannIdx++; + } } - - int t2 = (t1 * 3) / 2; - t2 = Trough(data.data(), dataIndex, t2 - 5, t2 + 5); - if (t2 < 0) { - return 0; + // Compute in place power spectrum + ArduinoFFT<float> FFT = ArduinoFFT<float>(vReal.data(), vImag.data(), dataLength, sampleFreq); + FFT.compute(FFTDirection::Forward); + FFT.complexToMagnitude(); + FFT.~ArduinoFFT(); + SpectrumAverage(vReal.data(), spectrum.data(), spectrum.size(), init); + peakLocation = 0.0f; + float threshold = peakDetectionThreshold; + float peakWidth = 0.0f; + int specLen = spectrum.size(); + float max = SpectrumMax(spectrum, hrROIbegin, hrROIend); + float signalToNoiseRatio = SignalToNoise(spectrum, hrROIbegin, hrROIend, max); + if (signalToNoiseRatio > signalToNoiseThreshold && spectrum.at(0) < dcThreshold) { + threshold *= max; + // Reuse VImag for interpolation x values passed to PeakSearch + for (int idx = 0; idx < dataLength; idx++) { + vImag[idx] = idx; + } + peakLocation = PeakSearch(vImag.data(), + spectrum.data(), + threshold, + peakWidth, + static_cast<float>(hrROIbegin), + static_cast<float>(hrROIend), + specLen); + peakLocation *= freqResolution; } - - int t3 = (t2 * 4) / 3; - t3 = Trough(data.data(), dataIndex, t3 - 4, t3 + 4); - if (t3 < 0) { - return (60 * 24 * 3) / t2; + // Peak too wide? (broad spectrum noise or large, rapid HR change) + if (peakWidth > maxPeakWidth) { + peakLocation = 0.0f; } - - return (60 * 24 * 4) / t3; + // Check HR limits + if (peakLocation < minHR || peakLocation > maxHR) { + peakLocation = 0.0f; + } + // Reset spectral averaging if bad reading + if (peakLocation == 0.0f) { + resetSpectralAvg = true; + } + // Set the ambient light threshold and return HR in BPM + alsThreshold = static_cast<uint16_t>(alsValue * alsFactor); + // Get current average HR. If HR reduced to zero, return -1 (reset) else HR + peakLocation = HeartRateAverage(peakLocation); + int rtn = -1; + if (peakLocation == 0.0f && lastPeakLocation > 0.0f) { + lastPeakLocation = 0.0f; + } else { + lastPeakLocation = peakLocation; + rtn = static_cast<int>((peakLocation * 60.0f) + 0.5f); + } + return rtn; } -void Ppg::SetOffset(uint16_t offset) { - this->offset = offset; - dataIndex = 0; +void Ppg::SpectrumAverage(const float* data, float* spectrum, int length, bool reset) { + if (reset) { + spectralAvgCount = 0; + } + float count = static_cast<float>(spectralAvgCount); + for (int idx = 0; idx < length; idx++) { + spectrum[idx] = (spectrum[idx] * count + data[idx]) / (count + 1); + } + if (spectralAvgCount < spectralAvgMax) { + spectralAvgCount++; + } } -void Ppg::Reset() { - dataIndex = 0; +float Ppg::HeartRateAverage(float hr) { + avgIndex++; + avgIndex %= dataAverage.size(); + dataAverage[avgIndex] = hr; + float avg = 0.0f; + float total = 0.0f; + float min = 300.0f; + float max = 0.0f; + for (const float& value : dataAverage) { + if (value > 0.0f) { + avg += value; + if (value < min) + min = value; + if (value > max) + max = value; + total++; + } + } + if (total > 0) { + avg /= total; + } else { + avg = 0.0f; + } + return avg; } diff --git a/src/components/heartrate/Ppg.h b/src/components/heartrate/Ppg.h index 1f709bab..373e7985 100644 --- a/src/components/heartrate/Ppg.h +++ b/src/components/heartrate/Ppg.h @@ -3,29 +3,78 @@ #include <array> #include <cstddef> #include <cstdint> -#include "components/heartrate/Biquad.h" -#include "components/heartrate/Ptagc.h" +// Note: Change internal define 'sqrt_internal sqrt' to +// 'sqrt_internal sqrtf' to save ~3KB of flash. +#define sqrt_internal sqrtf +#define FFT_SPEED_OVER_PRECISION +#include "libs/arduinoFFT/src/arduinoFFT.h" namespace Pinetime { namespace Controllers { class Ppg { public: Ppg(); - int8_t Preprocess(float spl); + int8_t Preprocess(uint16_t hrs, uint16_t als); int HeartRate(); - - void SetOffset(uint16_t offset); - void Reset(); + void Reset(bool resetDaqBuffer); + static constexpr int deltaTms = 100; + // Daq dataLength: Must be power of 2 + static constexpr uint16_t dataLength = 64; + static constexpr uint16_t spectrumLength = dataLength >> 1; private: - std::array<int8_t, 200> data; - size_t dataIndex = 0; - float offset; - Biquad hpf; - Ptagc agc; - Biquad lpf; + // The sampling frequency (Hz) based on sampling time in milliseconds (DeltaTms) + static constexpr float sampleFreq = 1000.0f / static_cast<float>(deltaTms); + // The frequency resolution (Hz) + static constexpr float freqResolution = sampleFreq / dataLength; + // Number of samples before each analysis + // 0.5 second update rate at 10Hz + static constexpr uint16_t overlapWindow = 5; + // Maximum number of spectrum running averages + // Note: actual number of spectra averaged = spectralAvgMax + 1 + static constexpr uint16_t spectralAvgMax = 2; + // Multiple Peaks above this threshold (% of max) are rejected + static constexpr float peakDetectionThreshold = 0.6f; + // Maximum peak width (bins) at threshold for valid peak. + static constexpr float maxPeakWidth = 2.5f; + // Metric for spectrum noise level. + static constexpr float signalToNoiseThreshold = 3.0f; + // Heart rate Region Of Interest begin (bins) + static constexpr uint16_t hrROIbegin = static_cast<uint16_t>((30.0f / 60.0f) / freqResolution + 0.5f); + // Heart rate Region Of Interest end (bins) + static constexpr uint16_t hrROIend = static_cast<uint16_t>((240.0f / 60.0f) / freqResolution + 0.5f); + // Minimum HR (Hz) + static constexpr float minHR = 40.0f / 60.0f; + // Maximum HR (Hz) + static constexpr float maxHR = 230.0f / 60.0f; + // Threshold for high DC level after filtering + static constexpr float dcThreshold = 0.5f; + // ALS detection factor + static constexpr float alsFactor = 2.0f; + + // Raw ADC data + std::array<uint16_t, dataLength> dataHRS; + // Stores Real numbers from FFT + std::array<float, dataLength> vReal; + // Stores Imaginary numbers from FFT + std::array<float, dataLength> vImag; + // Stores power spectrum calculated from FFT real and imag values + std::array<float, (spectrumLength)> spectrum; + // Stores each new HR value (Hz). Non zero values are averaged for HR output + std::array<float, 20> dataAverage; + + uint16_t avgIndex = 0; + uint16_t spectralAvgCount = 0; + float lastPeakLocation = 0.0f; + uint16_t alsThreshold = UINT16_MAX; + uint16_t alsValue = 0; + uint16_t dataIndex = 0; + float peakLocation; + bool resetSpectralAvg = true; - int ProcessHeartRate(); + int ProcessHeartRate(bool init); + float HeartRateAverage(float hr); + void SpectrumAverage(const float* data, float* spectrum, int length, bool reset); }; } } diff --git a/src/components/heartrate/Ptagc.cpp b/src/components/heartrate/Ptagc.cpp deleted file mode 100644 index 221be460..00000000 --- a/src/components/heartrate/Ptagc.cpp +++ /dev/null @@ -1,29 +0,0 @@ -/* - 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 deleted file mode 100644 index 3476636b..00000000 --- a/src/components/heartrate/Ptagc.h +++ /dev/null @@ -1,17 +0,0 @@ -#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/components/motion/MotionController.cpp b/src/components/motion/MotionController.cpp index 9d16e00d..72507ac5 100644 --- a/src/components/motion/MotionController.cpp +++ b/src/components/motion/MotionController.cpp @@ -2,25 +2,59 @@ #include <task.h> +#include "utility/Math.h" + using namespace Pinetime::Controllers; +namespace { + constexpr inline int32_t Clamp(int32_t val, int32_t min, int32_t max) { + return val < min ? min : (val > max ? max : val); + } + + // only returns meaningful values if inputs are acceleration due to gravity + int16_t DegreesRolled(int16_t y, int16_t z, int16_t prevY, int16_t prevZ) { + int16_t prevYAngle = Pinetime::Utility::Asin(Clamp(prevY * 32, -32767, 32767)); + int16_t yAngle = Pinetime::Utility::Asin(Clamp(y * 32, -32767, 32767)); + + if (z < 0 && prevZ < 0) { + return yAngle - prevYAngle; + } + if (prevZ < 0) { + if (y < 0) { + return -prevYAngle - yAngle - 180; + } + return -prevYAngle - yAngle + 180; + } + if (z < 0) { + if (y < 0) { + return prevYAngle + yAngle + 180; + } + return prevYAngle + yAngle - 180; + } + return prevYAngle - yAngle; + } +} + void MotionController::Update(int16_t x, int16_t y, int16_t z, uint32_t nbSteps) { if (this->nbSteps != nbSteps && service != nullptr) { service->OnNewStepCountValue(nbSteps); } - if (service != nullptr && (this->x != x || this->y != y || this->z != z)) { + if (service != nullptr && (xHistory[0] != x || yHistory[0] != y || zHistory[0] != z)) { service->OnNewMotionValues(x, y, z); } lastTime = time; time = xTaskGetTickCount(); - this->x = x; - lastY = this->y; - this->y = y; - lastZ = this->z; - this->z = z; + xHistory++; + xHistory[0] = x; + yHistory++; + yHistory[0] = y; + zHistory++; + zHistory[0] = z; + + stats = GetAccelStats(); int32_t deltaSteps = nbSteps - this->nbSteps; if (deltaSteps > 0) { @@ -29,38 +63,84 @@ void MotionController::Update(int16_t x, int16_t y, int16_t z, uint32_t nbSteps) this->nbSteps = nbSteps; } -bool MotionController::ShouldRaiseWake(bool isSleeping) { - if ((x + 335) <= 670 && z < 0) { - if (!isSleeping) { - if (y <= 0) { - return false; - } - lastYForRaiseWake = 0; - return false; - } +MotionController::AccelStats MotionController::GetAccelStats() const { + AccelStats stats; - if (y >= 0) { - lastYForRaiseWake = 0; - return false; - } - if (y + 230 < lastYForRaiseWake) { - lastYForRaiseWake = y; - return true; - } + for (uint8_t i = 0; i < AccelStats::numHistory; i++) { + stats.xMean += xHistory[histSize - i]; + stats.yMean += yHistory[histSize - i]; + stats.zMean += zHistory[histSize - i]; + stats.prevXMean += xHistory[1 + i]; + stats.prevYMean += yHistory[1 + i]; + stats.prevZMean += zHistory[1 + i]; } - return false; + stats.xMean /= AccelStats::numHistory; + stats.yMean /= AccelStats::numHistory; + stats.zMean /= AccelStats::numHistory; + stats.prevXMean /= AccelStats::numHistory; + stats.prevYMean /= AccelStats::numHistory; + stats.prevZMean /= AccelStats::numHistory; + + for (uint8_t i = 0; i < AccelStats::numHistory; i++) { + stats.xVariance += (xHistory[histSize - i] - stats.xMean) * (xHistory[histSize - i] - stats.xMean); + stats.yVariance += (yHistory[histSize - i] - stats.yMean) * (yHistory[histSize - i] - stats.yMean); + stats.zVariance += (zHistory[histSize - i] - stats.zMean) * (zHistory[histSize - i] - stats.zMean); + } + stats.xVariance /= AccelStats::numHistory; + stats.yVariance /= AccelStats::numHistory; + stats.zVariance /= AccelStats::numHistory; + + return stats; +} + +bool MotionController::ShouldRaiseWake() const { + constexpr uint32_t varianceThresh = 56 * 56; + constexpr int16_t xThresh = 384; + constexpr int16_t yThresh = -64; + constexpr int16_t rollDegreesThresh = -45; + + if (std::abs(stats.xMean) > xThresh) { + return false; + } + + // if the variance is below the threshold, the accelerometer values can be considered to be from acceleration due to gravity + if (stats.yVariance > varianceThresh || (stats.yMean < -724 && stats.zVariance > varianceThresh) || stats.yMean > yThresh) { + return false; + } + + return DegreesRolled(stats.yMean, stats.zMean, stats.prevYMean, stats.prevZMean) < rollDegreesThresh; } bool MotionController::ShouldShakeWake(uint16_t thresh) { /* Currently Polling at 10hz, If this ever goes faster scalar and EMA might need adjusting */ - int32_t speed = std::abs(z + (y / 2) + (x / 4) - lastY / 2 - lastZ) / (time - lastTime) * 100; - //(.2 * speed) + ((1 - .2) * accumulatedSpeed); - // implemented without floats as .25Alpha - accumulatedSpeed = (speed / 5) + ((accumulatedSpeed / 5) * 4); + int32_t speed = std::abs(zHistory[0] - zHistory[histSize - 1] + (yHistory[0] - yHistory[histSize - 1]) / 2 + + (xHistory[0] - xHistory[histSize - 1]) / 4) * + 100 / (time - lastTime); + // (.2 * speed) + ((1 - .2) * accumulatedSpeed); + accumulatedSpeed = speed / 5 + accumulatedSpeed * 4 / 5; return accumulatedSpeed > thresh; } +bool MotionController::ShouldLowerSleep() const { + if ((stats.xMean > 887 && DegreesRolled(stats.xMean, stats.zMean, stats.prevXMean, stats.prevZMean) > 30) || + (stats.xMean < -887 && DegreesRolled(stats.xMean, stats.zMean, stats.prevXMean, stats.prevZMean) < -30)) { + return true; + } + + if (stats.yMean < 724 || DegreesRolled(stats.yMean, stats.zMean, stats.prevYMean, stats.prevZMean) < 30) { + return false; + } + + for (uint8_t i = AccelStats::numHistory + 1; i < yHistory.Size(); i++) { + if (yHistory[i] < 265) { + return false; + } + } + + return true; +} + void MotionController::Init(Pinetime::Drivers::Bma421::DeviceTypes types) { switch (types) { case Drivers::Bma421::DeviceTypes::BMA421: diff --git a/src/components/motion/MotionController.h b/src/components/motion/MotionController.h index eb8d04aa..be0241d3 100644 --- a/src/components/motion/MotionController.h +++ b/src/components/motion/MotionController.h @@ -6,6 +6,7 @@ #include "drivers/Bma421.h" #include "components/ble/MotionService.h" +#include "utility/CircularBuffer.h" namespace Pinetime { namespace Controllers { @@ -20,15 +21,15 @@ namespace Pinetime { void Update(int16_t x, int16_t y, int16_t z, uint32_t nbSteps); int16_t X() const { - return x; + return xHistory[0]; } int16_t Y() const { - return y; + return yHistory[0]; } int16_t Z() const { - return z; + return zHistory[0]; } uint32_t NbSteps() const { @@ -44,20 +45,13 @@ namespace Pinetime { } bool ShouldShakeWake(uint16_t thresh); - bool ShouldRaiseWake(bool isSleeping); + bool ShouldRaiseWake() const; + bool ShouldLowerSleep() const; int32_t CurrentShakeSpeed() const { return accumulatedSpeed; } - void IsSensorOk(bool isOk) { - isSensorOk = isOk; - } - - bool IsSensorOk() const { - return isSensorOk; - } - DeviceTypes DeviceType() const { return deviceType; } @@ -68,6 +62,10 @@ namespace Pinetime { this->service = service; } + Pinetime::Controllers::MotionService* GetService() const { + return service; + } + private: uint32_t nbSteps = 0; uint32_t currentTripSteps = 0; @@ -75,15 +73,31 @@ namespace Pinetime { TickType_t lastTime = 0; TickType_t time = 0; - int16_t x = 0; - int16_t lastYForRaiseWake = 0; - int16_t lastY = 0; - int16_t y = 0; - int16_t lastZ = 0; - int16_t z = 0; + struct AccelStats { + static constexpr uint8_t numHistory = 2; + + int16_t xMean = 0; + int16_t yMean = 0; + int16_t zMean = 0; + int16_t prevXMean = 0; + int16_t prevYMean = 0; + int16_t prevZMean = 0; + + uint32_t xVariance = 0; + uint32_t yVariance = 0; + uint32_t zVariance = 0; + }; + + AccelStats GetAccelStats() const; + + AccelStats stats = {}; + + static constexpr uint8_t histSize = 8; + Utility::CircularBuffer<int16_t, histSize> xHistory = {}; + Utility::CircularBuffer<int16_t, histSize> yHistory = {}; + Utility::CircularBuffer<int16_t, histSize> zHistory = {}; int32_t accumulatedSpeed = 0; - bool isSensorOk = false; DeviceTypes deviceType = DeviceTypes::Unknown; Pinetime::Controllers::MotionService* service = nullptr; }; diff --git a/src/components/settings/Settings.h b/src/components/settings/Settings.h index d1e71656..602de3a5 100644 --- a/src/components/settings/Settings.h +++ b/src/components/settings/Settings.h @@ -3,20 +3,17 @@ #include <bitset> #include "components/brightness/BrightnessController.h" #include "components/fs/FS.h" +#include "displayapp/apps/Apps.h" namespace Pinetime { namespace Controllers { class Settings { public: enum class ClockType : uint8_t { H24, H12 }; + enum class WeatherFormat : uint8_t { Metric, Imperial }; enum class Notification : uint8_t { On, Off, Sleep }; enum class ChimesOption : uint8_t { None, Hours, HalfHours }; - enum class WakeUpMode : uint8_t { - SingleTap = 0, - DoubleTap = 1, - RaiseWrist = 2, - Shake = 3, - }; + enum class WakeUpMode : uint8_t { SingleTap = 0, DoubleTap = 1, RaiseWrist = 2, Shake = 3, LowerWrist = 4 }; enum class Colors : uint8_t { White, Silver, @@ -38,12 +35,14 @@ namespace Pinetime { Pink }; enum class PTSGaugeStyle : uint8_t { Full, Half, Numeric }; + enum class PTSWeather : uint8_t { On, Off }; struct PineTimeStyle { Colors ColorTime = Colors::Teal; Colors ColorBar = Colors::Teal; Colors ColorBG = Colors::Black; PTSGaugeStyle gaugeStyle = PTSGaugeStyle::Full; + PTSWeather weatherEnable = PTSWeather::Off; }; struct WatchFaceInfineat { @@ -61,15 +60,15 @@ namespace Pinetime { void Init(); void SaveSettings(); - void SetClockFace(uint8_t face) { - if (face != settings.clockFace) { + void SetWatchFace(Pinetime::Applications::WatchFace face) { + if (face != settings.watchFace) { settingsChanged = true; } - settings.clockFace = face; + settings.watchFace = face; }; - uint8_t GetClockFace() const { - return settings.clockFace; + Pinetime::Applications::WatchFace GetWatchFace() const { + return settings.watchFace; }; void SetChimeOption(ChimesOption chimeOption) { @@ -145,6 +144,16 @@ namespace Pinetime { return settings.PTS.gaugeStyle; }; + void SetPTSWeather(PTSWeather weatherEnable) { + if (weatherEnable != settings.PTS.weatherEnable) + settingsChanged = true; + settings.PTS.weatherEnable = weatherEnable; + }; + + PTSWeather GetPTSWeather() const { + return settings.PTS.weatherEnable; + }; + void SetAppMenu(uint8_t menu) { appMenu = menu; }; @@ -172,6 +181,17 @@ namespace Pinetime { return settings.clockType; }; + void SetWeatherFormat(WeatherFormat weatherFormat) { + if (weatherFormat != settings.weatherFormat) { + settingsChanged = true; + } + settings.weatherFormat = weatherFormat; + }; + + WeatherFormat GetWeatherFormat() const { + return settings.weatherFormat; + }; + void SetNotificationStatus(Notification status) { if (status != settings.notificationStatus) { settingsChanged = true; @@ -194,6 +214,21 @@ namespace Pinetime { return settings.screenTimeOut; }; + bool GetAlwaysOnDisplay() const { + return settings.alwaysOnDisplay && GetNotificationStatus() != Notification::Sleep; + }; + + void SetAlwaysOnDisplaySetting(bool state) { + if (state != settings.alwaysOnDisplay) { + settingsChanged = true; + } + settings.alwaysOnDisplay = state; + } + + bool GetAlwaysOnDisplaySetting() const { + return settings.alwaysOnDisplay; + } + void SetShakeThreshold(uint16_t thresh) { if (settings.shakeWakeThreshold != thresh) { settings.shakeWakeThreshold = thresh; @@ -225,7 +260,7 @@ namespace Pinetime { } }; - std::bitset<4> getWakeUpModes() const { + std::bitset<5> getWakeUpModes() const { return settings.wakeUpMode; } @@ -266,25 +301,29 @@ namespace Pinetime { private: Pinetime::Controllers::FS& fs; - static constexpr uint32_t settingsVersion = 0x0004; + static constexpr uint32_t settingsVersion = 0x0008; struct SettingsData { uint32_t version = settingsVersion; uint32_t stepsGoal = 10000; uint32_t screenTimeOut = 15000; + bool alwaysOnDisplay = false; + ClockType clockType = ClockType::H24; + WeatherFormat weatherFormat = WeatherFormat::Metric; Notification notificationStatus = Notification::On; - uint8_t clockFace = 0; + Pinetime::Applications::WatchFace watchFace = Pinetime::Applications::WatchFace::Digital; ChimesOption chimesOption = ChimesOption::None; PineTimeStyle PTS; WatchFaceInfineat watchFaceInfineat; - std::bitset<4> wakeUpMode {0}; + std::bitset<5> wakeUpMode {0}; uint16_t shakeWakeThreshold = 150; + Controllers::BrightnessController::Levels brightLevel = Controllers::BrightnessController::Levels::Medium; }; diff --git a/src/components/timer/Timer.cpp b/src/components/timer/Timer.cpp new file mode 100644 index 00000000..279178cd --- /dev/null +++ b/src/components/timer/Timer.cpp @@ -0,0 +1,28 @@ +#include "components/timer/Timer.h" + +using namespace Pinetime::Controllers; + +Timer::Timer(void* const timerData, TimerCallbackFunction_t timerCallbackFunction) { + timer = xTimerCreate("Timer", 1, pdFALSE, timerData, timerCallbackFunction); +} + +void Timer::StartTimer(std::chrono::milliseconds duration) { + xTimerChangePeriod(timer, pdMS_TO_TICKS(duration.count()), 0); + xTimerStart(timer, 0); +} + +std::chrono::milliseconds Timer::GetTimeRemaining() { + if (IsRunning()) { + TickType_t remainingTime = xTimerGetExpiryTime(timer) - xTaskGetTickCount(); + return std::chrono::milliseconds(remainingTime * 1000 / configTICK_RATE_HZ); + } + return std::chrono::milliseconds(0); +} + +void Timer::StopTimer() { + xTimerStop(timer, 0); +} + +bool Timer::IsRunning() { + return (xTimerIsTimerActive(timer) == pdTRUE); +} diff --git a/src/components/timer/TimerController.h b/src/components/timer/Timer.h index 1c2e44b6..2469666f 100644 --- a/src/components/timer/TimerController.h +++ b/src/components/timer/Timer.h @@ -6,17 +6,10 @@ #include <chrono> namespace Pinetime { - namespace System { - class SystemTask; - } - namespace Controllers { - - class TimerController { + class Timer { public: - TimerController() = default; - - void Init(System::SystemTask* systemTask); + Timer(void* timerData, TimerCallbackFunction_t timerCallbackFunction); void StartTimer(std::chrono::milliseconds duration); @@ -26,10 +19,7 @@ namespace Pinetime { bool IsRunning(); - void OnTimerEnd(); - private: - System::SystemTask* systemTask = nullptr; TimerHandle_t timer; }; } diff --git a/src/components/timer/TimerController.cpp b/src/components/timer/TimerController.cpp deleted file mode 100644 index 5e7f1eed..00000000 --- a/src/components/timer/TimerController.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "components/timer/TimerController.h" -#include "systemtask/SystemTask.h" - -using namespace Pinetime::Controllers; - -void TimerCallback(TimerHandle_t xTimer) { - auto* controller = static_cast<TimerController*>(pvTimerGetTimerID(xTimer)); - controller->OnTimerEnd(); -} - -void TimerController::Init(Pinetime::System::SystemTask* systemTask) { - this->systemTask = systemTask; - timer = xTimerCreate("Timer", 1, pdFALSE, this, TimerCallback); -} - -void TimerController::StartTimer(std::chrono::milliseconds duration) { - xTimerChangePeriod(timer, pdMS_TO_TICKS(duration.count()), 0); - xTimerStart(timer, 0); -} - -std::chrono::milliseconds TimerController::GetTimeRemaining() { - if (IsRunning()) { - TickType_t remainingTime = xTimerGetExpiryTime(timer) - xTaskGetTickCount(); - return std::chrono::milliseconds(remainingTime * 1000 / configTICK_RATE_HZ); - } - return std::chrono::milliseconds(0); -} - -void TimerController::StopTimer() { - xTimerStop(timer, 0); -} - -bool TimerController::IsRunning() { - return (xTimerIsTimerActive(timer) == pdTRUE); -} - -void TimerController::OnTimerEnd() { - systemTask->PushMessage(System::Messages::OnTimerDone); -} diff --git a/src/components/utility/LinearApproximation.h b/src/components/utility/LinearApproximation.h deleted file mode 100644 index 1fe58d44..00000000 --- a/src/components/utility/LinearApproximation.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include <cstddef> -#include <array> - -namespace Pinetime { - namespace Utility { - - // based on: https://github.com/SHristov92/LinearApproximation/blob/main/Linear.h - template <typename Key, typename Value, std::size_t Size> - class LinearApproximation { - using Point = struct { - Key key; - Value value; - }; - - public: - LinearApproximation(const std::array<Point, Size>&& sorted_points) : points {sorted_points} { - } - - Value GetValue(Key key) const { - if (key <= points[0].key) { - return points[0].value; - } - - for (std::size_t i = 1; i < Size; i++) { - const auto& p = points[i]; - const auto& p_prev = points[i - 1]; - - if (key < p.key) { - return p_prev.value + (key - p_prev.key) * (p.value - p_prev.value) / (p.key - p_prev.key); - } - } - - return points[Size - 1].value; - } - - private: - std::array<Point, Size> points; - }; - } -} |
