Skip to content

Commit 8020a7d

Browse files
sofarclaude
andcommitted
Add BLE Reminder Service with controller and persistence
New components: - ReminderController: stores up to 10 reminders (32-char messages), FreeRTOS timer scheduling, LittleFS persistence, mutex protection - ReminderService: BLE GATT service with 5 characteristics (upload, delete, list, ack notify, sync) wired to the controller Integration: - SystemTask: handles SetOffReminder (fires notifications) and SaveReminders (schedules timer, forwards file save to DisplayApp) - DisplayApp: handles SaveReminders message for LFS write operations which need more stack than SystemTask's 1400 bytes - LFS file/dir handles stored as controller members (not stack locals) to avoid SystemTask stack overflow - SetOffReminder handler extracted to separate function to reduce Work() stack frame Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5af3857 commit 8020a7d

16 files changed

Lines changed: 851 additions & 3 deletions

src/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,12 +458,14 @@ list(APPEND SOURCE_FILES
458458
components/ble/ServiceDiscovery.cpp
459459
components/ble/HeartRateService.cpp
460460
components/ble/MotionService.cpp
461+
components/ble/ReminderService.cpp
461462
components/firmwarevalidator/FirmwareValidator.cpp
462463
components/motor/MotorController.cpp
463464
components/settings/Settings.cpp
464465
components/timer/Timer.cpp
465466
components/stopwatch/StopWatchController.cpp
466467
components/alarm/AlarmController.cpp
468+
components/reminder/ReminderController.cpp
467469
components/fs/FS.cpp
468470
drivers/Cst816s.cpp
469471
FreeRTOS/port.c
@@ -526,11 +528,13 @@ list(APPEND RECOVERY_SOURCE_FILES
526528
components/ble/ServiceDiscovery.cpp
527529
components/ble/HeartRateService.cpp
528530
components/ble/MotionService.cpp
531+
components/ble/ReminderService.cpp
529532
components/firmwarevalidator/FirmwareValidator.cpp
530533
components/settings/Settings.cpp
531534
components/timer/Timer.cpp
532535
components/stopwatch/StopWatchController.cpp
533536
components/alarm/AlarmController.cpp
537+
components/reminder/ReminderController.cpp
534538
drivers/Cst816s.cpp
535539
FreeRTOS/port.c
536540
FreeRTOS/port_cmsis_systick.c

src/components/ble/NimbleController.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask,
3030
Pinetime::Drivers::SpiNorFlash& spiNorFlash,
3131
HeartRateController& heartRateController,
3232
MotionController& motionController,
33-
FS& fs)
33+
FS& fs,
34+
ReminderController& reminderController)
3435
: systemTask {systemTask},
3536
bleController {bleController},
3637
dateTimeController {dateTimeController},
@@ -46,6 +47,7 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask,
4647
immediateAlertService {systemTask, notificationManager},
4748
heartRateService {*this, heartRateController},
4849
motionService {*this, motionController},
50+
reminderService {systemTask, reminderController},
4951
fsService {systemTask, fs},
5052
serviceDiscovery({&currentTimeClient, &alertNotificationClient}) {
5153
}
@@ -92,6 +94,7 @@ void NimbleController::Init() {
9294
immediateAlertService.Init();
9395
heartRateService.Init();
9496
motionService.Init();
97+
reminderService.Init();
9598
fsService.Init();
9699

97100
int rc;

src/components/ble/NimbleController.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "components/ble/ImmediateAlertService.h"
2020
#include "components/ble/ServiceDiscovery.h"
2121
#include "components/ble/MotionService.h"
22+
#include "components/ble/ReminderService.h"
2223
#include "components/fs/FS.h"
2324

2425
namespace Pinetime {
@@ -46,7 +47,8 @@ namespace Pinetime {
4647
Pinetime::Drivers::SpiNorFlash& spiNorFlash,
4748
HeartRateController& heartRateController,
4849
MotionController& motionController,
49-
FS& fs);
50+
FS& fs,
51+
ReminderController& reminderController);
5052
void Init();
5153
void StartAdvertising();
5254
int OnGAPEvent(ble_gap_event* event);
@@ -87,6 +89,7 @@ namespace Pinetime {
8789
ImmediateAlertService immediateAlertService;
8890
HeartRateService heartRateService;
8991
MotionService motionService;
92+
ReminderService reminderService;
9093
FSService fsService;
9194
ServiceDiscovery serviceDiscovery;
9295

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/* Copyright (C) 2024
2+
3+
This file is part of InfiniTime.
4+
5+
InfiniTime is free software: you can redistribute it and/or modify
6+
it under the terms of the GNU General Public License as published
7+
by the Free Software Foundation, either version 3 of the License, or
8+
(at your option) any later version.
9+
10+
InfiniTime is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU General Public License for more details.
14+
15+
You should have received a copy of the GNU General Public License
16+
along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
#include "components/ble/ReminderService.h"
19+
#include "systemtask/SystemTask.h"
20+
#include <cstring>
21+
#include <nrf_log.h>
22+
23+
using namespace Pinetime::Controllers;
24+
25+
int ReminderCallback(uint16_t /*connHandle*/, uint16_t /*attrHandle*/, struct ble_gatt_access_ctxt* ctxt, void* arg) {
26+
return static_cast<ReminderService*>(arg)->OnCommand(ctxt);
27+
}
28+
29+
ReminderService::ReminderService(System::SystemTask& systemTask, ReminderController& reminderController)
30+
: characteristicDefinition {
31+
{.uuid = &uploadCharUuid.u, .access_cb = ReminderCallback, .arg = this, .flags = BLE_GATT_CHR_F_WRITE},
32+
{.uuid = &deleteCharUuid.u, .access_cb = ReminderCallback, .arg = this, .flags = BLE_GATT_CHR_F_WRITE},
33+
{.uuid = &listCharUuid.u, .access_cb = ReminderCallback, .arg = this, .flags = BLE_GATT_CHR_F_READ},
34+
{.uuid = &ackCharUuid.u,
35+
.access_cb = ReminderCallback,
36+
.arg = this,
37+
.flags = BLE_GATT_CHR_F_NOTIFY,
38+
.val_handle = &ackHandle},
39+
{.uuid = &syncCharUuid.u, .access_cb = ReminderCallback, .arg = this, .flags = BLE_GATT_CHR_F_WRITE},
40+
{0}},
41+
serviceDefinition {
42+
{.type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &serviceUuid.u, .characteristics = characteristicDefinition},
43+
{0}},
44+
systemTask {systemTask},
45+
reminderController {reminderController} {
46+
}
47+
48+
void ReminderService::Init() {
49+
int res;
50+
res = ble_gatts_count_cfg(serviceDefinition);
51+
ASSERT(res == 0);
52+
53+
res = ble_gatts_add_svcs(serviceDefinition);
54+
ASSERT(res == 0);
55+
}
56+
57+
int ReminderService::OnCommand(struct ble_gatt_access_ctxt* ctxt) {
58+
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
59+
size_t packetLen = OS_MBUF_PKTLEN(ctxt->om);
60+
61+
if (ble_uuid_cmp(ctxt->chr->uuid, &uploadCharUuid.u) == 0) {
62+
// Upload/modify a single reminder (expects sizeof(Reminder) bytes)
63+
if (packetLen < sizeof(Reminder)) {
64+
NRF_LOG_WARNING("[ReminderService] Upload packet too small: %u", packetLen);
65+
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
66+
}
67+
68+
Reminder reminder {};
69+
os_mbuf_copydata(ctxt->om, 0, sizeof(Reminder), &reminder);
70+
if (reminder.id >= Reminder::MaxReminders) {
71+
NRF_LOG_WARNING("[ReminderService] Invalid reminder ID: %u", reminder.id);
72+
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
73+
}
74+
75+
if (!reminderController.AddOrUpdate(reminder)) {
76+
return BLE_ATT_ERR_INSUFFICIENT_RES;
77+
}
78+
NRF_LOG_INFO("[ReminderService] Uploaded reminder %u: %02d:%02d", reminder.id, reminder.hours, reminder.minutes);
79+
80+
} else if (ble_uuid_cmp(ctxt->chr->uuid, &deleteCharUuid.u) == 0) {
81+
// Delete by ID (1 byte)
82+
if (packetLen < 1) {
83+
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
84+
}
85+
86+
uint8_t reminderId = 0;
87+
os_mbuf_copydata(ctxt->om, 0, 1, &reminderId);
88+
if (reminderId == 0xFF) {
89+
reminderController.ClearAll();
90+
NRF_LOG_INFO("[ReminderService] Cleared all reminders");
91+
} else {
92+
reminderController.Delete(reminderId);
93+
NRF_LOG_INFO("[ReminderService] Deleted reminder %u", reminderId);
94+
}
95+
96+
} else if (ble_uuid_cmp(ctxt->chr->uuid, &syncCharUuid.u) == 0) {
97+
// Sync: 1 byte count + N * sizeof(Reminder) bytes
98+
if (packetLen < 1) {
99+
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
100+
}
101+
102+
uint8_t syncCount = 0;
103+
os_mbuf_copydata(ctxt->om, 0, 1, &syncCount);
104+
105+
if (syncCount > Reminder::MaxReminders) {
106+
NRF_LOG_WARNING("[ReminderService] Sync count %u exceeds max", syncCount);
107+
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
108+
}
109+
110+
size_t expectedSize = 1 + (syncCount * sizeof(Reminder));
111+
if (packetLen < expectedSize) {
112+
NRF_LOG_WARNING("[ReminderService] Sync packet too small: %u < %u", packetLen, expectedSize);
113+
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
114+
}
115+
116+
reminderController.ClearAll();
117+
for (uint8_t i = 0; i < syncCount; i++) {
118+
Reminder reminder {};
119+
os_mbuf_copydata(ctxt->om, 1 + (i * sizeof(Reminder)), sizeof(Reminder), &reminder);
120+
if (reminder.id < Reminder::MaxReminders) {
121+
reminderController.AddOrUpdate(reminder);
122+
}
123+
}
124+
NRF_LOG_INFO("[ReminderService] Synced %u reminders", syncCount);
125+
}
126+
127+
} else if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
128+
if (ble_uuid_cmp(ctxt->chr->uuid, &listCharUuid.u) == 0) {
129+
// List all reminders: 1 byte count + N * sizeof(Reminder) structs
130+
uint8_t activeCount = reminderController.ActiveCount();
131+
int rc = os_mbuf_append(ctxt->om, &activeCount, sizeof(activeCount));
132+
if (rc != 0) {
133+
return BLE_ATT_ERR_INSUFFICIENT_RES;
134+
}
135+
136+
const auto& all = reminderController.GetAll();
137+
for (uint8_t i = 0; i < activeCount; i++) {
138+
rc = os_mbuf_append(ctxt->om, &all[i], sizeof(Reminder));
139+
if (rc != 0) {
140+
return BLE_ATT_ERR_INSUFFICIENT_RES;
141+
}
142+
}
143+
NRF_LOG_INFO("[ReminderService] Listed %u reminders", activeCount);
144+
}
145+
}
146+
147+
return 0;
148+
}
149+
150+
void ReminderService::SendAckNotification(uint8_t reminderId, uint32_t timestamp) {
151+
uint16_t connectionHandle = systemTask.nimble().connHandle();
152+
if (connectionHandle == 0 || connectionHandle == BLE_HS_CONN_HANDLE_NONE) {
153+
return;
154+
}
155+
156+
// 5 bytes: 1 byte ID + 4 bytes timestamp (little-endian)
157+
uint8_t data[5];
158+
data[0] = reminderId;
159+
data[1] = static_cast<uint8_t>(timestamp);
160+
data[2] = static_cast<uint8_t>(timestamp >> 8);
161+
data[3] = static_cast<uint8_t>(timestamp >> 16);
162+
data[4] = static_cast<uint8_t>(timestamp >> 24);
163+
164+
auto* om = ble_hs_mbuf_from_flat(data, sizeof(data));
165+
ble_gattc_notify_custom(connectionHandle, ackHandle, om);
166+
NRF_LOG_INFO("[ReminderService] Sent ack for reminder %u", reminderId);
167+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* Copyright (C) 2024
2+
3+
This file is part of InfiniTime.
4+
5+
InfiniTime is free software: you can redistribute it and/or modify
6+
it under the terms of the GNU General Public License as published
7+
by the Free Software Foundation, either version 3 of the License, or
8+
(at your option) any later version.
9+
10+
InfiniTime is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU General Public License for more details.
14+
15+
You should have received a copy of the GNU General Public License
16+
along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
#pragma once
19+
20+
#include <cstdint>
21+
22+
#define min // workaround: nimble's min/max macros conflict with libstdc++
23+
#define max
24+
#include <host/ble_gap.h>
25+
#undef max
26+
#undef min
27+
28+
#include "components/reminder/ReminderController.h"
29+
30+
namespace Pinetime {
31+
namespace System {
32+
class SystemTask;
33+
}
34+
35+
namespace Controllers {
36+
37+
class ReminderService {
38+
public:
39+
ReminderService(Pinetime::System::SystemTask& systemTask, Pinetime::Controllers::ReminderController& reminderController);
40+
void Init();
41+
42+
int OnCommand(struct ble_gatt_access_ctxt* ctxt);
43+
44+
void SendAckNotification(uint8_t reminderId, uint32_t timestamp);
45+
46+
private:
47+
// 0006yyxx-78fc-48fe-8e23-433b3a1942d0
48+
static constexpr ble_uuid128_t CharUuid(uint8_t x, uint8_t y) {
49+
return ble_uuid128_t {.u = {.type = BLE_UUID_TYPE_128},
50+
.value = {0xd0, 0x42, 0x19, 0x3a, 0x3b, 0x43, 0x23, 0x8e, 0xfe, 0x48, 0xfc, 0x78, y, x, 0x06, 0x00}};
51+
}
52+
53+
static constexpr ble_uuid128_t BaseUuid() {
54+
return CharUuid(0x00, 0x00);
55+
}
56+
57+
ble_uuid128_t serviceUuid {BaseUuid()};
58+
ble_uuid128_t uploadCharUuid {CharUuid(0x01, 0x00)}; // Write: upload/modify reminder
59+
ble_uuid128_t deleteCharUuid {CharUuid(0x02, 0x00)}; // Write: delete by ID
60+
ble_uuid128_t listCharUuid {CharUuid(0x03, 0x00)}; // Read: list all reminders
61+
ble_uuid128_t ackCharUuid {CharUuid(0x04, 0x00)}; // Notify: ack events
62+
ble_uuid128_t syncCharUuid {CharUuid(0x05, 0x00)}; // Write: clear all + bulk upload
63+
64+
struct ble_gatt_chr_def characteristicDefinition[6];
65+
struct ble_gatt_svc_def serviceDefinition[2];
66+
67+
Pinetime::System::SystemTask& systemTask;
68+
Pinetime::Controllers::ReminderController& reminderController;
69+
70+
uint16_t ackHandle {};
71+
};
72+
}
73+
}

0 commit comments

Comments
 (0)