diff --git a/docs/schedule_reminder_usage.md b/docs/schedule_reminder_usage.md new file mode 100644 index 0000000000..b98739a5b3 --- /dev/null +++ b/docs/schedule_reminder_usage.md @@ -0,0 +1,473 @@ +# 日程提醒功能使用指南 + +## 概述 + +日程提醒功能为 xiaozhi-esp32 项目添加了定时提醒功能,支持单次和重复提醒,可以通过 MCP 协议远程管理。 + +## 功能特性 + +- ✅ 单次和重复提醒 +- ✅ 通过 MCP 协议远程管理 +- ✅ 线程安全设计 +- ✅ 持久化存储 +- ✅ 完整的错误处理 +- ✅ 与现有通知系统集成 + +## 配置方法 + +### 1. 启用 MCP 协议 + +首先需要确保 MCP 协议已启用: + +```bash +idf.py menuconfig +``` + +导航到: +``` +Component config → MCP Server Configuration +``` + +配置选项: +- [*] **Enable MCP Server** - 启用 MCP 服务器(必需) +- **MCP Server Port** - MCP 服务器端口(默认 8080) +- [*] **Enable MCP Tools** - 启用 MCP 工具(必需) + +### 2. 启用日程提醒功能 + +继续在 menuconfig 中配置: + +``` +Component config → Schedule Reminder Features +``` + +配置选项: +- [ ] **Enable Schedule Reminder** - 启用日程提醒功能(默认关闭) +- **Maximum schedule items** - 最大日程数量(默认20,范围1-50) +- **Schedule check interval (seconds)** - 检查间隔(默认30秒,范围10-300) +- [*] **Enable Schedule MCP Tools** - 启用 MCP 管理工具(默认开启) + +### 3. 编译项目 + +```bash +idf.py build +idf.py flash monitor +``` + +### 4. 理解 MCP 架构 + +在 xiaozhi-esp32 项目中,**MCP 服务器是运行在 ESP32 设备上的**,不需要在您的电脑上部署额外的服务器软件。 + +**架构说明:** +- **MCP 服务器**:运行在 ESP32 设备上(您编译的固件) +- **MCP 客户端**:运行在您的电脑上(浏览器、命令行工具等) +- **通信方式**:通过 WebSocket 或 HTTP 协议 + +### 5. 连接 MCP 客户端 + +设备启动后,可以通过以下方式连接 MCP 客户端: + +#### 方法一:使用浏览器开发者工具(推荐) + +1. **打开浏览器**:Chrome、Firefox 或 Edge +2. **打开开发者工具**:按 F12 或右键 → 检查 +3. **切换到 Console 标签** +4. **执行以下 JavaScript 代码**: + +```javascript +// 替换为您的 ESP32 设备 IP 地址 +const deviceIP = '192.168.1.100'; // 修改为实际设备 IP +const ws = new WebSocket(`ws://${deviceIP}:8080/mcp`); + +ws.onopen = function() { + console.log('MCP 连接已建立'); + + // 发送日程管理命令 + const message = { + type: 'mcp', + payload: { + tool: 'schedule.add', + parameters: { + title: "测试提醒", + description: "这是一个测试提醒", + trigger_time: Math.floor(Date.now() / 1000) + 3600, // 1小时后 + recurring: false + } + } + }; + + ws.send(JSON.stringify(message)); + console.log('命令已发送'); +}; + +ws.onmessage = function(event) { + console.log('收到响应:', event.data); +}; + +ws.onerror = function(error) { + console.error('连接错误:', error); +}; +``` + +#### 方法二:使用命令行工具 + +```bash +# 替换为您的 ESP32 设备 IP 地址 +DEVICE_IP="192.168.1.100" # 修改为实际设备 IP + +# 使用 curl 发送 MCP 命令 +curl -X POST http://${DEVICE_IP}:8080/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "type": "mcp", + "payload": { + "tool": "schedule.add", + "parameters": { + "title": "测试提醒", + "description": "这是一个测试提醒", + "trigger_time": 1730822400, + "recurring": false + } + } + }' +``` + +#### 方法三:使用专门的 WebSocket 客户端工具 + +**推荐工具:** +1. **WebSocket King**(Chrome 扩展) + - 下载:Chrome 网上应用店搜索 "WebSocket King" + - 使用:输入 `ws://设备IP:8080/mcp` 连接 + +2. **Postman**(支持 WebSocket) + - 下载:https://www.postman.com/downloads/ + - 使用:新建 WebSocket 请求 + +3. **wscat**(命令行工具) + ```bash + # 安装 + npm install -g wscat + + # 连接 + wscat -c ws://设备IP:8080/mcp + ``` + +### 6. 获取设备 IP 地址 + +设备启动后,在串口监视器中查看日志: + +```bash +idf.py monitor +``` + +查找类似这样的日志: +``` +I (1234) wifi: connected to SSID, ip: 192.168.1.100 +``` + +**或者使用网络扫描工具:** +```bash +# 在 Linux/Mac 上 +nmap -sn 192.168.1.0/24 + +# 在 Windows 上 +arp -a +``` + +## 使用方式 + +### 1. 通过 MCP 工具管理 + +#### 添加日程 + +```json +{ + "title": "会议提醒", + "description": "每周团队会议", + "trigger_time": 1730822400, + "recurring": true, + "repeat_interval": 604800 +} +``` + +**参数说明:** +- `title` (必需): 提醒标题 +- `description` (可选): 详细描述 +- `trigger_time` (必需): 触发时间(Unix 时间戳) +- `recurring` (可选): 是否重复(默认 false) +- `repeat_interval` (可选): 重复间隔(秒) + +#### 列出所有日程 + +```json +{} +``` + +#### 删除日程 + +```json +{ + "id": "1730822400" +} +``` + +#### 更新日程 + +```json +{ + "id": "1730822400", + "title": "更新后的会议提醒", + "trigger_time": 1730908800, + "enabled": true +} +``` + +### 2. 通过代码 API 使用 + +#### 初始化 + +```cpp +#include "features/schedule_reminder/schedule_reminder.h" + +auto& schedule_reminder = ScheduleReminder::GetInstance(); +if (!schedule_reminder.Initialize()) { + ESP_LOGE(TAG, "日程提醒初始化失败"); + return; +} +``` + +#### 添加日程 + +```cpp +ScheduleItem item; +item.id = std::to_string(time(nullptr)); +item.title = "吃药提醒"; +item.description = "记得按时吃药"; +item.trigger_time = time(nullptr) + 3600; // 1小时后 +item.recurring = true; +item.repeat_interval = 86400; // 每天重复 +item.created_at = std::to_string(time(nullptr)); + +ScheduleError result = schedule_reminder.AddSchedule(item); +switch (result) { + case ScheduleError::kSuccess: + ESP_LOGI(TAG, "日程添加成功"); + break; + case ScheduleError::kMaxItemsReached: + ESP_LOGE(TAG, "达到最大日程数量限制"); + break; + case ScheduleError::kDuplicateId: + ESP_LOGE(TAG, "日程ID重复"); + break; + // ... 其他错误处理 +} +``` + +#### 设置提醒回调 + +```cpp +schedule_reminder.SetReminderCallback([](const ScheduleItem& item) { + // 使用系统 Alert 显示提醒 + char message[256]; + if (item.description.empty()) { + snprintf(message, sizeof(message), "提醒: %s", item.title.c_str()); + } else { + snprintf(message, sizeof(message), "提醒: %s - %s", + item.title.c_str(), item.description.c_str()); + } + + // 调用系统通知 + Alert("日程提醒", message, "bell", Lang::Sounds::OGG_NOTIFICATION); + + ESP_LOGI(TAG, "日程触发: %s", item.title.c_str()); +}); +``` + +#### 其他操作 + +```cpp +// 获取所有日程 +auto schedules = schedule_reminder.GetSchedules(); + +// 获取特定日程 +ScheduleItem* item = schedule_reminder.GetSchedule("schedule_id"); + +// 删除日程 +ScheduleError result = schedule_reminder.RemoveSchedule("schedule_id"); + +// 更新日程 +ScheduleError result = schedule_reminder.UpdateSchedule("schedule_id", updated_item); +``` + +## 使用场景示例 + +### 1. 每日提醒 + +```json +{ + "title": "晨间锻炼", + "description": "每天早晨锻炼身体", + "trigger_time": 1730822400, + "recurring": true, + "repeat_interval": 86400 +} +``` + +### 2. 每周会议 + +```json +{ + "title": "周会", + "description": "每周团队会议", + "trigger_time": 1730822400, + "recurring": true, + "repeat_interval": 604800 +} +``` + +### 3. 一次性事件 + +```json +{ + "title": "生日提醒", + "description": "朋友的生日聚会", + "trigger_time": 1730822400, + "recurring": false +} +``` + +### 4. 定时任务 + +```json +{ + "title": "浇花提醒", + "description": "给阳台的花浇水", + "trigger_time": 1730822400, + "recurring": true, + "repeat_interval": 172800 // 每2天 +} +``` + +## 错误处理 + +### 错误码说明 + +```cpp +enum class ScheduleError { + kSuccess = 0, // 操作成功 + kMaxItemsReached, // 达到最大日程数量 + kDuplicateId, // 日程ID重复 + kInvalidTime, // 无效的触发时间 + kStorageError, // 存储错误 + kNotFound, // 日程未找到 + kNotInitialized // 未初始化 +}; +``` + +### 错误处理示例 + +```cpp +ScheduleError result = schedule_reminder.AddSchedule(item); +if (result != ScheduleError::kSuccess) { + switch (result) { + case ScheduleError::kMaxItemsReached: + // 处理达到最大数量 + break; + case ScheduleError::kInvalidTime: + // 处理时间错误 + break; + case ScheduleError::kStorageError: + // 处理存储错误 + break; + default: + // 处理其他错误 + break; + } +} +``` + +## 技术细节 + +### 存储格式 + +日程数据以 JSON 格式存储在系统 Settings 中: + +```json +{ + "version": 1, + "schedules": [ + { + "id": "1730822400", + "title": "会议提醒", + "description": "每周团队会议", + "trigger_time": 1730822400, + "enabled": true, + "recurring": true, + "repeat_interval": 604800, + "created_at": "1730822400" + } + ] +} +``` + +### 线程安全 + +所有公共方法都使用互斥锁保护,确保多线程环境下的数据安全。 + +### 性能考虑 + +- **检查间隔**:默认30秒,可根据需要调整 +- **内存使用**:每个日程约占用200-300字节 +- **CPU 使用**:检查逻辑轻量,对系统影响小 + +## 常见问题 + +### Q: 如何计算 Unix 时间戳? +A: 可以使用在线工具或编程语言的时间函数计算。例如在 Python 中: +```python +import time +timestamp = int(time.time()) + 3600 # 1小时后 +``` + +### Q: 重复间隔的单位是什么? +A: 单位为秒。常用值: +- 3600 = 1小时 +- 86400 = 1天 +- 604800 = 1周 + +### Q: 如何禁用某个提醒? +A: 使用更新工具将 `enabled` 字段设为 false。 + +### Q: 最大支持多少个日程? +A: 默认20个,可在配置中调整到最多50个。 + +### Q: 数据会持久化吗? +A: 是的,所有日程都会保存到 flash 中,重启后仍然有效。 + +## 故障排除 + +### 1. 提醒未触发 +- 检查设备时间是否正确 +- 确认日程的 `enabled` 字段为 true +- 查看日志确认检查间隔是否正常 + +### 2. MCP 工具无法使用 +- 确认 `ENABLE_SCHEDULE_MCP_TOOLS` 已启用 +- 检查网络连接状态 +- 查看 MCP 协议日志 + +### 3. 存储错误 +- 检查 flash 存储空间 +- 查看系统 Settings 日志 +- 尝试重启设备 + +## 版本历史 + +- v1.0: 初始版本 + - 基础日程管理功能 + - MCP 工具支持 + - 持久化存储 + +--- + +**注意**: 使用前请确保已正确配置并编译项目。如有问题,请查看系统日志获取详细信息。 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 00aa5c4a46..6529e59447 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -37,7 +37,7 @@ set(SOURCES "audio/audio_codec.cc" "main.cc" ) -set(INCLUDE_DIRS "." "display" "display/lvgl_display" "display/lvgl_display/jpg" "audio" "protocols") +set(INCLUDE_DIRS "." "display" "display/lvgl_display" "display/lvgl_display/jpg" "audio" "protocols" "features") # Add board common files file(GLOB BOARD_COMMON_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/common/*.cc) @@ -661,6 +661,9 @@ endif() file(GLOB COMMON_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/common/*.ogg) +# Include features directory +include("features/CMakeLists.txt") + # If target chip is ESP32, exclude specific files to avoid build errors if(CONFIG_IDF_TARGET_ESP32) list(REMOVE_ITEM SOURCES "audio/codecs/box_audio_codec.cc" diff --git a/main/application.cc b/main/application.cc index 3cdfe9744e..29f167914b 100644 --- a/main/application.cc +++ b/main/application.cc @@ -10,6 +10,11 @@ #include "assets.h" #include "settings.h" +#if CONFIG_ENABLE_SCHEDULE_REMINDER +#include "features/schedule_reminder/schedule_reminder.h" +#include "features/schedule_reminder/schedule_manager.h" +#endif + #include #include #include @@ -533,6 +538,11 @@ void Application::Start() { SystemInfo::PrintHeapStats(); SetDeviceState(kDeviceStateIdle); + // Initialize schedule reminder feature + #if CONFIG_ENABLE_SCHEDULE_REMINDER + InitializeScheduleReminder(); + #endif + has_server_time_ = ota.HasServerTime(); if (protocol_started) { std::string message = std::string(Lang::Strings::VERSION) + ota.GetCurrentVersion(); @@ -891,4 +901,45 @@ void Application::SetAecMode(AecMode mode) { void Application::PlaySound(const std::string_view& sound) { audio_service_.PlaySound(sound); -} \ No newline at end of file +} + +#if CONFIG_ENABLE_SCHEDULE_REMINDER +void Application::InitializeScheduleReminder() { + auto& schedule_reminder = ScheduleReminder::GetInstance(); + + // 设置提醒回调 - 使用现有 Alert 系统 + schedule_reminder.SetReminderCallback([this](const ScheduleItem& item) { + this->OnScheduleTriggered(item); + }); + + // 初始化日程管理器 + if (!schedule_reminder.Initialize()) { + ESP_LOGE(TAG, "Failed to initialize schedule reminder"); + return; + } + + // 注册 MCP 工具 + #if CONFIG_ENABLE_SCHEDULE_MCP_TOOLS + ScheduleManager::RegisterMcpTools(); + #endif + + ESP_LOGI(TAG, "Schedule reminder feature initialized successfully"); +} + +void Application::OnScheduleTriggered(const ScheduleItem& item) { + // 创建提醒消息 + char message[256]; + if (item.description.empty()) { + snprintf(message, sizeof(message), "提醒: %s", item.title.c_str()); + } else { + snprintf(message, sizeof(message), "提醒: %s - %s", + item.title.c_str(), item.description.c_str()); + } + + // 使用系统现有的通知机制 + Alert("日程提醒", message, "bell", Lang::Sounds::OGG_NOTIFICATION); + + // 记录日志 + ESP_LOGI(TAG, "Schedule triggered: %s", item.title.c_str()); +} +#endif diff --git a/main/features/CMakeLists.txt b/main/features/CMakeLists.txt new file mode 100644 index 0000000000..51c0cdc654 --- /dev/null +++ b/main/features/CMakeLists.txt @@ -0,0 +1,17 @@ +# Features CMakeLists.txt +# This file includes all feature modules + +# Schedule Reminder Feature +if(CONFIG_ENABLE_SCHEDULE_REMINDER) + set(SCHEDULE_REMINDER_SRCS + "schedule_reminder/schedule_reminder.cc" + "schedule_reminder/schedule_manager.cc" + ) + + set(SCHEDULE_REMINDER_INCLUDES + "schedule_reminder" + ) + + list(APPEND COMPONENT_SRCS ${SCHEDULE_REMINDER_SRCS}) + list(APPEND COMPONENT_PRIV_INCLUDES ${SCHEDULE_REMINDER_INCLUDES}) +endif() diff --git a/main/features/schedule_reminder/Kconfig b/main/features/schedule_reminder/Kconfig new file mode 100644 index 0000000000..656a111d43 --- /dev/null +++ b/main/features/schedule_reminder/Kconfig @@ -0,0 +1,30 @@ +menu "Schedule Reminder Features" + config ENABLE_SCHEDULE_REMINDER + bool "Enable Schedule Reminder" + default n + help + Enable schedule reminder functionality + + config MAX_SCHEDULE_ITEMS + int "Maximum schedule items" + range 1 50 + default 20 + depends on ENABLE_SCHEDULE_REMINDER + help + Maximum number of schedule items that can be stored + + config SCHEDULE_CHECK_INTERVAL + int "Schedule check interval (seconds)" + range 10 300 + default 30 + depends on ENABLE_SCHEDULE_REMINDER + help + Interval to check for due schedules + + config ENABLE_SCHEDULE_MCP_TOOLS + bool "Enable Schedule MCP Tools" + default y + depends on ENABLE_SCHEDULE_REMINDER + help + Enable MCP tools for schedule management +endmenu diff --git a/main/features/schedule_reminder/schedule_manager.cc b/main/features/schedule_reminder/schedule_manager.cc new file mode 100644 index 0000000000..d876ca5338 --- /dev/null +++ b/main/features/schedule_reminder/schedule_manager.cc @@ -0,0 +1,194 @@ +#include "schedule_manager.h" +#include +#include + +#define TAG "ScheduleManager" + +void ScheduleManager::RegisterMcpTools() { + auto& mcp_server = McpServer::GetInstance(); + + // 添加日程工具 + mcp_server.AddTool("schedule.add", + "Add a new schedule reminder", + GetAddScheduleProperties(), + AddScheduleTool); + + // 列出日程工具 + mcp_server.AddTool("schedule.list", + "List all schedule reminders", + PropertyList(), + ListSchedulesTool); + + // 删除日程工具 + mcp_server.AddTool("schedule.remove", + "Remove a schedule reminder", + GetRemoveScheduleProperties(), + RemoveScheduleTool); + + // 更新日程工具 + mcp_server.AddTool("schedule.update", + "Update an existing schedule reminder", + GetUpdateScheduleProperties(), + UpdateScheduleTool); + + ESP_LOGI(TAG, "Schedule MCP tools registered"); +} + +PropertyList ScheduleManager::GetAddScheduleProperties() { + PropertyList props; + props.AddProperty("title", "Schedule title", PropertyType::kString, true); + props.AddProperty("description", "Schedule description", PropertyType::kString, false); + props.AddProperty("trigger_time", "Trigger time (Unix timestamp)", PropertyType::kNumber, true); + props.AddProperty("recurring", "Whether this is a recurring schedule", PropertyType::kBoolean, false); + props.AddProperty("repeat_interval", "Repeat interval in seconds", PropertyType::kNumber, false); + return props; +} + +PropertyList ScheduleManager::GetRemoveScheduleProperties() { + PropertyList props; + props.AddProperty("id", "Schedule ID to remove", PropertyType::kString, true); + return props; +} + +PropertyList ScheduleManager::GetUpdateScheduleProperties() { + PropertyList props; + props.AddProperty("id", "Schedule ID to update", PropertyType::kString, true); + props.AddProperty("title", "New schedule title", PropertyType::kString, false); + props.AddProperty("description", "New schedule description", PropertyType::kString, false); + props.AddProperty("trigger_time", "New trigger time (Unix timestamp)", PropertyType::kNumber, false); + props.AddProperty("enabled", "Whether the schedule is enabled", PropertyType::kBoolean, false); + props.AddProperty("recurring", "Whether this is a recurring schedule", PropertyType::kBoolean, false); + props.AddProperty("repeat_interval", "Repeat interval in seconds", PropertyType::kNumber, false); + return props; +} + +bool ScheduleManager::AddScheduleTool(const PropertyList& properties) { + ScheduleItem item; + item.id = std::to_string(time(nullptr)); // Use timestamp as ID + item.title = properties.GetValue("title"); + item.description = properties.GetValue("description", ""); + item.trigger_time = properties.GetValue("trigger_time"); + item.recurring = properties.GetValue("recurring", false); + item.repeat_interval = properties.GetValue("repeat_interval", 0); + item.created_at = std::to_string(time(nullptr)); + + ScheduleError result = ScheduleReminder::GetInstance().AddSchedule(item); + + switch (result) { + case ScheduleError::kSuccess: + ESP_LOGI(TAG, "Schedule added via MCP: %s", item.title.c_str()); + return true; + case ScheduleError::kMaxItemsReached: + ESP_LOGE(TAG, "Failed to add schedule via MCP: maximum items reached"); + return false; + case ScheduleError::kDuplicateId: + ESP_LOGE(TAG, "Failed to add schedule via MCP: duplicate ID"); + return false; + case ScheduleError::kInvalidTime: + ESP_LOGE(TAG, "Failed to add schedule via MCP: invalid trigger time"); + return false; + case ScheduleError::kStorageError: + ESP_LOGE(TAG, "Failed to add schedule via MCP: storage error"); + return false; + case ScheduleError::kNotInitialized: + ESP_LOGE(TAG, "Failed to add schedule via MCP: not initialized"); + return false; + default: + ESP_LOGE(TAG, "Failed to add schedule via MCP: unknown error"); + return false; + } +} + +bool ScheduleManager::ListSchedulesTool(const PropertyList& properties) { + auto schedules = ScheduleReminder::GetInstance().GetSchedules(); + + // 这里可以返回日程列表给 MCP 客户端 + // 实际实现中可能需要格式化输出 + + ESP_LOGI(TAG, "Listed %d schedules via MCP", schedules.size()); + + // 简单记录到日志 + for (const auto& schedule : schedules) { + ESP_LOGI(TAG, "Schedule: %s (ID: %s, Time: %ld)", + schedule.title.c_str(), schedule.id.c_str(), schedule.trigger_time); + } + + return true; +} + +bool ScheduleManager::RemoveScheduleTool(const PropertyList& properties) { + std::string id = properties.GetValue("id"); + ScheduleError result = ScheduleReminder::GetInstance().RemoveSchedule(id); + + switch (result) { + case ScheduleError::kSuccess: + ESP_LOGI(TAG, "Schedule removed via MCP: %s", id.c_str()); + return true; + case ScheduleError::kNotFound: + ESP_LOGE(TAG, "Failed to remove schedule via MCP: schedule not found - %s", id.c_str()); + return false; + case ScheduleError::kStorageError: + ESP_LOGE(TAG, "Failed to remove schedule via MCP: storage error - %s", id.c_str()); + return false; + case ScheduleError::kNotInitialized: + ESP_LOGE(TAG, "Failed to remove schedule via MCP: not initialized - %s", id.c_str()); + return false; + default: + ESP_LOGE(TAG, "Failed to remove schedule via MCP: unknown error - %s", id.c_str()); + return false; + } +} + +bool ScheduleManager::UpdateScheduleTool(const PropertyList& properties) { + std::string id = properties.GetValue("id"); + + // Get existing schedule + ScheduleItem* existing_item = ScheduleReminder::GetInstance().GetSchedule(id); + if (!existing_item) { + ESP_LOGE(TAG, "Schedule not found for update: %s", id.c_str()); + return false; + } + + // Create updated schedule item + ScheduleItem updated_item = *existing_item; + + // Update provided fields + if (properties.HasValue("title")) { + updated_item.title = properties.GetValue("title"); + } + if (properties.HasValue("description")) { + updated_item.description = properties.GetValue("description"); + } + if (properties.HasValue("trigger_time")) { + updated_item.trigger_time = properties.GetValue("trigger_time"); + } + if (properties.HasValue("enabled")) { + updated_item.enabled = properties.GetValue("enabled"); + } + if (properties.HasValue("recurring")) { + updated_item.recurring = properties.GetValue("recurring"); + } + if (properties.HasValue("repeat_interval")) { + updated_item.repeat_interval = properties.GetValue("repeat_interval"); + } + + ScheduleError result = ScheduleReminder::GetInstance().UpdateSchedule(id, updated_item); + + switch (result) { + case ScheduleError::kSuccess: + ESP_LOGI(TAG, "Schedule updated via MCP: %s", id.c_str()); + return true; + case ScheduleError::kNotFound: + ESP_LOGE(TAG, "Failed to update schedule via MCP: schedule not found - %s", id.c_str()); + return false; + case ScheduleError::kStorageError: + ESP_LOGE(TAG, "Failed to update schedule via MCP: storage error - %s", id.c_str()); + return false; + case ScheduleError::kNotInitialized: + ESP_LOGE(TAG, "Failed to update schedule via MCP: not initialized - %s", id.c_str()); + return false; + default: + ESP_LOGE(TAG, "Failed to update schedule via MCP: unknown error - %s", id.c_str()); + return false; + } +} diff --git a/main/features/schedule_reminder/schedule_manager.h b/main/features/schedule_reminder/schedule_manager.h new file mode 100644 index 0000000000..8d5b112fb2 --- /dev/null +++ b/main/features/schedule_reminder/schedule_manager.h @@ -0,0 +1,24 @@ +#ifndef SCHEDULE_MANAGER_H +#define SCHEDULE_MANAGER_H + +#include "mcp_server.h" +#include "schedule_reminder.h" + +class ScheduleManager { +public: + static void RegisterMcpTools(); + +private: + // MCP 工具实现 + static bool AddScheduleTool(const PropertyList& properties); + static bool ListSchedulesTool(const PropertyList& properties); + static bool RemoveScheduleTool(const PropertyList& properties); + static bool UpdateScheduleTool(const PropertyList& properties); + + // 工具属性定义 + static PropertyList GetAddScheduleProperties(); + static PropertyList GetRemoveScheduleProperties(); + static PropertyList GetUpdateScheduleProperties(); +}; + +#endif // SCHEDULE_MANAGER_H diff --git a/main/features/schedule_reminder/schedule_reminder.cc b/main/features/schedule_reminder/schedule_reminder.cc new file mode 100644 index 0000000000..3a471c6719 --- /dev/null +++ b/main/features/schedule_reminder/schedule_reminder.cc @@ -0,0 +1,363 @@ +#include "schedule_reminder.h" +#include +#include +#include "settings.h" +#include +#include + +#define TAG "ScheduleReminder" + +ScheduleReminder& ScheduleReminder::GetInstance() { + static ScheduleReminder instance; + return instance; +} + +ScheduleReminder::ScheduleReminder() + : check_timer_(nullptr), initialized_(false) { +} + +ScheduleReminder::~ScheduleReminder() { + if (check_timer_) { + esp_timer_stop(check_timer_); + esp_timer_delete(check_timer_); + check_timer_ = nullptr; + } +} + +bool ScheduleReminder::Initialize() { + if (initialized_) { + ESP_LOGW(TAG, "Schedule reminder already initialized"); + return true; + } + + std::lock_guard lock(schedules_mutex_); + + LoadSchedules(); + + if (!SetupTimer()) { + ESP_LOGE(TAG, "Failed to setup schedule timer"); + return false; + } + + initialized_ = true; + ESP_LOGI(TAG, "Schedule reminder initialized successfully"); + return true; +} + +void ScheduleReminder::Shutdown() { + std::lock_guard lock(schedules_mutex_); + + if (check_timer_) { + esp_timer_stop(check_timer_); + esp_timer_delete(check_timer_); + check_timer_ = nullptr; + } + + initialized_ = false; + ESP_LOGI(TAG, "Schedule reminder shutdown"); +} + +bool ScheduleReminder::SetupTimer() { + esp_timer_create_args_t timer_args = { + .callback = TimerCallback, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "schedule_check_timer", + .skip_unhandled_events = true + }; + + esp_err_t err = esp_timer_create(&timer_args, &check_timer_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to create schedule timer: %s", esp_err_to_name(err)); + return false; + } + + err = esp_timer_start_periodic(check_timer_, CONFIG_SCHEDULE_CHECK_INTERVAL * 1000000); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to start schedule timer: %s", esp_err_to_name(err)); + esp_timer_delete(check_timer_); + check_timer_ = nullptr; + return false; + } + + ESP_LOGI(TAG, "Schedule timer started with interval: %d seconds", CONFIG_SCHEDULE_CHECK_INTERVAL); + return true; +} + +void ScheduleReminder::TimerCallback(void* arg) { + ScheduleReminder* instance = static_cast(arg); + instance->CheckDueSchedules(); +} + +void ScheduleReminder::CheckDueSchedules() { + if (!initialized_) { + return; + } + + std::lock_guard lock(schedules_mutex_); + + time_t now = time(nullptr); + bool schedules_updated = false; + + for (auto& item : schedules_) { + if (!item.enabled) continue; + + if (item.trigger_time <= now) { + ESP_LOGI(TAG, "Schedule due: %s", item.title.c_str()); + + // Trigger reminder callback + if (reminder_callback_) { + reminder_callback_(item); + } + + // Handle recurring reminders + if (item.recurring && item.repeat_interval > 0) { + item.trigger_time += item.repeat_interval; + schedules_updated = true; + ESP_LOGI(TAG, "Recurring schedule updated: %s, next trigger: %ld", + item.title.c_str(), item.trigger_time); + } else { + item.enabled = false; // One-time reminder, disable + schedules_updated = true; + ESP_LOGI(TAG, "One-time schedule disabled: %s", item.title.c_str()); + } + } + } + + // Save updated schedules + if (schedules_updated) { + SaveSchedules(); + } +} + +ScheduleError ScheduleReminder::AddSchedule(const ScheduleItem& item) { + if (!initialized_) { + ESP_LOGE(TAG, "Schedule reminder not initialized"); + return ScheduleError::kNotInitialized; + } + + // Validate input + if (item.id.empty()) { + ESP_LOGE(TAG, "Cannot add schedule: empty ID"); + return ScheduleError::kInvalidTime; + } + + if (item.trigger_time <= time(nullptr)) { + ESP_LOGE(TAG, "Cannot add schedule: trigger time must be in the future"); + return ScheduleError::kInvalidTime; + } + + std::lock_guard lock(schedules_mutex_); + + if (schedules_.size() >= CONFIG_MAX_SCHEDULE_ITEMS) { + ESP_LOGE(TAG, "Cannot add schedule: maximum items reached (%d)", CONFIG_MAX_SCHEDULE_ITEMS); + return ScheduleError::kMaxItemsReached; + } + + // Check if ID already exists + for (const auto& existing : schedules_) { + if (existing.id == item.id) { + ESP_LOGE(TAG, "Schedule with ID %s already exists", item.id.c_str()); + return ScheduleError::kDuplicateId; + } + } + + schedules_.push_back(item); + + if (!SaveSchedules()) { + ESP_LOGE(TAG, "Failed to save schedules after adding"); + schedules_.pop_back(); // Rollback + return ScheduleError::kStorageError; + } + + ESP_LOGI(TAG, "Schedule added: %s (ID: %s)", item.title.c_str(), item.id.c_str()); + return ScheduleError::kSuccess; +} + +ScheduleError ScheduleReminder::RemoveSchedule(const std::string& id) { + if (!initialized_) { + ESP_LOGE(TAG, "Schedule reminder not initialized"); + return ScheduleError::kNotInitialized; + } + + std::lock_guard lock(schedules_mutex_); + + auto it = std::find_if(schedules_.begin(), schedules_.end(), + [&id](const ScheduleItem& item) { return item.id == id; }); + + if (it != schedules_.end()) { + schedules_.erase(it); + + if (!SaveSchedules()) { + ESP_LOGE(TAG, "Failed to save schedules after removal"); + return ScheduleError::kStorageError; + } + + ESP_LOGI(TAG, "Schedule removed: %s", id.c_str()); + return ScheduleError::kSuccess; + } + + ESP_LOGW(TAG, "Schedule not found for removal: %s", id.c_str()); + return ScheduleError::kNotFound; +} + +ScheduleError ScheduleReminder::UpdateSchedule(const std::string& id, const ScheduleItem& new_item) { + if (!initialized_) { + ESP_LOGE(TAG, "Schedule reminder not initialized"); + return ScheduleError::kNotInitialized; + } + + std::lock_guard lock(schedules_mutex_); + + for (auto& item : schedules_) { + if (item.id == id) { + item = new_item; + + if (!SaveSchedules()) { + ESP_LOGE(TAG, "Failed to save schedules after update"); + return ScheduleError::kStorageError; + } + + ESP_LOGI(TAG, "Schedule updated: %s", id.c_str()); + return ScheduleError::kSuccess; + } + } + + ESP_LOGW(TAG, "Schedule not found for update: %s", id.c_str()); + return ScheduleError::kNotFound; +} + +std::vector ScheduleReminder::GetSchedules() const { + std::lock_guard lock(schedules_mutex_); + return schedules_; +} + +ScheduleItem* ScheduleReminder::GetSchedule(const std::string& id) { + std::lock_guard lock(schedules_mutex_); + + for (auto& item : schedules_) { + if (item.id == id) { + return &item; + } + } + return nullptr; +} + +void ScheduleReminder::LoadSchedules() { + Settings settings("schedule", true); + std::string schedules_json = settings.GetString("schedules"); + + if (schedules_json.empty()) { + ESP_LOGI(TAG, "No saved schedules found"); + return; + } + + cJSON* root = cJSON_Parse(schedules_json.c_str()); + if (!root) { + ESP_LOGE(TAG, "Failed to parse schedules JSON, clearing corrupted data"); + settings.EraseKey("schedules"); + return; + } + + // Data version check + cJSON* version = cJSON_GetObjectItem(root, "version"); + if (!version || version->valueint != 1) { + ESP_LOGW(TAG, "Unsupported schedule data version, skipping load"); + cJSON_Delete(root); + return; + } + + cJSON* schedules_array = cJSON_GetObjectItem(root, "schedules"); + if (cJSON_IsArray(schedules_array)) { + cJSON* schedule_item; + cJSON_ArrayForEach(schedule_item, schedules_array) { + ScheduleItem item; + + cJSON* id = cJSON_GetObjectItem(schedule_item, "id"); + cJSON* title = cJSON_GetObjectItem(schedule_item, "title"); + cJSON* description = cJSON_GetObjectItem(schedule_item, "description"); + cJSON* trigger_time = cJSON_GetObjectItem(schedule_item, "trigger_time"); + cJSON* enabled = cJSON_GetObjectItem(schedule_item, "enabled"); + cJSON* recurring = cJSON_GetObjectItem(schedule_item, "recurring"); + cJSON* repeat_interval = cJSON_GetObjectItem(schedule_item, "repeat_interval"); + cJSON* created_at = cJSON_GetObjectItem(schedule_item, "created_at"); + + if (id && cJSON_IsString(id)) item.id = id->valuestring; + if (title && cJSON_IsString(title)) item.title = title->valuestring; + if (description && cJSON_IsString(description)) item.description = description->valuestring; + if (trigger_time && cJSON_IsNumber(trigger_time)) item.trigger_time = trigger_time->valueint; + if (enabled && cJSON_IsBool(enabled)) item.enabled = cJSON_IsTrue(enabled); + if (recurring && cJSON_IsBool(recurring)) item.recurring = cJSON_IsTrue(recurring); + if (repeat_interval && cJSON_IsNumber(repeat_interval)) item.repeat_interval = repeat_interval->valueint; + if (created_at && cJSON_IsString(created_at)) item.created_at = created_at->valuestring; + + schedules_.push_back(item); + } + } + + cJSON_Delete(root); + ESP_LOGI(TAG, "Loaded %d schedules", schedules_.size()); +} + +bool ScheduleReminder::SaveSchedules() { + cJSON* root = cJSON_CreateObject(); + if (!root) { + ESP_LOGE(TAG, "Failed to create JSON root object"); + return false; + } + + cJSON* schedules_array = cJSON_CreateArray(); + if (!schedules_array) { + ESP_LOGE(TAG, "Failed to create schedules array"); + cJSON_Delete(root); + return false; + } + + for (const auto& item : schedules_) { + cJSON* schedule_obj = cJSON_CreateObject(); + if (!schedule_obj) { + ESP_LOGE(TAG, "Failed to create schedule object"); + cJSON_Delete(root); + return false; + } + + cJSON_AddStringToObject(schedule_obj, "id", item.id.c_str()); + cJSON_AddStringToObject(schedule_obj, "title", item.title.c_str()); + cJSON_AddStringToObject(schedule_obj, "description", item.description.c_str()); + cJSON_AddNumberToObject(schedule_obj, "trigger_time", item.trigger_time); + cJSON_AddBoolToObject(schedule_obj, "enabled", item.enabled); + cJSON_AddBoolToObject(schedule_obj, "recurring", item.recurring); + cJSON_AddNumberToObject(schedule_obj, "repeat_interval", item.repeat_interval); + cJSON_AddStringToObject(schedule_obj, "created_at", item.created_at.c_str()); + + cJSON_AddItemToArray(schedules_array, schedule_obj); + } + + cJSON_AddItemToObject(root, "schedules", schedules_array); + cJSON_AddNumberToObject(root, "version", 1); // Data version control + + char* json_str = cJSON_PrintUnformatted(root); + if (!json_str) { + ESP_LOGE(TAG, "Failed to serialize JSON"); + cJSON_Delete(root); + return false; + } + + Settings settings("schedule", true); + bool success = settings.SetString("schedules", json_str); + + free(json_str); + cJSON_Delete(root); + + if (!success) { + ESP_LOGE(TAG, "Failed to save schedules to settings"); + return false; + } + + ESP_LOGI(TAG, "Schedules saved successfully (%d items)", schedules_.size()); + return true; +} + +void ScheduleReminder::SetReminderCallback(std::function callback) { + reminder_callback_ = callback; +} diff --git a/main/features/schedule_reminder/schedule_reminder.h b/main/features/schedule_reminder/schedule_reminder.h new file mode 100644 index 0000000000..d4646e8fcf --- /dev/null +++ b/main/features/schedule_reminder/schedule_reminder.h @@ -0,0 +1,148 @@ +#ifndef SCHEDULE_REMINDER_H +#define SCHEDULE_REMINDER_H + +#include +#include +#include +#include +#include + +/** + * @brief Schedule item data structure + * + * Represents a single schedule reminder with all necessary information + * for triggering and managing the reminder. + */ +struct ScheduleItem { + std::string id; ///< Unique identifier + std::string title; ///< Reminder title + std::string description; ///< Detailed description + time_t trigger_time; ///< Trigger time (Unix timestamp) + bool enabled; ///< Whether the reminder is enabled + bool recurring; ///< Whether this is a recurring reminder + int repeat_interval; ///< Repeat interval in seconds + std::string created_at; ///< Creation timestamp + + ScheduleItem() : + trigger_time(0), + enabled(true), + recurring(false), + repeat_interval(0) + {} +}; + +/** + * @brief Schedule reminder error codes + */ +enum class ScheduleError { + kSuccess = 0, ///< Operation completed successfully + kMaxItemsReached, ///< Maximum number of schedule items reached + kDuplicateId, ///< Schedule with this ID already exists + kInvalidTime, ///< Invalid trigger time specified + kStorageError, ///< Error accessing storage + kNotFound, ///< Schedule item not found + kNotInitialized ///< Schedule reminder not initialized +}; + +/** + * @brief Schedule reminder management class + * + * Provides functionality for managing and triggering schedule reminders + * using the system's existing notification mechanisms. This class follows + * the singleton pattern to ensure global consistency. + */ +class ScheduleReminder { +public: + /** + * @brief Get the singleton instance + * @return Reference to the singleton instance + */ + static ScheduleReminder& GetInstance(); + + /** + * @brief Initialize the schedule reminder system + * @return true if initialization successful, false otherwise + */ + bool Initialize(); + + /** + * @brief Shutdown the schedule reminder system + */ + void Shutdown(); + + /** + * @brief Add a new schedule + * @param item Schedule item to add + * @return ScheduleError indicating operation result + */ + ScheduleError AddSchedule(const ScheduleItem& item); + + /** + * @brief Remove a schedule by ID + * @param id Schedule ID to remove + * @return ScheduleError indicating operation result + */ + ScheduleError RemoveSchedule(const std::string& id); + + /** + * @brief Update an existing schedule + * @param id Schedule ID to update + * @param item New schedule data + * @return ScheduleError indicating operation result + */ + ScheduleError UpdateSchedule(const std::string& id, const ScheduleItem& item); + + /** + * @brief Get all schedules + * @return Vector of all schedule items + */ + std::vector GetSchedules() const; + + /** + * @brief Get a specific schedule by ID + * @param id Schedule ID to retrieve + * @return Pointer to schedule item if found, nullptr otherwise + */ + ScheduleItem* GetSchedule(const std::string& id); + + /** + * @brief Check for due schedules and trigger reminders + */ + void CheckDueSchedules(); + + /** + * @brief Set the reminder callback function + * @param callback Function to call when a reminder is triggered + */ + void SetReminderCallback(std::function callback); + +private: + /// Private constructor for singleton pattern + ScheduleReminder(); + + /// Destructor + ~ScheduleReminder(); + + /// Setup the periodic timer for checking schedules + void SetupTimer(); + + /// Load schedules from persistent storage + void LoadSchedules(); + + /// Save schedules to persistent storage + void SaveSchedules(); + + /// Process a due schedule item + void ProcessDueSchedule(const ScheduleItem& item); + + /// Timer callback function + static void TimerCallback(void* arg); + + std::vector schedules_; ///< List of schedule items + std::function reminder_callback_; ///< Reminder callback function + esp_timer_handle_t check_timer_; ///< Timer handle for periodic checks + bool initialized_; ///< Initialization flag + mutable std::mutex schedules_mutex_; ///< Mutex for thread safety +}; + +#endif // SCHEDULE_REMINDER_H