aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/alarm/AlarmController.cpp100
-rw-r--r--src/components/alarm/AlarmController.h46
-rw-r--r--src/components/battery/BatteryController.cpp2
-rw-r--r--src/components/ble/DfuService.cpp10
-rw-r--r--src/components/ble/DfuService.h8
-rw-r--r--src/components/ble/HeartRateService.cpp8
-rw-r--r--src/components/ble/HeartRateService.h17
-rw-r--r--src/components/ble/MotionService.cpp14
-rw-r--r--src/components/ble/MotionService.h10
-rw-r--r--src/components/ble/MusicService.cpp8
-rw-r--r--src/components/ble/MusicService.h11
-rw-r--r--src/components/ble/NavigationService.cpp4
-rw-r--r--src/components/ble/NavigationService.h8
-rw-r--r--src/components/ble/NimbleController.cpp24
-rw-r--r--src/components/ble/NimbleController.h10
-rw-r--r--src/components/ble/NotificationManager.h7
-rw-r--r--src/components/ble/SimpleWeatherService.cpp173
-rw-r--r--src/components/ble/SimpleWeatherService.h174
-rw-r--r--src/components/ble/weather/WeatherData.h385
-rw-r--r--src/components/ble/weather/WeatherService.cpp607
-rw-r--r--src/components/ble/weather/WeatherService.h174
-rw-r--r--src/components/brightness/BrightnessController.cpp124
-rw-r--r--src/components/brightness/BrightnessController.h24
-rw-r--r--src/components/datetime/DateTimeController.cpp77
-rw-r--r--src/components/datetime/DateTimeController.h20
-rw-r--r--src/components/datetime/TODO.md41
-rw-r--r--src/components/gfx/Gfx.cpp196
-rw-r--r--src/components/gfx/Gfx.h62
-rw-r--r--src/components/heartrate/Biquad.cpp26
-rw-r--r--src/components/heartrate/Biquad.h22
-rw-r--r--src/components/heartrate/Ppg.cpp327
-rw-r--r--src/components/heartrate/Ppg.h75
-rw-r--r--src/components/heartrate/Ptagc.cpp29
-rw-r--r--src/components/heartrate/Ptagc.h17
-rw-r--r--src/components/motion/MotionController.cpp136
-rw-r--r--src/components/motion/MotionController.h52
-rw-r--r--src/components/settings/Settings.h69
-rw-r--r--src/components/timer/Timer.cpp28
-rw-r--r--src/components/timer/Timer.h (renamed from src/components/timer/TimerController.h)14
-rw-r--r--src/components/timer/TimerController.cpp39
-rw-r--r--src/components/utility/LinearApproximation.h42
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({&currentTimeClient, &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], &current_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;
- };
- }
-}