From 0dfb5f1535937e455d4a74320033f09820295d3e Mon Sep 17 00:00:00 2001 From: StillHammer Date: Tue, 25 Nov 2025 22:13:16 +0800 Subject: [PATCH] chore: Normalize line endings and update project documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Normalize CRLF to LF across all source files - Replace CLAUDE.md.old with updated CLAUDE.md - Standardize configuration file formatting - Update module source files with consistent line endings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 148 +-- CLAUDE.md | 64 ++ CLAUDE.md.old | 112 --- CMakeLists.txt | 178 ++-- README.md | 444 ++++----- config/notification.json | 12 +- config/scheduler.json | 14 +- docs/GROVEENGINE_GUIDE.md | 1498 ++++++++++++++-------------- src/main.cpp | 526 +++++----- src/modules/NotificationModule.cpp | 344 +++---- src/modules/NotificationModule.h | 192 ++-- src/modules/SchedulerModule.cpp | 358 +++---- src/modules/SchedulerModule.h | 188 ++-- 13 files changed, 2015 insertions(+), 2063 deletions(-) create mode 100644 CLAUDE.md delete mode 100644 CLAUDE.md.old diff --git a/.gitignore b/.gitignore index d7dd2af..bd2a7f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,74 +1,74 @@ -# Build Outputs -bin/ -obj/ -*.exe -*.dll -*.pdb - -# Visual Studio / Rider -.vs/ -.vscode/ -.idea/ -*.user -*.suo -*.cache -*.docstates -*.tmp - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# NuGet -*.nupkg -*.snupkg -packages/ -!packages/build/ - -# Test Results -TestResults/ -[Tt]est[Rr]esult*/ -*.trx -*.coverage -*.coveragexml - -# Runtime Data & Logs -data/ -logs/ -*.log -*.csv - -# Configuration Secrets -appsettings.Development.json -appsettings.Production.json -config/secrets.json -*.key - -# Audio Models & Cache -whisper-models/ -*.bin -audio-cache/ - -# OS Generated -.DS_Store -Thumbs.db -*.tmp -*.temp - -# Large Media Files -*.mp4 -*.avi -*.mov -*.wmv -*.mp3 -*.wavbuild/ -*.o -*.a -*.so -*.dll -build/ -*.o -*.a -*.so -*.dll +# Build Outputs +bin/ +obj/ +*.exe +*.dll +*.pdb + +# Visual Studio / Rider +.vs/ +.vscode/ +.idea/ +*.user +*.suo +*.cache +*.docstates +*.tmp + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# NuGet +*.nupkg +*.snupkg +packages/ +!packages/build/ + +# Test Results +TestResults/ +[Tt]est[Rr]esult*/ +*.trx +*.coverage +*.coveragexml + +# Runtime Data & Logs +data/ +logs/ +*.log +*.csv + +# Configuration Secrets +appsettings.Development.json +appsettings.Production.json +config/secrets.json +*.key + +# Audio Models & Cache +whisper-models/ +*.bin +audio-cache/ + +# OS Generated +.DS_Store +Thumbs.db +*.tmp +*.temp + +# Large Media Files +*.mp4 +*.avi +*.mov +*.wmv +*.mp3 +*.wavbuild/ +*.o +*.a +*.so +*.dll +build/ +*.o +*.a +*.so +*.dll diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6db1d9a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,64 @@ +# AISSIA - Assistant Personnel Intelligent + +Assistant pour gérer le temps, l'hyperfocus et l'apprentissage de langues. Basé sur **GroveEngine** (C++17 hot-reload module system). + +## Statut + +| Module | Status | Description | +|--------|--------|-------------| +| SchedulerModule | Fait | Détection hyperfocus, rappels pauses | +| NotificationModule | Fait | Alertes système, TTS, priorités | +| AIAssistantModule | TODO | Intégration LLM (Claude API) | +| LanguageLearningModule | TODO | Pratique langue cible | +| DataModule | TODO | SQLite persistence | + +## Règles de Développement + +### Contraintes Modules +- **200-300 lignes max** par module +- **Logique métier pure** (pas de threading/network dans les modules) +- **Communication JSON** via IIO pub/sub +- **Hot-reload ready** : sérialiser tout l'état dans `getState()` + +### NEVER +- `cmake ..` ou `#include "../"` (dépendances parent) +- Modules > 300 lignes +- Infrastructure dans les modules + +### ALWAYS +- Build autonome depuis le module +- JSON pour toute communication inter-modules +- Topics pub/sub : `module:event` (ex: `scheduler:hyperfocus_detected`) + +## Structure + +``` +Aissia/ +├── src/ +│ ├── main.cpp # Main loop 10Hz + hot-reload +│ └── modules/ # Modules implémentés +│ ├── SchedulerModule.* +│ └── NotificationModule.* +├── config/ # JSON config par module +├── external/GroveEngine/ # Engine (symlink) +└── docs/ # Documentation détaillée +``` + +## Build + +```bash +cmake -B build && cmake --build build -j4 +./build/aissia + +# Hot-reload: rebuild modules seulement +cmake --build build --target modules +``` + +## Documentation + +| Doc | Contenu | +|-----|---------| +| `docs/GROVEENGINE_GUIDE.md` | API complète IModule, IIO, IDataNode, hot-reload | +| `docs/project-overview.md` | Architecture AISSIA, phases dev | +| `docs/architecture/intelligent-document-retrieval.md` | AIAssistantModule: retrieval agentique, multi-provider LLM | +| `README.md` | Quick start, roadmap | diff --git a/CLAUDE.md.old b/CLAUDE.md.old deleted file mode 100644 index aa6d787..0000000 --- a/CLAUDE.md.old +++ /dev/null @@ -1,112 +0,0 @@ -# AISSIA - Assistant Personnel Intelligent - -## Description - -Assistant personnel qui aide à gérer le temps, l'hyperfocus et l'apprentissage de langues. Interventions proactives avec l'IA pour forcer les transitions et planifier intelligemment. - -## Fonctionnalités MVP - -1. **SchedulerModule** : Planning de tâches, détection hyperfocus (max 2h), rappels de pauses -2. **AIAssistantModule** : Interventions contextuelles via Claude, dialogue naturel -3. **LanguageLearningModule** : Conversation dans langue cible, corrections intelligentes -4. **NotificationModule** : Alertes système Windows, TTS, support multilingue -5. **DataModule** : SQLite local, historique, métriques - -## Architecture Technique - -### Principe : Architecture Modulaire WarFactory - -Hot-reload 0.4ms, modules 200-300 lignes, build autonome par module. - -``` -MainServer Process -├── CoordinationModule → Charge appconfig.json -├── DebugEngine → SequentialModuleSystem -└── Modules (.dll) - ├── scheduler.dll - ├── ai-assistant.dll - ├── language.dll - ├── notification.dll - └── data.dll -``` - -### 5 Interfaces Fondamentales - -```cpp -ICoordinationModule → Orchestrateur global -IEngine → DebugEngine → HighPerfEngine -IModuleSystem → Sequential → Threaded → Cluster -IModule → Logique métier pure (200 lignes max) -IIO → IntraIO → LocalIO → NetworkIO -``` - -### Contraintes Critiques - -**Modules** : -- 200-300 lignes maximum -- Logique métier pure (pas de threading, network) -- Communication JSON uniquement -- Build autonome : `cmake .` depuis module - -**NEVER** : -- ❌ `cmake ..` ou `#include "../"` -- ❌ Modules > 300 lignes -- ❌ Dépendances entre modules - -**ALWAYS** : -- ✅ Build autonome -- ✅ JSON communication -- ✅ Hot-reload ready -- ✅ Task-centric design - -### Workflow Développement - -```bash -cd modules/scheduler/ -cmake . # NEVER cmake .. -make -./scheduler-test - -# Edit SchedulerModule.cpp → Save → Hot-reload 0.4ms -``` - -### Communication Inter-Modules (JSON) - -```json -{"event": "hyperfocus", "duration_minutes": 120} - → AIAssistantModule -{"type": "break_suggestion", "message": "Pause ?"} - → NotificationModule -{"notification": "system_toast", "tts": true} -``` - -## Structure Projet - -``` -Aissia/ -├── CLAUDE.md -├── docs/ # Documentation détaillée -├── modules/ # À créer -│ ├── scheduler/ -│ ├── ai-assistant/ -│ ├── language-learning/ -│ ├── notification/ -│ └── data/ -└── src/ # Infrastructure -``` - -## Priorités - -1. Infrastructure (IModule, IEngine, hot-reload) -2. SchedulerModule -3. NotificationModule -4. AIAssistantModule -5. LanguageLearningModule -6. DataModule - -## Références - -- `docs/README.md` : Vue d'ensemble -- `docs/architecture/architecture-technique.md` : Architecture complète -- `CDCDraft.md` : Cahier des charges -- GroveEngine : Architecture source WarFactory (accès via `.claude/settings.json`) diff --git a/CMakeLists.txt b/CMakeLists.txt index d70262b..275800d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,89 +1,89 @@ -cmake_minimum_required(VERSION 3.20) -project(Aissia VERSION 0.1.0 LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Export compile commands for IDE support -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -# ============================================================================ -# GroveEngine Integration -# ============================================================================ -set(GROVE_BUILD_TESTS OFF CACHE BOOL "Disable GroveEngine tests" FORCE) -add_subdirectory(external/GroveEngine) - -# ============================================================================ -# Main Executable -# ============================================================================ -add_executable(aissia - src/main.cpp -) - -target_link_libraries(aissia PRIVATE - GroveEngine::impl - spdlog::spdlog -) - -target_include_directories(aissia PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src -) - -# ============================================================================ -# Hot-Reloadable Modules (.so) -# ============================================================================ - -# SchedulerModule - Gestion du temps et détection hyperfocus -add_library(SchedulerModule SHARED - src/modules/SchedulerModule.cpp -) -target_link_libraries(SchedulerModule PRIVATE - GroveEngine::impl - spdlog::spdlog -) -set_target_properties(SchedulerModule PROPERTIES - PREFIX "lib" - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules -) - -# NotificationModule - Alertes système et TTS -add_library(NotificationModule SHARED - src/modules/NotificationModule.cpp -) -target_link_libraries(NotificationModule PRIVATE - GroveEngine::impl - spdlog::spdlog -) -set_target_properties(NotificationModule PROPERTIES - PREFIX "lib" - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules -) - -# Futurs modules (décommenter quand implémentés): -# add_library(AIAssistantModule SHARED src/modules/AIAssistantModule.cpp) -# add_library(LanguageLearningModule SHARED src/modules/LanguageLearningModule.cpp) -# add_library(DataModule SHARED src/modules/DataModule.cpp) - -# ============================================================================ -# Copy config files to build directory -# ============================================================================ -file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/ - DESTINATION ${CMAKE_BINARY_DIR}/config) - -# ============================================================================ -# Development targets -# ============================================================================ - -# Quick rebuild of modules only (for hot-reload workflow) -add_custom_target(modules - DEPENDS SchedulerModule NotificationModule - COMMENT "Building hot-reloadable modules only" -) - -# Run Aissia -add_custom_target(run - COMMAND $ - DEPENDS aissia modules - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - COMMENT "Running Aissia" -) +cmake_minimum_required(VERSION 3.20) +project(Aissia VERSION 0.1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Export compile commands for IDE support +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# ============================================================================ +# GroveEngine Integration +# ============================================================================ +set(GROVE_BUILD_TESTS OFF CACHE BOOL "Disable GroveEngine tests" FORCE) +add_subdirectory(external/GroveEngine) + +# ============================================================================ +# Main Executable +# ============================================================================ +add_executable(aissia + src/main.cpp +) + +target_link_libraries(aissia PRIVATE + GroveEngine::impl + spdlog::spdlog +) + +target_include_directories(aissia PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +# ============================================================================ +# Hot-Reloadable Modules (.so) +# ============================================================================ + +# SchedulerModule - Gestion du temps et détection hyperfocus +add_library(SchedulerModule SHARED + src/modules/SchedulerModule.cpp +) +target_link_libraries(SchedulerModule PRIVATE + GroveEngine::impl + spdlog::spdlog +) +set_target_properties(SchedulerModule PROPERTIES + PREFIX "lib" + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules +) + +# NotificationModule - Alertes système et TTS +add_library(NotificationModule SHARED + src/modules/NotificationModule.cpp +) +target_link_libraries(NotificationModule PRIVATE + GroveEngine::impl + spdlog::spdlog +) +set_target_properties(NotificationModule PROPERTIES + PREFIX "lib" + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules +) + +# Futurs modules (décommenter quand implémentés): +# add_library(AIAssistantModule SHARED src/modules/AIAssistantModule.cpp) +# add_library(LanguageLearningModule SHARED src/modules/LanguageLearningModule.cpp) +# add_library(DataModule SHARED src/modules/DataModule.cpp) + +# ============================================================================ +# Copy config files to build directory +# ============================================================================ +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/ + DESTINATION ${CMAKE_BINARY_DIR}/config) + +# ============================================================================ +# Development targets +# ============================================================================ + +# Quick rebuild of modules only (for hot-reload workflow) +add_custom_target(modules + DEPENDS SchedulerModule NotificationModule + COMMENT "Building hot-reloadable modules only" +) + +# Run Aissia +add_custom_target(run + COMMAND $ + DEPENDS aissia modules + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Running Aissia" +) diff --git a/README.md b/README.md index 0e79c42..6dc843f 100644 --- a/README.md +++ b/README.md @@ -1,222 +1,222 @@ -# AISSIA - AI Smart Schedule & Interactive Assistant - -**AISSIA** is an intelligent personal assistant for time management, hyperfocus detection, and language learning, powered by **GroveEngine**. - -## What is AISSIA? - -AISSIA helps manage: -- **Hyperfocus**: Detects when you've been working too long and need a break -- **Time Management**: Intelligent scheduling and task planning -- **Language Learning**: Interactive practice in target languages -- **Notifications**: Context-aware alerts with TTS support - -## Built on GroveEngine - -AISSIA leverages **GroveEngine**, a C++17 hot-reload module system that enables: - -- **Hot-Reload**: Modify code at runtime without losing state (<1ms reload latency) -- **Modular Architecture**: Self-contained modules (200-300 lines each) -- **Pub/Sub Communication**: Decoupled inter-module messaging -- **State Preservation**: Automatic state serialization across reloads -- **Configuration Hot-Reload**: Update settings without code changes - -### Why GroveEngine? - -✅ **Ultra-fast Development**: Hot-reload validated at 0.4ms -✅ **Type Safety**: Strong C++ typing, no "wildcode" -✅ **Proven Architecture**: Production-ready module system -✅ **Privacy-First**: Local mode, data never uploaded -✅ **Progressive Evolution**: MVP → Production → Cloud without rewrite - -## Project Structure - -``` -Aissia/ -├── external/ -│ └── GroveEngine/ # GroveEngine (symlink) -├── src/ -│ ├── main.cpp # Main application loop -│ └── modules/ -│ ├── SchedulerModule.* # Time management & hyperfocus -│ ├── NotificationModule.* # Alerts & TTS -│ ├── AIAssistantModule.* # (TODO) LLM integration -│ ├── LanguageLearningModule.* # (TODO) Language practice -│ └── DataModule.* # (TODO) SQLite persistence -├── config/ -│ ├── scheduler.json -│ └── notification.json -├── docs/ -│ ├── README.md # Project documentation -│ └── GROVEENGINE_GUIDE.md # GroveEngine user guide -└── CMakeLists.txt -``` - -## Modules - -### Implemented - -**SchedulerModule**: Time management and hyperfocus detection -- Tracks work sessions and break intervals -- Detects hyperfocus based on configurable threshold (default: 120 minutes) -- Reminds for breaks (default: every 45 minutes) -- Task estimation and tracking - -**NotificationModule**: System alerts with priority levels -- Priority-based notifications (LOW, NORMAL, HIGH, URGENT) -- Silent mode (respects URGENT priority) -- TTS support (configurable) -- Multilingual support (fr/en/jp) -- Queue management with rate limiting - -### Planned - -**AIAssistantModule**: LLM-powered contextual interventions -- Claude API integration -- Context-aware suggestions -- Intelligent document retrieval - -**LanguageLearningModule**: Language practice and learning -- Conversation in target language -- Vocabulary tracking -- Progress monitoring - -**DataModule**: SQLite persistence -- Task history -- Session analytics -- Configuration backup - -## Getting Started - -### Prerequisites - -- CMake 3.20+ -- C++17 compiler (GCC/Clang) -- GroveEngine (included via symlink) - -### Build - -```bash -# Configure -cmake -B build - -# Build everything -cmake --build build -j4 - -# Build modules only (for hot-reload workflow) -cmake --build build --target modules - -# Run -./build/aissia -``` - -### Hot-Reload Workflow - -1. Start AISSIA: `./build/aissia` -2. Edit a module: `src/modules/SchedulerModule.cpp` -3. Rebuild: `cmake --build build --target modules` -4. **Module reloads automatically with state preserved** - -### Configuration - -Configuration files in `config/`: - -**scheduler.json**: -```json -{ - "hyperfocusThresholdMinutes": 120, - "breakReminderIntervalMinutes": 45, - "breakDurationMinutes": 10, - "workdayStartHour": 9, - "workdayEndHour": 18 -} -``` - -**notification.json**: -```json -{ - "language": "fr", - "silentMode": false, - "ttsEnabled": false, - "maxQueueSize": 50 -} -``` - -## Documentation - -- **[docs/README.md](docs/README.md)**: Project documentation and architecture -- **[docs/GROVEENGINE_GUIDE.md](docs/GROVEENGINE_GUIDE.md)**: Complete GroveEngine user guide -- **[CDCDraft.md](CDCDraft.md)**: Detailed requirements specification (French) - -## Development Philosophy - -### MVP-First - -- **Phase 1 (Required)**: Local Windows mode only -- **Phase 2+ (Optional)**: Cloud features if needed -- **Configuration-Driven**: 90% of needs via JSON -- **Simplicity First**: Complexity emerges from interaction - -### Module Development - -Each module is a self-contained unit (~200-300 lines): - -1. Implements `IModule` interface -2. Pure business logic (no infrastructure code) -3. Communicates via `IIO` pub/sub -4. Serializes state for hot-reload -5. Configurable via `IDataNode` - -See `docs/GROVEENGINE_GUIDE.md` for detailed module development guide. - -## Architecture - -AISSIA runs on a simple loop (10Hz for assistant workload): - -``` -┌─────────────────────────────────────────────┐ -│ Main Loop (10Hz) │ -│ │ -│ ┌────────────────────────────────────┐ │ -│ │ 1. Check for module file changes │ │ -│ │ 2. Hot-reload if modified │ │ -│ │ 3. Process all modules │ │ -│ │ 4. Route inter-module messages │ │ -│ │ 5. Sleep to maintain 10Hz │ │ -│ └────────────────────────────────────┘ │ -└─────────────────────────────────────────────┘ -``` - -Modules communicate via topics: -- `scheduler:hyperfocus_detected` → NotificationModule alerts user -- `notification:alert_sent` → DataModule logs event -- `aiassistant:suggestion` → NotificationModule displays suggestion - -## Technical Stack - -- **Core**: C++17 -- **Module System**: GroveEngine -- **Logging**: spdlog -- **Data Format**: JSON (nlohmann/json) -- **Build**: CMake 3.20+ -- **Future**: SQLite (DataModule), Claude API (AIAssistantModule) - -## Roadmap - -- [x] Project setup with GroveEngine -- [x] SchedulerModule (time management & hyperfocus) -- [x] NotificationModule (alerts & TTS) -- [ ] AIAssistantModule (LLM integration) -- [ ] LanguageLearningModule (language practice) -- [ ] DataModule (SQLite persistence) -- [ ] Windows Toast notifications -- [ ] Real TTS integration -- [ ] Claude API integration - -## License - -To be determined - -## Links - -- **GroveEngine**: [../GroveEngine](../GroveEngine) -- **Claude Code**: https://docs.claude.com/en/docs/claude-code +# AISSIA - AI Smart Schedule & Interactive Assistant + +**AISSIA** is an intelligent personal assistant for time management, hyperfocus detection, and language learning, powered by **GroveEngine**. + +## What is AISSIA? + +AISSIA helps manage: +- **Hyperfocus**: Detects when you've been working too long and need a break +- **Time Management**: Intelligent scheduling and task planning +- **Language Learning**: Interactive practice in target languages +- **Notifications**: Context-aware alerts with TTS support + +## Built on GroveEngine + +AISSIA leverages **GroveEngine**, a C++17 hot-reload module system that enables: + +- **Hot-Reload**: Modify code at runtime without losing state (<1ms reload latency) +- **Modular Architecture**: Self-contained modules (200-300 lines each) +- **Pub/Sub Communication**: Decoupled inter-module messaging +- **State Preservation**: Automatic state serialization across reloads +- **Configuration Hot-Reload**: Update settings without code changes + +### Why GroveEngine? + +✅ **Ultra-fast Development**: Hot-reload validated at 0.4ms +✅ **Type Safety**: Strong C++ typing, no "wildcode" +✅ **Proven Architecture**: Production-ready module system +✅ **Privacy-First**: Local mode, data never uploaded +✅ **Progressive Evolution**: MVP → Production → Cloud without rewrite + +## Project Structure + +``` +Aissia/ +├── external/ +│ └── GroveEngine/ # GroveEngine (symlink) +├── src/ +│ ├── main.cpp # Main application loop +│ └── modules/ +│ ├── SchedulerModule.* # Time management & hyperfocus +│ ├── NotificationModule.* # Alerts & TTS +│ ├── AIAssistantModule.* # (TODO) LLM integration +│ ├── LanguageLearningModule.* # (TODO) Language practice +│ └── DataModule.* # (TODO) SQLite persistence +├── config/ +│ ├── scheduler.json +│ └── notification.json +├── docs/ +│ ├── README.md # Project documentation +│ └── GROVEENGINE_GUIDE.md # GroveEngine user guide +└── CMakeLists.txt +``` + +## Modules + +### Implemented + +**SchedulerModule**: Time management and hyperfocus detection +- Tracks work sessions and break intervals +- Detects hyperfocus based on configurable threshold (default: 120 minutes) +- Reminds for breaks (default: every 45 minutes) +- Task estimation and tracking + +**NotificationModule**: System alerts with priority levels +- Priority-based notifications (LOW, NORMAL, HIGH, URGENT) +- Silent mode (respects URGENT priority) +- TTS support (configurable) +- Multilingual support (fr/en/jp) +- Queue management with rate limiting + +### Planned + +**AIAssistantModule**: LLM-powered contextual interventions +- Claude API integration +- Context-aware suggestions +- Intelligent document retrieval + +**LanguageLearningModule**: Language practice and learning +- Conversation in target language +- Vocabulary tracking +- Progress monitoring + +**DataModule**: SQLite persistence +- Task history +- Session analytics +- Configuration backup + +## Getting Started + +### Prerequisites + +- CMake 3.20+ +- C++17 compiler (GCC/Clang) +- GroveEngine (included via symlink) + +### Build + +```bash +# Configure +cmake -B build + +# Build everything +cmake --build build -j4 + +# Build modules only (for hot-reload workflow) +cmake --build build --target modules + +# Run +./build/aissia +``` + +### Hot-Reload Workflow + +1. Start AISSIA: `./build/aissia` +2. Edit a module: `src/modules/SchedulerModule.cpp` +3. Rebuild: `cmake --build build --target modules` +4. **Module reloads automatically with state preserved** + +### Configuration + +Configuration files in `config/`: + +**scheduler.json**: +```json +{ + "hyperfocusThresholdMinutes": 120, + "breakReminderIntervalMinutes": 45, + "breakDurationMinutes": 10, + "workdayStartHour": 9, + "workdayEndHour": 18 +} +``` + +**notification.json**: +```json +{ + "language": "fr", + "silentMode": false, + "ttsEnabled": false, + "maxQueueSize": 50 +} +``` + +## Documentation + +- **[docs/README.md](docs/README.md)**: Project documentation and architecture +- **[docs/GROVEENGINE_GUIDE.md](docs/GROVEENGINE_GUIDE.md)**: Complete GroveEngine user guide +- **[CDCDraft.md](CDCDraft.md)**: Detailed requirements specification (French) + +## Development Philosophy + +### MVP-First + +- **Phase 1 (Required)**: Local Windows mode only +- **Phase 2+ (Optional)**: Cloud features if needed +- **Configuration-Driven**: 90% of needs via JSON +- **Simplicity First**: Complexity emerges from interaction + +### Module Development + +Each module is a self-contained unit (~200-300 lines): + +1. Implements `IModule` interface +2. Pure business logic (no infrastructure code) +3. Communicates via `IIO` pub/sub +4. Serializes state for hot-reload +5. Configurable via `IDataNode` + +See `docs/GROVEENGINE_GUIDE.md` for detailed module development guide. + +## Architecture + +AISSIA runs on a simple loop (10Hz for assistant workload): + +``` +┌─────────────────────────────────────────────┐ +│ Main Loop (10Hz) │ +│ │ +│ ┌────────────────────────────────────┐ │ +│ │ 1. Check for module file changes │ │ +│ │ 2. Hot-reload if modified │ │ +│ │ 3. Process all modules │ │ +│ │ 4. Route inter-module messages │ │ +│ │ 5. Sleep to maintain 10Hz │ │ +│ └────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ +``` + +Modules communicate via topics: +- `scheduler:hyperfocus_detected` → NotificationModule alerts user +- `notification:alert_sent` → DataModule logs event +- `aiassistant:suggestion` → NotificationModule displays suggestion + +## Technical Stack + +- **Core**: C++17 +- **Module System**: GroveEngine +- **Logging**: spdlog +- **Data Format**: JSON (nlohmann/json) +- **Build**: CMake 3.20+ +- **Future**: SQLite (DataModule), Claude API (AIAssistantModule) + +## Roadmap + +- [x] Project setup with GroveEngine +- [x] SchedulerModule (time management & hyperfocus) +- [x] NotificationModule (alerts & TTS) +- [ ] AIAssistantModule (LLM integration) +- [ ] LanguageLearningModule (language practice) +- [ ] DataModule (SQLite persistence) +- [ ] Windows Toast notifications +- [ ] Real TTS integration +- [ ] Claude API integration + +## License + +To be determined + +## Links + +- **GroveEngine**: [../GroveEngine](../GroveEngine) +- **Claude Code**: https://docs.claude.com/en/docs/claude-code diff --git a/config/notification.json b/config/notification.json index 788db63..8e52d84 100644 --- a/config/notification.json +++ b/config/notification.json @@ -1,6 +1,6 @@ -{ - "language": "fr", - "silentMode": false, - "ttsEnabled": false, - "maxQueueSize": 50 -} +{ + "language": "fr", + "silentMode": false, + "ttsEnabled": false, + "maxQueueSize": 50 +} diff --git a/config/scheduler.json b/config/scheduler.json index b57fa13..6ad1832 100644 --- a/config/scheduler.json +++ b/config/scheduler.json @@ -1,7 +1,7 @@ -{ - "hyperfocusThresholdMinutes": 120, - "breakReminderIntervalMinutes": 45, - "breakDurationMinutes": 10, - "workdayStartHour": 9, - "workdayEndHour": 18 -} +{ + "hyperfocusThresholdMinutes": 120, + "breakReminderIntervalMinutes": 45, + "breakDurationMinutes": 10, + "workdayStartHour": 9, + "workdayEndHour": 18 +} diff --git a/docs/GROVEENGINE_GUIDE.md b/docs/GROVEENGINE_GUIDE.md index 0388694..f3545bb 100644 --- a/docs/GROVEENGINE_GUIDE.md +++ b/docs/GROVEENGINE_GUIDE.md @@ -1,749 +1,749 @@ -# GroveEngine User Guide - -GroveEngine is a C++17 hot-reload module system designed for building modular applications with runtime code replacement capabilities. - -## Table of Contents - -1. [Overview](#overview) -2. [Core Concepts](#core-concepts) -3. [Project Setup](#project-setup) -4. [Creating Modules](#creating-modules) -5. [Module Lifecycle](#module-lifecycle) -6. [Inter-Module Communication](#inter-module-communication) -7. [Hot-Reload](#hot-reload) -8. [Configuration Management](#configuration-management) -9. [Task Scheduling](#task-scheduling) -10. [API Reference](#api-reference) - ---- - -## Overview - -GroveEngine provides: - -- **Hot-Reload**: Replace module code at runtime without losing state -- **Modular Architecture**: Self-contained modules with clear interfaces -- **Pub/Sub Communication**: Decoupled inter-module messaging via topics -- **State Preservation**: Automatic state serialization across reloads -- **Configuration Hot-Reload**: Update module configuration without code changes - -### Design Philosophy - -- Modules contain pure business logic (200-300 lines recommended) -- No infrastructure code in modules (threading, networking, persistence) -- All data via `IDataNode` abstraction (backend agnostic) -- Pull-based message processing (modules control when they read messages) - ---- - -## Core Concepts - -### IModule - -The base interface all modules implement. Defines the contract for: -- Processing logic (`process()`) -- Configuration (`setConfiguration()`, `getConfiguration()`) -- State management (`getState()`, `setState()`) -- Lifecycle (`shutdown()`) - -### IDataNode - -Hierarchical data structure for configuration, state, and messages. Supports: -- Typed accessors (`getString()`, `getInt()`, `getDouble()`, `getBool()`) -- Tree navigation (`getChild()`, `getChildNames()`) -- Pattern matching (`getChildrenByNameMatch()`) - -### IIO - -Pub/Sub communication interface: -- `publish()`: Send messages to topics -- `subscribe()`: Listen to topic patterns -- `pullMessage()`: Consume received messages - -### ModuleLoader - -Handles dynamic loading of `.so` files: -- `load()`: Load a module from shared library -- `reload()`: Hot-reload with state preservation -- `unload()`: Clean unload - ---- - -## Project Setup - -### Directory Structure - -``` -MyProject/ -├── CMakeLists.txt -├── external/ -│ └── GroveEngine/ # GroveEngine (submodule or symlink) -├── src/ -│ ├── main.cpp -│ └── modules/ -│ ├── MyModule.h -│ └── MyModule.cpp -└── config/ - └── mymodule.json -``` - -### CMakeLists.txt - -```cmake -cmake_minimum_required(VERSION 3.20) -project(MyProject VERSION 0.1.0 LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -# ============================================================================ -# GroveEngine Integration -# ============================================================================ -set(GROVE_BUILD_TESTS OFF CACHE BOOL "Disable GroveEngine tests" FORCE) -add_subdirectory(external/GroveEngine) - -# ============================================================================ -# Main Executable -# ============================================================================ -add_executable(myapp src/main.cpp) - -target_link_libraries(myapp PRIVATE - GroveEngine::impl - spdlog::spdlog -) - -# ============================================================================ -# Hot-Reloadable Modules (.so) -# ============================================================================ -add_library(MyModule SHARED - src/modules/MyModule.cpp -) - -target_link_libraries(MyModule PRIVATE - GroveEngine::impl - spdlog::spdlog -) - -set_target_properties(MyModule PROPERTIES - PREFIX "lib" - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules -) - -# ============================================================================ -# Copy config files to build directory -# ============================================================================ -file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/ - DESTINATION ${CMAKE_BINARY_DIR}/config) - -# ============================================================================ -# Convenience targets -# ============================================================================ -add_custom_target(modules - DEPENDS MyModule - COMMENT "Building hot-reloadable modules only" -) -``` - -### Linking GroveEngine - -Option 1: Git submodule -```bash -git submodule add external/GroveEngine -``` - -Option 2: Symlink (local development) -```bash -ln -s /path/to/GroveEngine external/GroveEngine -``` - ---- - -## Creating Modules - -### Module Header - -```cpp -// src/modules/MyModule.h -#pragma once - -#include -#include -#include -#include -#include - -namespace myapp { - -class MyModule : public grove::IModule { -public: - MyModule(); - - // Required IModule interface - void process(const grove::IDataNode& input) override; - void setConfiguration(const grove::IDataNode& configNode, - grove::IIO* io, - grove::ITaskScheduler* scheduler) override; - const grove::IDataNode& getConfiguration() override; - std::unique_ptr getHealthStatus() override; - void shutdown() override; - std::unique_ptr getState() override; - void setState(const grove::IDataNode& state) override; - std::string getType() const override { return "mymodule"; } - bool isIdle() const override { return true; } - -private: - std::shared_ptr m_logger; - std::unique_ptr m_config; - grove::IIO* m_io = nullptr; - - // Module state (will be preserved across hot-reloads) - int m_counter = 0; - std::string m_status; -}; - -} // namespace myapp - -// Required C exports for dynamic loading -extern "C" { - grove::IModule* createModule(); - void destroyModule(grove::IModule* module); -} -``` - -### Module Implementation - -```cpp -// src/modules/MyModule.cpp -#include "MyModule.h" -#include -#include - -namespace myapp { - -MyModule::MyModule() { - m_logger = spdlog::get("MyModule"); - if (!m_logger) { - m_logger = spdlog::stdout_color_mt("MyModule"); - } - m_config = std::make_unique("config"); -} - -void MyModule::setConfiguration(const grove::IDataNode& configNode, - grove::IIO* io, - grove::ITaskScheduler* scheduler) { - m_io = io; - m_config = std::make_unique("config"); - - // Read configuration values with defaults - m_status = configNode.getString("initialStatus", "ready"); - int startCount = configNode.getInt("startCount", 0); - - m_logger->info("MyModule configured: status={}, startCount={}", - m_status, startCount); -} - -const grove::IDataNode& MyModule::getConfiguration() { - return *m_config; -} - -void MyModule::process(const grove::IDataNode& input) { - // Get frame timing from input - double deltaTime = input.getDouble("deltaTime", 0.016); - int frameCount = input.getInt("frameCount", 0); - - // Your processing logic here - m_counter++; - - // Process incoming messages - while (m_io && m_io->hasMessages() > 0) { - auto msg = m_io->pullMessage(); - m_logger->debug("Received message on topic: {}", msg.topic); - // Handle message... - } - - // Publish events if needed - if (m_counter % 100 == 0) { - auto event = std::make_unique("event"); - event->setInt("counter", m_counter); - m_io->publish("mymodule:milestone", std::move(event)); - } -} - -std::unique_ptr MyModule::getHealthStatus() { - auto status = std::make_unique("health"); - status->setString("status", "running"); - status->setInt("counter", m_counter); - return status; -} - -void MyModule::shutdown() { - m_logger->info("MyModule shutting down, counter={}", m_counter); -} - -// ============================================================================ -// State Serialization (Critical for Hot-Reload) -// ============================================================================ - -std::unique_ptr MyModule::getState() { - auto state = std::make_unique("state"); - - // Serialize all state that must survive hot-reload - state->setInt("counter", m_counter); - state->setString("status", m_status); - - m_logger->debug("State saved: counter={}", m_counter); - return state; -} - -void MyModule::setState(const grove::IDataNode& state) { - // Restore state after hot-reload - m_counter = state.getInt("counter", 0); - m_status = state.getString("status", "ready"); - - m_logger->info("State restored: counter={}", m_counter); -} - -} // namespace myapp - -// ============================================================================ -// C Export Functions (Required for dlopen) -// ============================================================================ - -extern "C" { - -grove::IModule* createModule() { - return new myapp::MyModule(); -} - -void destroyModule(grove::IModule* module) { - delete module; -} - -} -``` - ---- - -## Module Lifecycle - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Module Lifecycle │ -└─────────────────────────────────────────────────────────────────┘ - -1. LOAD - ModuleLoader::load(path, name) - │ - ▼ - dlopen() → createModule() - │ - ▼ - setConfiguration(config, io, scheduler) - │ - ▼ - Module Ready - -2. PROCESS (Main Loop) - ┌──────────────────────┐ - │ process(input) │◄────┐ - │ - Read deltaTime │ │ - │ - Update state │ │ - │ - Pull messages │ │ - │ - Publish events │ │ - └──────────────────────┘ │ - │ │ - └────────────────────┘ - -3. HOT-RELOAD - File change detected - │ - ▼ - getState() → Save state - │ - ▼ - unload() → dlclose() - │ - ▼ - load(path, name, isReload=true) - │ - ▼ - setConfiguration(config, io, scheduler) - │ - ▼ - setState(savedState) - │ - ▼ - Module continues with preserved state - -4. SHUTDOWN - shutdown() - │ - ▼ - destroyModule() - │ - ▼ - dlclose() -``` - ---- - -## Inter-Module Communication - -### IIO Pub/Sub System - -Modules communicate via topics using publish/subscribe pattern. - -#### Publishing Messages - -```cpp -void MyModule::process(const grove::IDataNode& input) { - // Create message data - auto data = std::make_unique("data"); - data->setString("event", "player_moved"); - data->setDouble("x", 100.5); - data->setDouble("y", 200.3); - - // Publish to topic - m_io->publish("game:player:position", std::move(data)); -} -``` - -#### Subscribing to Topics - -```cpp -void MyModule::setConfiguration(const grove::IDataNode& configNode, - grove::IIO* io, - grove::ITaskScheduler* scheduler) { - m_io = io; - - // Subscribe to specific topic - m_io->subscribe("game:player:*"); - - // Subscribe with low-frequency batching (for non-critical updates) - grove::SubscriptionConfig config; - config.batchInterval = 1000; // 1 second batches - m_io->subscribeLowFreq("analytics:*", config); -} -``` - -#### Processing Messages - -```cpp -void MyModule::process(const grove::IDataNode& input) { - // Pull-based: module controls when to process messages - while (m_io->hasMessages() > 0) { - grove::Message msg = m_io->pullMessage(); - - if (msg.topic == "game:player:position") { - double x = msg.data->getDouble("x", 0.0); - double y = msg.data->getDouble("y", 0.0); - // Handle position update... - } - } -} -``` - -### Topic Patterns - -Topics support wildcard matching: - -| Pattern | Matches | -|---------|---------| -| `game:player:*` | `game:player:position`, `game:player:health` | -| `economy:*` | `economy:prices`, `economy:trade` | -| `*:error` | `network:error`, `database:error` | - ---- - -## Hot-Reload - -### How It Works - -1. **File Watcher** detects `.so` modification -2. **State Extraction**: `getState()` serializes module state -3. **Unload**: Old library closed with `dlclose()` -4. **Load**: New library loaded with `dlopen()` (cache bypass via temp copy) -5. **Configure**: `setConfiguration()` called with same config -6. **Restore**: `setState()` restores serialized state - -### Implementing Hot-Reload Support - -```cpp -// Critical: Serialize ALL state that must survive reload -std::unique_ptr MyModule::getState() { - auto state = std::make_unique("state"); - - // Primitives - state->setInt("counter", m_counter); - state->setDouble("health", m_health); - state->setString("name", m_name); - state->setBool("active", m_active); - - // Complex state: serialize to JSON child nodes - auto entitiesNode = std::make_unique("entities"); - for (const auto& entity : m_entities) { - auto entityNode = std::make_unique(entity.id); - entityNode->setDouble("x", entity.x); - entityNode->setDouble("y", entity.y); - entitiesNode->setChild(entity.id, std::move(entityNode)); - } - state->setChild("entities", std::move(entitiesNode)); - - return state; -} - -void MyModule::setState(const grove::IDataNode& state) { - // Restore primitives - m_counter = state.getInt("counter", 0); - m_health = state.getDouble("health", 100.0); - m_name = state.getString("name", "default"); - m_active = state.getBool("active", true); - - // Restore complex state - m_entities.clear(); - auto* entitiesNode = state.getChildReadOnly("entities"); - if (entitiesNode) { - for (const auto& name : entitiesNode->getChildNames()) { - auto* entityNode = entitiesNode->getChildReadOnly(name); - if (entityNode) { - Entity e; - e.id = name; - e.x = entityNode->getDouble("x", 0.0); - e.y = entityNode->getDouble("y", 0.0); - m_entities.push_back(e); - } - } - } -} -``` - -### File Watcher Example - -```cpp -class FileWatcher { -public: - void watch(const std::string& path) { - if (fs::exists(path)) { - m_lastModified[path] = fs::last_write_time(path); - } - } - - bool hasChanged(const std::string& path) { - if (!fs::exists(path)) return false; - - auto currentTime = fs::last_write_time(path); - auto it = m_lastModified.find(path); - - if (it == m_lastModified.end()) { - m_lastModified[path] = currentTime; - return false; - } - - if (currentTime != it->second) { - it->second = currentTime; - return true; - } - return false; - } - -private: - std::unordered_map m_lastModified; -}; -``` - ---- - -## Configuration Management - -### JSON Configuration Files - -```json -// config/mymodule.json -{ - "initialStatus": "ready", - "startCount": 0, - "maxItems": 100, - "debugMode": false, - "updateRate": 0.016 -} -``` - -### Loading Configuration - -```cpp -std::unique_ptr loadConfig(const std::string& path) { - if (fs::exists(path)) { - std::ifstream file(path); - nlohmann::json j; - file >> j; - return std::make_unique("config", j); - } - // Return empty config with defaults - return std::make_unique("config"); -} -``` - -### Runtime Config Updates - -Modules can optionally support runtime configuration changes: - -```cpp -bool MyModule::updateConfig(const grove::IDataNode& newConfig) { - // Validate new config - int maxItems = newConfig.getInt("maxItems", 100); - if (maxItems < 1 || maxItems > 10000) { - m_logger->warn("Invalid maxItems: {}", maxItems); - return false; - } - - // Apply changes - m_maxItems = maxItems; - m_debugMode = newConfig.getBool("debugMode", false); - - m_logger->info("Config updated: maxItems={}", m_maxItems); - return true; -} -``` - ---- - -## Task Scheduling - -For computationally expensive operations, delegate to `ITaskScheduler`: - -```cpp -void MyModule::setConfiguration(const grove::IDataNode& configNode, - grove::IIO* io, - grove::ITaskScheduler* scheduler) { - m_scheduler = scheduler; // Store reference -} - -void MyModule::process(const grove::IDataNode& input) { - // Delegate expensive pathfinding calculation - if (needsPathfinding) { - auto taskData = std::make_unique("task"); - taskData->setDouble("startX", unit.x); - taskData->setDouble("startY", unit.y); - taskData->setDouble("targetX", target.x); - taskData->setDouble("targetY", target.y); - taskData->setString("unitId", unit.id); - - m_scheduler->scheduleTask("pathfinding", std::move(taskData)); - } - - // Check for completed tasks - while (m_scheduler->hasCompletedTasks() > 0) { - auto result = m_scheduler->getCompletedTask(); - std::string unitId = result->getString("unitId", ""); - // Apply pathfinding result... - } -} -``` - ---- - -## API Reference - -### IDataNode - -| Method | Description | -|--------|-------------| -| `getString(name, default)` | Get string property | -| `getInt(name, default)` | Get integer property | -| `getDouble(name, default)` | Get double property | -| `getBool(name, default)` | Get boolean property | -| `setString(name, value)` | Set string property | -| `setInt(name, value)` | Set integer property | -| `setDouble(name, value)` | Set double property | -| `setBool(name, value)` | Set boolean property | -| `hasProperty(name)` | Check if property exists | -| `getChild(name)` | Get child node (transfers ownership) | -| `getChildReadOnly(name)` | Get child node (no ownership transfer) | -| `setChild(name, node)` | Add/replace child node | -| `getChildNames()` | Get names of all children | - -### IIO - -| Method | Description | -|--------|-------------| -| `publish(topic, data)` | Publish message to topic | -| `subscribe(pattern, config)` | Subscribe to topic pattern | -| `subscribeLowFreq(pattern, config)` | Subscribe with batching | -| `hasMessages()` | Count of pending messages | -| `pullMessage()` | Consume one message | -| `getHealth()` | Get IO health metrics | - -### IModule - -| Method | Description | -|--------|-------------| -| `process(input)` | Main processing method | -| `setConfiguration(config, io, scheduler)` | Initialize module | -| `getConfiguration()` | Get current config | -| `getState()` | Serialize state for hot-reload | -| `setState(state)` | Restore state after hot-reload | -| `getHealthStatus()` | Get module health report | -| `shutdown()` | Clean shutdown | -| `isIdle()` | Check if safe to hot-reload | -| `getType()` | Get module type identifier | - -### ModuleLoader - -| Method | Description | -|--------|-------------| -| `load(path, name, isReload)` | Load module from .so | -| `reload(module)` | Hot-reload with state preservation | -| `unload()` | Unload current module | -| `isLoaded()` | Check if module is loaded | -| `getLoadedPath()` | Get path of loaded module | - -### IOFactory - -| Method | Description | -|--------|-------------| -| `create(type, instanceId)` | Create IO by type string | -| `create(IOType, instanceId)` | Create IO by enum | -| `createFromConfig(config, instanceId)` | Create IO from config | - -**IO Types:** -- `"intra"` / `IOType::INTRA` - Same-process (development) -- `"local"` / `IOType::LOCAL` - Same-machine (production single-server) -- `"network"` / `IOType::NETWORK` - Distributed (MMO scale) - ---- - -## Building and Running - -```bash -# Configure -cmake -B build - -# Build everything -cmake --build build -j4 - -# Build modules only (for hot-reload workflow) -cmake --build build --target modules - -# Run -./build/myapp -``` - -### Hot-Reload Workflow - -1. Start application: `./build/myapp` -2. Edit module source: `src/modules/MyModule.cpp` -3. Rebuild module: `cmake --build build --target modules` -4. Application detects change and hot-reloads automatically - ---- - -## Best Practices - -1. **Keep modules small**: 200-300 lines of pure business logic -2. **No infrastructure code**: Let GroveEngine handle threading, persistence -3. **Serialize all state**: Everything in `getState()` survives hot-reload -4. **Use typed accessors**: `getInt()`, `getString()` with sensible defaults -5. **Pull-based messaging**: Process messages in `process()`, not callbacks -6. **Validate config**: Check configuration values in `setConfiguration()` -7. **Log appropriately**: Debug for development, Info for production events +# GroveEngine User Guide + +GroveEngine is a C++17 hot-reload module system designed for building modular applications with runtime code replacement capabilities. + +## Table of Contents + +1. [Overview](#overview) +2. [Core Concepts](#core-concepts) +3. [Project Setup](#project-setup) +4. [Creating Modules](#creating-modules) +5. [Module Lifecycle](#module-lifecycle) +6. [Inter-Module Communication](#inter-module-communication) +7. [Hot-Reload](#hot-reload) +8. [Configuration Management](#configuration-management) +9. [Task Scheduling](#task-scheduling) +10. [API Reference](#api-reference) + +--- + +## Overview + +GroveEngine provides: + +- **Hot-Reload**: Replace module code at runtime without losing state +- **Modular Architecture**: Self-contained modules with clear interfaces +- **Pub/Sub Communication**: Decoupled inter-module messaging via topics +- **State Preservation**: Automatic state serialization across reloads +- **Configuration Hot-Reload**: Update module configuration without code changes + +### Design Philosophy + +- Modules contain pure business logic (200-300 lines recommended) +- No infrastructure code in modules (threading, networking, persistence) +- All data via `IDataNode` abstraction (backend agnostic) +- Pull-based message processing (modules control when they read messages) + +--- + +## Core Concepts + +### IModule + +The base interface all modules implement. Defines the contract for: +- Processing logic (`process()`) +- Configuration (`setConfiguration()`, `getConfiguration()`) +- State management (`getState()`, `setState()`) +- Lifecycle (`shutdown()`) + +### IDataNode + +Hierarchical data structure for configuration, state, and messages. Supports: +- Typed accessors (`getString()`, `getInt()`, `getDouble()`, `getBool()`) +- Tree navigation (`getChild()`, `getChildNames()`) +- Pattern matching (`getChildrenByNameMatch()`) + +### IIO + +Pub/Sub communication interface: +- `publish()`: Send messages to topics +- `subscribe()`: Listen to topic patterns +- `pullMessage()`: Consume received messages + +### ModuleLoader + +Handles dynamic loading of `.so` files: +- `load()`: Load a module from shared library +- `reload()`: Hot-reload with state preservation +- `unload()`: Clean unload + +--- + +## Project Setup + +### Directory Structure + +``` +MyProject/ +├── CMakeLists.txt +├── external/ +│ └── GroveEngine/ # GroveEngine (submodule or symlink) +├── src/ +│ ├── main.cpp +│ └── modules/ +│ ├── MyModule.h +│ └── MyModule.cpp +└── config/ + └── mymodule.json +``` + +### CMakeLists.txt + +```cmake +cmake_minimum_required(VERSION 3.20) +project(MyProject VERSION 0.1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# ============================================================================ +# GroveEngine Integration +# ============================================================================ +set(GROVE_BUILD_TESTS OFF CACHE BOOL "Disable GroveEngine tests" FORCE) +add_subdirectory(external/GroveEngine) + +# ============================================================================ +# Main Executable +# ============================================================================ +add_executable(myapp src/main.cpp) + +target_link_libraries(myapp PRIVATE + GroveEngine::impl + spdlog::spdlog +) + +# ============================================================================ +# Hot-Reloadable Modules (.so) +# ============================================================================ +add_library(MyModule SHARED + src/modules/MyModule.cpp +) + +target_link_libraries(MyModule PRIVATE + GroveEngine::impl + spdlog::spdlog +) + +set_target_properties(MyModule PROPERTIES + PREFIX "lib" + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules +) + +# ============================================================================ +# Copy config files to build directory +# ============================================================================ +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/ + DESTINATION ${CMAKE_BINARY_DIR}/config) + +# ============================================================================ +# Convenience targets +# ============================================================================ +add_custom_target(modules + DEPENDS MyModule + COMMENT "Building hot-reloadable modules only" +) +``` + +### Linking GroveEngine + +Option 1: Git submodule +```bash +git submodule add external/GroveEngine +``` + +Option 2: Symlink (local development) +```bash +ln -s /path/to/GroveEngine external/GroveEngine +``` + +--- + +## Creating Modules + +### Module Header + +```cpp +// src/modules/MyModule.h +#pragma once + +#include +#include +#include +#include +#include + +namespace myapp { + +class MyModule : public grove::IModule { +public: + MyModule(); + + // Required IModule interface + void process(const grove::IDataNode& input) override; + void setConfiguration(const grove::IDataNode& configNode, + grove::IIO* io, + grove::ITaskScheduler* scheduler) override; + const grove::IDataNode& getConfiguration() override; + std::unique_ptr getHealthStatus() override; + void shutdown() override; + std::unique_ptr getState() override; + void setState(const grove::IDataNode& state) override; + std::string getType() const override { return "mymodule"; } + bool isIdle() const override { return true; } + +private: + std::shared_ptr m_logger; + std::unique_ptr m_config; + grove::IIO* m_io = nullptr; + + // Module state (will be preserved across hot-reloads) + int m_counter = 0; + std::string m_status; +}; + +} // namespace myapp + +// Required C exports for dynamic loading +extern "C" { + grove::IModule* createModule(); + void destroyModule(grove::IModule* module); +} +``` + +### Module Implementation + +```cpp +// src/modules/MyModule.cpp +#include "MyModule.h" +#include +#include + +namespace myapp { + +MyModule::MyModule() { + m_logger = spdlog::get("MyModule"); + if (!m_logger) { + m_logger = spdlog::stdout_color_mt("MyModule"); + } + m_config = std::make_unique("config"); +} + +void MyModule::setConfiguration(const grove::IDataNode& configNode, + grove::IIO* io, + grove::ITaskScheduler* scheduler) { + m_io = io; + m_config = std::make_unique("config"); + + // Read configuration values with defaults + m_status = configNode.getString("initialStatus", "ready"); + int startCount = configNode.getInt("startCount", 0); + + m_logger->info("MyModule configured: status={}, startCount={}", + m_status, startCount); +} + +const grove::IDataNode& MyModule::getConfiguration() { + return *m_config; +} + +void MyModule::process(const grove::IDataNode& input) { + // Get frame timing from input + double deltaTime = input.getDouble("deltaTime", 0.016); + int frameCount = input.getInt("frameCount", 0); + + // Your processing logic here + m_counter++; + + // Process incoming messages + while (m_io && m_io->hasMessages() > 0) { + auto msg = m_io->pullMessage(); + m_logger->debug("Received message on topic: {}", msg.topic); + // Handle message... + } + + // Publish events if needed + if (m_counter % 100 == 0) { + auto event = std::make_unique("event"); + event->setInt("counter", m_counter); + m_io->publish("mymodule:milestone", std::move(event)); + } +} + +std::unique_ptr MyModule::getHealthStatus() { + auto status = std::make_unique("health"); + status->setString("status", "running"); + status->setInt("counter", m_counter); + return status; +} + +void MyModule::shutdown() { + m_logger->info("MyModule shutting down, counter={}", m_counter); +} + +// ============================================================================ +// State Serialization (Critical for Hot-Reload) +// ============================================================================ + +std::unique_ptr MyModule::getState() { + auto state = std::make_unique("state"); + + // Serialize all state that must survive hot-reload + state->setInt("counter", m_counter); + state->setString("status", m_status); + + m_logger->debug("State saved: counter={}", m_counter); + return state; +} + +void MyModule::setState(const grove::IDataNode& state) { + // Restore state after hot-reload + m_counter = state.getInt("counter", 0); + m_status = state.getString("status", "ready"); + + m_logger->info("State restored: counter={}", m_counter); +} + +} // namespace myapp + +// ============================================================================ +// C Export Functions (Required for dlopen) +// ============================================================================ + +extern "C" { + +grove::IModule* createModule() { + return new myapp::MyModule(); +} + +void destroyModule(grove::IModule* module) { + delete module; +} + +} +``` + +--- + +## Module Lifecycle + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Module Lifecycle │ +└─────────────────────────────────────────────────────────────────┘ + +1. LOAD + ModuleLoader::load(path, name) + │ + ▼ + dlopen() → createModule() + │ + ▼ + setConfiguration(config, io, scheduler) + │ + ▼ + Module Ready + +2. PROCESS (Main Loop) + ┌──────────────────────┐ + │ process(input) │◄────┐ + │ - Read deltaTime │ │ + │ - Update state │ │ + │ - Pull messages │ │ + │ - Publish events │ │ + └──────────────────────┘ │ + │ │ + └────────────────────┘ + +3. HOT-RELOAD + File change detected + │ + ▼ + getState() → Save state + │ + ▼ + unload() → dlclose() + │ + ▼ + load(path, name, isReload=true) + │ + ▼ + setConfiguration(config, io, scheduler) + │ + ▼ + setState(savedState) + │ + ▼ + Module continues with preserved state + +4. SHUTDOWN + shutdown() + │ + ▼ + destroyModule() + │ + ▼ + dlclose() +``` + +--- + +## Inter-Module Communication + +### IIO Pub/Sub System + +Modules communicate via topics using publish/subscribe pattern. + +#### Publishing Messages + +```cpp +void MyModule::process(const grove::IDataNode& input) { + // Create message data + auto data = std::make_unique("data"); + data->setString("event", "player_moved"); + data->setDouble("x", 100.5); + data->setDouble("y", 200.3); + + // Publish to topic + m_io->publish("game:player:position", std::move(data)); +} +``` + +#### Subscribing to Topics + +```cpp +void MyModule::setConfiguration(const grove::IDataNode& configNode, + grove::IIO* io, + grove::ITaskScheduler* scheduler) { + m_io = io; + + // Subscribe to specific topic + m_io->subscribe("game:player:*"); + + // Subscribe with low-frequency batching (for non-critical updates) + grove::SubscriptionConfig config; + config.batchInterval = 1000; // 1 second batches + m_io->subscribeLowFreq("analytics:*", config); +} +``` + +#### Processing Messages + +```cpp +void MyModule::process(const grove::IDataNode& input) { + // Pull-based: module controls when to process messages + while (m_io->hasMessages() > 0) { + grove::Message msg = m_io->pullMessage(); + + if (msg.topic == "game:player:position") { + double x = msg.data->getDouble("x", 0.0); + double y = msg.data->getDouble("y", 0.0); + // Handle position update... + } + } +} +``` + +### Topic Patterns + +Topics support wildcard matching: + +| Pattern | Matches | +|---------|---------| +| `game:player:*` | `game:player:position`, `game:player:health` | +| `economy:*` | `economy:prices`, `economy:trade` | +| `*:error` | `network:error`, `database:error` | + +--- + +## Hot-Reload + +### How It Works + +1. **File Watcher** detects `.so` modification +2. **State Extraction**: `getState()` serializes module state +3. **Unload**: Old library closed with `dlclose()` +4. **Load**: New library loaded with `dlopen()` (cache bypass via temp copy) +5. **Configure**: `setConfiguration()` called with same config +6. **Restore**: `setState()` restores serialized state + +### Implementing Hot-Reload Support + +```cpp +// Critical: Serialize ALL state that must survive reload +std::unique_ptr MyModule::getState() { + auto state = std::make_unique("state"); + + // Primitives + state->setInt("counter", m_counter); + state->setDouble("health", m_health); + state->setString("name", m_name); + state->setBool("active", m_active); + + // Complex state: serialize to JSON child nodes + auto entitiesNode = std::make_unique("entities"); + for (const auto& entity : m_entities) { + auto entityNode = std::make_unique(entity.id); + entityNode->setDouble("x", entity.x); + entityNode->setDouble("y", entity.y); + entitiesNode->setChild(entity.id, std::move(entityNode)); + } + state->setChild("entities", std::move(entitiesNode)); + + return state; +} + +void MyModule::setState(const grove::IDataNode& state) { + // Restore primitives + m_counter = state.getInt("counter", 0); + m_health = state.getDouble("health", 100.0); + m_name = state.getString("name", "default"); + m_active = state.getBool("active", true); + + // Restore complex state + m_entities.clear(); + auto* entitiesNode = state.getChildReadOnly("entities"); + if (entitiesNode) { + for (const auto& name : entitiesNode->getChildNames()) { + auto* entityNode = entitiesNode->getChildReadOnly(name); + if (entityNode) { + Entity e; + e.id = name; + e.x = entityNode->getDouble("x", 0.0); + e.y = entityNode->getDouble("y", 0.0); + m_entities.push_back(e); + } + } + } +} +``` + +### File Watcher Example + +```cpp +class FileWatcher { +public: + void watch(const std::string& path) { + if (fs::exists(path)) { + m_lastModified[path] = fs::last_write_time(path); + } + } + + bool hasChanged(const std::string& path) { + if (!fs::exists(path)) return false; + + auto currentTime = fs::last_write_time(path); + auto it = m_lastModified.find(path); + + if (it == m_lastModified.end()) { + m_lastModified[path] = currentTime; + return false; + } + + if (currentTime != it->second) { + it->second = currentTime; + return true; + } + return false; + } + +private: + std::unordered_map m_lastModified; +}; +``` + +--- + +## Configuration Management + +### JSON Configuration Files + +```json +// config/mymodule.json +{ + "initialStatus": "ready", + "startCount": 0, + "maxItems": 100, + "debugMode": false, + "updateRate": 0.016 +} +``` + +### Loading Configuration + +```cpp +std::unique_ptr loadConfig(const std::string& path) { + if (fs::exists(path)) { + std::ifstream file(path); + nlohmann::json j; + file >> j; + return std::make_unique("config", j); + } + // Return empty config with defaults + return std::make_unique("config"); +} +``` + +### Runtime Config Updates + +Modules can optionally support runtime configuration changes: + +```cpp +bool MyModule::updateConfig(const grove::IDataNode& newConfig) { + // Validate new config + int maxItems = newConfig.getInt("maxItems", 100); + if (maxItems < 1 || maxItems > 10000) { + m_logger->warn("Invalid maxItems: {}", maxItems); + return false; + } + + // Apply changes + m_maxItems = maxItems; + m_debugMode = newConfig.getBool("debugMode", false); + + m_logger->info("Config updated: maxItems={}", m_maxItems); + return true; +} +``` + +--- + +## Task Scheduling + +For computationally expensive operations, delegate to `ITaskScheduler`: + +```cpp +void MyModule::setConfiguration(const grove::IDataNode& configNode, + grove::IIO* io, + grove::ITaskScheduler* scheduler) { + m_scheduler = scheduler; // Store reference +} + +void MyModule::process(const grove::IDataNode& input) { + // Delegate expensive pathfinding calculation + if (needsPathfinding) { + auto taskData = std::make_unique("task"); + taskData->setDouble("startX", unit.x); + taskData->setDouble("startY", unit.y); + taskData->setDouble("targetX", target.x); + taskData->setDouble("targetY", target.y); + taskData->setString("unitId", unit.id); + + m_scheduler->scheduleTask("pathfinding", std::move(taskData)); + } + + // Check for completed tasks + while (m_scheduler->hasCompletedTasks() > 0) { + auto result = m_scheduler->getCompletedTask(); + std::string unitId = result->getString("unitId", ""); + // Apply pathfinding result... + } +} +``` + +--- + +## API Reference + +### IDataNode + +| Method | Description | +|--------|-------------| +| `getString(name, default)` | Get string property | +| `getInt(name, default)` | Get integer property | +| `getDouble(name, default)` | Get double property | +| `getBool(name, default)` | Get boolean property | +| `setString(name, value)` | Set string property | +| `setInt(name, value)` | Set integer property | +| `setDouble(name, value)` | Set double property | +| `setBool(name, value)` | Set boolean property | +| `hasProperty(name)` | Check if property exists | +| `getChild(name)` | Get child node (transfers ownership) | +| `getChildReadOnly(name)` | Get child node (no ownership transfer) | +| `setChild(name, node)` | Add/replace child node | +| `getChildNames()` | Get names of all children | + +### IIO + +| Method | Description | +|--------|-------------| +| `publish(topic, data)` | Publish message to topic | +| `subscribe(pattern, config)` | Subscribe to topic pattern | +| `subscribeLowFreq(pattern, config)` | Subscribe with batching | +| `hasMessages()` | Count of pending messages | +| `pullMessage()` | Consume one message | +| `getHealth()` | Get IO health metrics | + +### IModule + +| Method | Description | +|--------|-------------| +| `process(input)` | Main processing method | +| `setConfiguration(config, io, scheduler)` | Initialize module | +| `getConfiguration()` | Get current config | +| `getState()` | Serialize state for hot-reload | +| `setState(state)` | Restore state after hot-reload | +| `getHealthStatus()` | Get module health report | +| `shutdown()` | Clean shutdown | +| `isIdle()` | Check if safe to hot-reload | +| `getType()` | Get module type identifier | + +### ModuleLoader + +| Method | Description | +|--------|-------------| +| `load(path, name, isReload)` | Load module from .so | +| `reload(module)` | Hot-reload with state preservation | +| `unload()` | Unload current module | +| `isLoaded()` | Check if module is loaded | +| `getLoadedPath()` | Get path of loaded module | + +### IOFactory + +| Method | Description | +|--------|-------------| +| `create(type, instanceId)` | Create IO by type string | +| `create(IOType, instanceId)` | Create IO by enum | +| `createFromConfig(config, instanceId)` | Create IO from config | + +**IO Types:** +- `"intra"` / `IOType::INTRA` - Same-process (development) +- `"local"` / `IOType::LOCAL` - Same-machine (production single-server) +- `"network"` / `IOType::NETWORK` - Distributed (MMO scale) + +--- + +## Building and Running + +```bash +# Configure +cmake -B build + +# Build everything +cmake --build build -j4 + +# Build modules only (for hot-reload workflow) +cmake --build build --target modules + +# Run +./build/myapp +``` + +### Hot-Reload Workflow + +1. Start application: `./build/myapp` +2. Edit module source: `src/modules/MyModule.cpp` +3. Rebuild module: `cmake --build build --target modules` +4. Application detects change and hot-reloads automatically + +--- + +## Best Practices + +1. **Keep modules small**: 200-300 lines of pure business logic +2. **No infrastructure code**: Let GroveEngine handle threading, persistence +3. **Serialize all state**: Everything in `getState()` survives hot-reload +4. **Use typed accessors**: `getInt()`, `getString()` with sensible defaults +5. **Pull-based messaging**: Process messages in `process()`, not callbacks +6. **Validate config**: Check configuration values in `setConfiguration()` +7. **Log appropriately**: Debug for development, Info for production events diff --git a/src/main.cpp b/src/main.cpp index c4d9c6c..13ddbdb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,263 +1,263 @@ -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; - -// Global flag for clean shutdown -static volatile bool g_running = true; - -void signalHandler(int signal) { - spdlog::info("Signal {} reçu, arrêt en cours...", signal); - g_running = false; -} - -// Simple file watcher for hot-reload -class FileWatcher { -public: - void watch(const std::string& path) { - if (fs::exists(path)) { - m_lastModified[path] = fs::last_write_time(path); - } - } - - bool hasChanged(const std::string& path) { - if (!fs::exists(path)) return false; - - auto currentTime = fs::last_write_time(path); - auto it = m_lastModified.find(path); - - if (it == m_lastModified.end()) { - m_lastModified[path] = currentTime; - return false; - } - - if (currentTime != it->second) { - it->second = currentTime; - return true; - } - return false; - } - -private: - std::unordered_map m_lastModified; -}; - -// Load JSON config file -std::unique_ptr loadConfig(const std::string& path) { - if (fs::exists(path)) { - std::ifstream file(path); - nlohmann::json j; - file >> j; - auto config = std::make_unique("config", j); - spdlog::info("Config chargée: {}", path); - return config; - } else { - spdlog::warn("Config non trouvée: {}, utilisation des défauts", path); - return std::make_unique("config"); - } -} - -// Module entry in our simple manager -struct ModuleEntry { - std::string name; - std::string configFile; - std::string path; - std::unique_ptr loader; - std::unique_ptr io; - grove::IModule* module = nullptr; -}; - -int main(int argc, char* argv[]) { - // Setup logging - auto console = spdlog::stdout_color_mt("Aissia"); - spdlog::set_default_logger(console); - spdlog::set_level(spdlog::level::debug); - spdlog::set_pattern("[%H:%M:%S.%e] [%n] [%^%l%$] %v"); - - spdlog::info("========================================"); - spdlog::info(" AISSIA - Assistant Personnel IA"); - spdlog::info(" Powered by GroveEngine"); - spdlog::info("========================================"); - - // Signal handling - std::signal(SIGINT, signalHandler); - std::signal(SIGTERM, signalHandler); - - // Paths - const std::string modulesDir = "./modules/"; - const std::string configDir = "./config/"; - - // ========================================================================= - // Module Management - // ========================================================================= - std::map modules; - FileWatcher watcher; - - // Liste des modules à charger - std::vector> moduleList = { - {"SchedulerModule", "scheduler.json"}, - {"NotificationModule", "notification.json"}, - // Futurs modules: - // {"AIAssistantModule", "ai_assistant.json"}, - // {"LanguageLearningModule", "language.json"}, - // {"DataModule", "data.json"}, - }; - - // Charger les modules - for (const auto& [moduleName, configFile] : moduleList) { - std::string modulePath = modulesDir + "lib" + moduleName + ".so"; - - if (!fs::exists(modulePath)) { - spdlog::warn("{} non trouvé: {}", moduleName, modulePath); - continue; - } - - ModuleEntry entry; - entry.name = moduleName; - entry.configFile = configFile; - entry.path = modulePath; - entry.loader = std::make_unique(); - entry.io = grove::IOFactory::create("intra", moduleName); - - auto modulePtr = entry.loader->load(modulePath, moduleName); - if (!modulePtr) { - spdlog::error("Échec du chargement: {}", moduleName); - continue; - } - - // Configure - auto config = loadConfig(configDir + configFile); - modulePtr->setConfiguration(*config, entry.io.get(), nullptr); - - entry.module = modulePtr.release(); - watcher.watch(modulePath); - - spdlog::info("{} chargé et configuré", moduleName); - modules[moduleName] = std::move(entry); - } - - if (modules.empty()) { - spdlog::error("Aucun module chargé! Build les modules: cmake --build build --target modules"); - return 1; - } - - // ========================================================================= - // Main Loop - // ========================================================================= - spdlog::info("Démarrage de la boucle principale (Ctrl+C pour quitter)"); - - using Clock = std::chrono::high_resolution_clock; - auto startTime = Clock::now(); - auto lastFrame = Clock::now(); - const auto targetFrameTime = std::chrono::milliseconds(100); // 10 Hz pour un assistant - - grove::JsonDataNode frameInput("frame"); - uint64_t frameCount = 0; - - while (g_running) { - auto frameStart = Clock::now(); - - // Calculate times - auto deltaTime = std::chrono::duration(frameStart - lastFrame).count(); - auto gameTime = std::chrono::duration(frameStart - startTime).count(); - lastFrame = frameStart; - - // Prepare frame input - frameInput.setDouble("deltaTime", deltaTime); - frameInput.setInt("frameCount", frameCount); - frameInput.setDouble("gameTime", gameTime); - - // ===================================================================== - // Hot-Reload Check (every 10 frames = ~1 second) - // ===================================================================== - if (frameCount % 10 == 0) { - for (auto& [name, entry] : modules) { - if (watcher.hasChanged(entry.path)) { - spdlog::info("Modification détectée: {}, hot-reload...", name); - - // Get state before reload - std::unique_ptr state; - if (entry.module) { - state = entry.module->getState(); - } - - // Reload - auto reloaded = entry.loader->load(entry.path, name, true); - if (reloaded) { - auto config = loadConfig(configDir + entry.configFile); - reloaded->setConfiguration(*config, entry.io.get(), nullptr); - - if (state) { - reloaded->setState(*state); - } - - entry.module = reloaded.release(); - spdlog::info("{} rechargé avec succès!", name); - } - } - } - } - - // ===================================================================== - // Process all modules - // ===================================================================== - for (auto& [name, entry] : modules) { - if (entry.module) { - entry.module->process(frameInput); - } - } - - // Process IO messages - for (auto& [name, entry] : modules) { - while (entry.io && entry.io->hasMessages() > 0) { - auto msg = entry.io->pullMessage(); - // Route messages between modules if needed - } - } - - // ===================================================================== - // Frame timing - // ===================================================================== - frameCount++; - - auto frameEnd = Clock::now(); - auto frameDuration = frameEnd - frameStart; - - if (frameDuration < targetFrameTime) { - std::this_thread::sleep_for(targetFrameTime - frameDuration); - } - - // Status log every 30 seconds - if (frameCount % 300 == 0) { - int minutes = static_cast(gameTime / 60.0f); - int seconds = static_cast(gameTime) % 60; - spdlog::debug("Session: {}m{}s, {} modules actifs", - minutes, seconds, modules.size()); - } - } - - // ========================================================================= - // Shutdown - // ========================================================================= - spdlog::info("Arrêt en cours..."); - for (auto& [name, entry] : modules) { - if (entry.module) { - entry.module->shutdown(); - spdlog::info("{} arrêté", name); - } - } - spdlog::info("À bientôt!"); - - return 0; -} +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// Global flag for clean shutdown +static volatile bool g_running = true; + +void signalHandler(int signal) { + spdlog::info("Signal {} reçu, arrêt en cours...", signal); + g_running = false; +} + +// Simple file watcher for hot-reload +class FileWatcher { +public: + void watch(const std::string& path) { + if (fs::exists(path)) { + m_lastModified[path] = fs::last_write_time(path); + } + } + + bool hasChanged(const std::string& path) { + if (!fs::exists(path)) return false; + + auto currentTime = fs::last_write_time(path); + auto it = m_lastModified.find(path); + + if (it == m_lastModified.end()) { + m_lastModified[path] = currentTime; + return false; + } + + if (currentTime != it->second) { + it->second = currentTime; + return true; + } + return false; + } + +private: + std::unordered_map m_lastModified; +}; + +// Load JSON config file +std::unique_ptr loadConfig(const std::string& path) { + if (fs::exists(path)) { + std::ifstream file(path); + nlohmann::json j; + file >> j; + auto config = std::make_unique("config", j); + spdlog::info("Config chargée: {}", path); + return config; + } else { + spdlog::warn("Config non trouvée: {}, utilisation des défauts", path); + return std::make_unique("config"); + } +} + +// Module entry in our simple manager +struct ModuleEntry { + std::string name; + std::string configFile; + std::string path; + std::unique_ptr loader; + std::unique_ptr io; + grove::IModule* module = nullptr; +}; + +int main(int argc, char* argv[]) { + // Setup logging + auto console = spdlog::stdout_color_mt("Aissia"); + spdlog::set_default_logger(console); + spdlog::set_level(spdlog::level::debug); + spdlog::set_pattern("[%H:%M:%S.%e] [%n] [%^%l%$] %v"); + + spdlog::info("========================================"); + spdlog::info(" AISSIA - Assistant Personnel IA"); + spdlog::info(" Powered by GroveEngine"); + spdlog::info("========================================"); + + // Signal handling + std::signal(SIGINT, signalHandler); + std::signal(SIGTERM, signalHandler); + + // Paths + const std::string modulesDir = "./modules/"; + const std::string configDir = "./config/"; + + // ========================================================================= + // Module Management + // ========================================================================= + std::map modules; + FileWatcher watcher; + + // Liste des modules à charger + std::vector> moduleList = { + {"SchedulerModule", "scheduler.json"}, + {"NotificationModule", "notification.json"}, + // Futurs modules: + // {"AIAssistantModule", "ai_assistant.json"}, + // {"LanguageLearningModule", "language.json"}, + // {"DataModule", "data.json"}, + }; + + // Charger les modules + for (const auto& [moduleName, configFile] : moduleList) { + std::string modulePath = modulesDir + "lib" + moduleName + ".so"; + + if (!fs::exists(modulePath)) { + spdlog::warn("{} non trouvé: {}", moduleName, modulePath); + continue; + } + + ModuleEntry entry; + entry.name = moduleName; + entry.configFile = configFile; + entry.path = modulePath; + entry.loader = std::make_unique(); + entry.io = grove::IOFactory::create("intra", moduleName); + + auto modulePtr = entry.loader->load(modulePath, moduleName); + if (!modulePtr) { + spdlog::error("Échec du chargement: {}", moduleName); + continue; + } + + // Configure + auto config = loadConfig(configDir + configFile); + modulePtr->setConfiguration(*config, entry.io.get(), nullptr); + + entry.module = modulePtr.release(); + watcher.watch(modulePath); + + spdlog::info("{} chargé et configuré", moduleName); + modules[moduleName] = std::move(entry); + } + + if (modules.empty()) { + spdlog::error("Aucun module chargé! Build les modules: cmake --build build --target modules"); + return 1; + } + + // ========================================================================= + // Main Loop + // ========================================================================= + spdlog::info("Démarrage de la boucle principale (Ctrl+C pour quitter)"); + + using Clock = std::chrono::high_resolution_clock; + auto startTime = Clock::now(); + auto lastFrame = Clock::now(); + const auto targetFrameTime = std::chrono::milliseconds(100); // 10 Hz pour un assistant + + grove::JsonDataNode frameInput("frame"); + uint64_t frameCount = 0; + + while (g_running) { + auto frameStart = Clock::now(); + + // Calculate times + auto deltaTime = std::chrono::duration(frameStart - lastFrame).count(); + auto gameTime = std::chrono::duration(frameStart - startTime).count(); + lastFrame = frameStart; + + // Prepare frame input + frameInput.setDouble("deltaTime", deltaTime); + frameInput.setInt("frameCount", frameCount); + frameInput.setDouble("gameTime", gameTime); + + // ===================================================================== + // Hot-Reload Check (every 10 frames = ~1 second) + // ===================================================================== + if (frameCount % 10 == 0) { + for (auto& [name, entry] : modules) { + if (watcher.hasChanged(entry.path)) { + spdlog::info("Modification détectée: {}, hot-reload...", name); + + // Get state before reload + std::unique_ptr state; + if (entry.module) { + state = entry.module->getState(); + } + + // Reload + auto reloaded = entry.loader->load(entry.path, name, true); + if (reloaded) { + auto config = loadConfig(configDir + entry.configFile); + reloaded->setConfiguration(*config, entry.io.get(), nullptr); + + if (state) { + reloaded->setState(*state); + } + + entry.module = reloaded.release(); + spdlog::info("{} rechargé avec succès!", name); + } + } + } + } + + // ===================================================================== + // Process all modules + // ===================================================================== + for (auto& [name, entry] : modules) { + if (entry.module) { + entry.module->process(frameInput); + } + } + + // Process IO messages + for (auto& [name, entry] : modules) { + while (entry.io && entry.io->hasMessages() > 0) { + auto msg = entry.io->pullMessage(); + // Route messages between modules if needed + } + } + + // ===================================================================== + // Frame timing + // ===================================================================== + frameCount++; + + auto frameEnd = Clock::now(); + auto frameDuration = frameEnd - frameStart; + + if (frameDuration < targetFrameTime) { + std::this_thread::sleep_for(targetFrameTime - frameDuration); + } + + // Status log every 30 seconds + if (frameCount % 300 == 0) { + int minutes = static_cast(gameTime / 60.0f); + int seconds = static_cast(gameTime) % 60; + spdlog::debug("Session: {}m{}s, {} modules actifs", + minutes, seconds, modules.size()); + } + } + + // ========================================================================= + // Shutdown + // ========================================================================= + spdlog::info("Arrêt en cours..."); + for (auto& [name, entry] : modules) { + if (entry.module) { + entry.module->shutdown(); + spdlog::info("{} arrêté", name); + } + } + spdlog::info("À bientôt!"); + + return 0; +} diff --git a/src/modules/NotificationModule.cpp b/src/modules/NotificationModule.cpp index 2884ed0..e41ee1b 100644 --- a/src/modules/NotificationModule.cpp +++ b/src/modules/NotificationModule.cpp @@ -1,172 +1,172 @@ -#include "NotificationModule.h" -#include - -namespace aissia { - -NotificationModule::NotificationModule() { - m_logger = spdlog::get("NotificationModule"); - if (!m_logger) { - m_logger = spdlog::stdout_color_mt("NotificationModule"); - } - m_config = std::make_unique("config"); -} - -void NotificationModule::setConfiguration(const grove::IDataNode& configNode, - grove::IIO* io, - grove::ITaskScheduler* scheduler) { - m_io = io; - m_config = std::make_unique("config"); - - // Charger la configuration - m_language = configNode.getString("language", "fr"); - m_silentMode = configNode.getBool("silentMode", false); - m_ttsEnabled = configNode.getBool("ttsEnabled", false); - m_maxQueueSize = configNode.getInt("maxQueueSize", 50); - - m_logger->info("NotificationModule configuré: langue={}, silent={}, tts={}", - m_language, m_silentMode, m_ttsEnabled); -} - -const grove::IDataNode& NotificationModule::getConfiguration() { - return *m_config; -} - -void NotificationModule::process(const grove::IDataNode& input) { - // Traiter la file de notifications - processNotificationQueue(); -} - -void NotificationModule::notify(const std::string& title, const std::string& message, Priority priority) { - if (m_silentMode && priority != Priority::URGENT) { - return; // Ignorer les notifications non-urgentes en mode silencieux - } - - Notification notif; - notif.id = std::to_string(++m_notificationCount); - notif.title = title; - notif.message = message; - notif.priority = priority; - notif.language = m_language; - notif.read = false; - notif.timestamp = 0; // Sera mis à jour lors du process - - // Limiter la taille de la queue - while (static_cast(m_pendingNotifications.size()) >= m_maxQueueSize) { - m_pendingNotifications.pop(); - } - - m_pendingNotifications.push(notif); - - if (priority == Priority::URGENT) { - m_urgentCount++; - } -} - -void NotificationModule::processNotificationQueue() { - // Traiter jusqu'à 3 notifications par frame pour éviter le spam - int processed = 0; - while (!m_pendingNotifications.empty() && processed < 3) { - Notification notif = m_pendingNotifications.front(); - m_pendingNotifications.pop(); - - displayNotification(notif); - processed++; - } -} - -void NotificationModule::displayNotification(const Notification& notif) { - std::string emoji = priorityToEmoji(notif.priority); - std::string priorityStr = priorityToString(notif.priority); - - // Affichage console (sera remplacé par Windows toast + TTS) - switch (notif.priority) { - case Priority::URGENT: - m_logger->warn("{} [{}] {}: {}", emoji, priorityStr, notif.title, notif.message); - break; - case Priority::HIGH: - m_logger->info("{} [{}] {}: {}", emoji, priorityStr, notif.title, notif.message); - break; - case Priority::NORMAL: - m_logger->info("{} {}: {}", emoji, notif.title, notif.message); - break; - case Priority::LOW: - m_logger->debug("{} {}: {}", emoji, notif.title, notif.message); - break; - } - - // TODO: Intégrer Windows Toast notifications - // TODO: Intégrer TTS si m_ttsEnabled -} - -std::string NotificationModule::priorityToString(Priority p) { - switch (p) { - case Priority::LOW: return "INFO"; - case Priority::NORMAL: return "RAPPEL"; - case Priority::HIGH: return "IMPORTANT"; - case Priority::URGENT: return "URGENT"; - default: return "?"; - } -} - -std::string NotificationModule::priorityToEmoji(Priority p) { - switch (p) { - case Priority::LOW: return "[i]"; - case Priority::NORMAL: return "[*]"; - case Priority::HIGH: return "[!]"; - case Priority::URGENT: return "[!!!]"; - default: return "[ ]"; - } -} - -std::unique_ptr NotificationModule::getHealthStatus() { - auto status = std::make_unique("status"); - status->setString("status", "running"); - status->setInt("pendingCount", m_pendingNotifications.size()); - status->setInt("totalNotifications", m_notificationCount); - status->setInt("urgentCount", m_urgentCount); - status->setBool("silentMode", m_silentMode); - return status; -} - -void NotificationModule::shutdown() { - // Vider la queue avant arrêt - while (!m_pendingNotifications.empty()) { - displayNotification(m_pendingNotifications.front()); - m_pendingNotifications.pop(); - } - m_logger->info("NotificationModule arrêté. Total: {} notifications ({} urgentes)", - m_notificationCount, m_urgentCount); -} - -std::unique_ptr NotificationModule::getState() { - auto state = std::make_unique("state"); - - state->setInt("notificationCount", m_notificationCount); - state->setInt("urgentCount", m_urgentCount); - state->setInt("pendingCount", m_pendingNotifications.size()); - - m_logger->debug("État sauvegardé: {} notifications", m_notificationCount); - return state; -} - -void NotificationModule::setState(const grove::IDataNode& state) { - m_notificationCount = state.getInt("notificationCount", 0); - m_urgentCount = state.getInt("urgentCount", 0); - // Note: On ne restaure pas la queue - les notifications en attente sont perdues au reload - - m_logger->info("État restauré: {} notifications historiques", m_notificationCount); -} - -} // namespace aissia - -extern "C" { - -grove::IModule* createModule() { - return new aissia::NotificationModule(); -} - -void destroyModule(grove::IModule* module) { - delete module; -} - -} +#include "NotificationModule.h" +#include + +namespace aissia { + +NotificationModule::NotificationModule() { + m_logger = spdlog::get("NotificationModule"); + if (!m_logger) { + m_logger = spdlog::stdout_color_mt("NotificationModule"); + } + m_config = std::make_unique("config"); +} + +void NotificationModule::setConfiguration(const grove::IDataNode& configNode, + grove::IIO* io, + grove::ITaskScheduler* scheduler) { + m_io = io; + m_config = std::make_unique("config"); + + // Charger la configuration + m_language = configNode.getString("language", "fr"); + m_silentMode = configNode.getBool("silentMode", false); + m_ttsEnabled = configNode.getBool("ttsEnabled", false); + m_maxQueueSize = configNode.getInt("maxQueueSize", 50); + + m_logger->info("NotificationModule configuré: langue={}, silent={}, tts={}", + m_language, m_silentMode, m_ttsEnabled); +} + +const grove::IDataNode& NotificationModule::getConfiguration() { + return *m_config; +} + +void NotificationModule::process(const grove::IDataNode& input) { + // Traiter la file de notifications + processNotificationQueue(); +} + +void NotificationModule::notify(const std::string& title, const std::string& message, Priority priority) { + if (m_silentMode && priority != Priority::URGENT) { + return; // Ignorer les notifications non-urgentes en mode silencieux + } + + Notification notif; + notif.id = std::to_string(++m_notificationCount); + notif.title = title; + notif.message = message; + notif.priority = priority; + notif.language = m_language; + notif.read = false; + notif.timestamp = 0; // Sera mis à jour lors du process + + // Limiter la taille de la queue + while (static_cast(m_pendingNotifications.size()) >= m_maxQueueSize) { + m_pendingNotifications.pop(); + } + + m_pendingNotifications.push(notif); + + if (priority == Priority::URGENT) { + m_urgentCount++; + } +} + +void NotificationModule::processNotificationQueue() { + // Traiter jusqu'à 3 notifications par frame pour éviter le spam + int processed = 0; + while (!m_pendingNotifications.empty() && processed < 3) { + Notification notif = m_pendingNotifications.front(); + m_pendingNotifications.pop(); + + displayNotification(notif); + processed++; + } +} + +void NotificationModule::displayNotification(const Notification& notif) { + std::string emoji = priorityToEmoji(notif.priority); + std::string priorityStr = priorityToString(notif.priority); + + // Affichage console (sera remplacé par Windows toast + TTS) + switch (notif.priority) { + case Priority::URGENT: + m_logger->warn("{} [{}] {}: {}", emoji, priorityStr, notif.title, notif.message); + break; + case Priority::HIGH: + m_logger->info("{} [{}] {}: {}", emoji, priorityStr, notif.title, notif.message); + break; + case Priority::NORMAL: + m_logger->info("{} {}: {}", emoji, notif.title, notif.message); + break; + case Priority::LOW: + m_logger->debug("{} {}: {}", emoji, notif.title, notif.message); + break; + } + + // TODO: Intégrer Windows Toast notifications + // TODO: Intégrer TTS si m_ttsEnabled +} + +std::string NotificationModule::priorityToString(Priority p) { + switch (p) { + case Priority::LOW: return "INFO"; + case Priority::NORMAL: return "RAPPEL"; + case Priority::HIGH: return "IMPORTANT"; + case Priority::URGENT: return "URGENT"; + default: return "?"; + } +} + +std::string NotificationModule::priorityToEmoji(Priority p) { + switch (p) { + case Priority::LOW: return "[i]"; + case Priority::NORMAL: return "[*]"; + case Priority::HIGH: return "[!]"; + case Priority::URGENT: return "[!!!]"; + default: return "[ ]"; + } +} + +std::unique_ptr NotificationModule::getHealthStatus() { + auto status = std::make_unique("status"); + status->setString("status", "running"); + status->setInt("pendingCount", m_pendingNotifications.size()); + status->setInt("totalNotifications", m_notificationCount); + status->setInt("urgentCount", m_urgentCount); + status->setBool("silentMode", m_silentMode); + return status; +} + +void NotificationModule::shutdown() { + // Vider la queue avant arrêt + while (!m_pendingNotifications.empty()) { + displayNotification(m_pendingNotifications.front()); + m_pendingNotifications.pop(); + } + m_logger->info("NotificationModule arrêté. Total: {} notifications ({} urgentes)", + m_notificationCount, m_urgentCount); +} + +std::unique_ptr NotificationModule::getState() { + auto state = std::make_unique("state"); + + state->setInt("notificationCount", m_notificationCount); + state->setInt("urgentCount", m_urgentCount); + state->setInt("pendingCount", m_pendingNotifications.size()); + + m_logger->debug("État sauvegardé: {} notifications", m_notificationCount); + return state; +} + +void NotificationModule::setState(const grove::IDataNode& state) { + m_notificationCount = state.getInt("notificationCount", 0); + m_urgentCount = state.getInt("urgentCount", 0); + // Note: On ne restaure pas la queue - les notifications en attente sont perdues au reload + + m_logger->info("État restauré: {} notifications historiques", m_notificationCount); +} + +} // namespace aissia + +extern "C" { + +grove::IModule* createModule() { + return new aissia::NotificationModule(); +} + +void destroyModule(grove::IModule* module) { + delete module; +} + +} diff --git a/src/modules/NotificationModule.h b/src/modules/NotificationModule.h index e3e5654..1c1d705 100644 --- a/src/modules/NotificationModule.h +++ b/src/modules/NotificationModule.h @@ -1,96 +1,96 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include - -namespace aissia { - -/** - * @brief Notification Module - Alertes système et TTS - * - * Fonctionnalités: - * - Notifications système (console pour l'instant, Windows toast plus tard) - * - File d'attente des notifications avec priorités - * - Support multilingue (FR/EN/JP selon config) - * - Mode silencieux configurable - * - * Souscrit à: - * - "scheduler/hyperfocus_alert" : Alerte hyperfocus -> notification urgente - * - "scheduler/break_reminder" : Rappel pause -> notification normale - * - "ai/suggestion" : Suggestion IA -> notification info - * - "language/correction" : Correction langue -> notification légère - */ -class NotificationModule : public grove::IModule { -public: - enum class Priority { - LOW, // Info, peut être ignorée - NORMAL, // Rappels standards - HIGH, // Alertes importantes - URGENT // Hyperfocus, urgences - }; - - struct Notification { - std::string id; - std::string title; - std::string message; - Priority priority; - std::string language; // "fr", "en", "jp" - bool read; - float timestamp; - }; - - NotificationModule(); - ~NotificationModule() override = default; - - // IModule interface - void process(const grove::IDataNode& input) override; - void setConfiguration(const grove::IDataNode& configNode, grove::IIO* io, - grove::ITaskScheduler* scheduler) override; - const grove::IDataNode& getConfiguration() override; - std::unique_ptr getHealthStatus() override; - void shutdown() override; - std::unique_ptr getState() override; - void setState(const grove::IDataNode& state) override; - std::string getType() const override { return "NotificationModule"; } - bool isIdle() const override { return true; } - int getVersion() const override { return 1; } - - // API publique - void notify(const std::string& title, const std::string& message, Priority priority = Priority::NORMAL); - -private: - // Configuration - std::string m_language = "fr"; - bool m_silentMode = false; - bool m_ttsEnabled = false; // Text-to-Speech (future) - int m_maxQueueSize = 50; - - // État - std::queue m_pendingNotifications; - int m_notificationCount = 0; - int m_urgentCount = 0; - - // Services - grove::IIO* m_io = nullptr; - std::unique_ptr m_config; - std::shared_ptr m_logger; - - // Helpers - void processNotificationQueue(); - void displayNotification(const Notification& notif); - std::string priorityToString(Priority p); - std::string priorityToEmoji(Priority p); -}; - -} // namespace aissia - -extern "C" { - grove::IModule* createModule(); - void destroyModule(grove::IModule* module); -} +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace aissia { + +/** + * @brief Notification Module - Alertes système et TTS + * + * Fonctionnalités: + * - Notifications système (console pour l'instant, Windows toast plus tard) + * - File d'attente des notifications avec priorités + * - Support multilingue (FR/EN/JP selon config) + * - Mode silencieux configurable + * + * Souscrit à: + * - "scheduler/hyperfocus_alert" : Alerte hyperfocus -> notification urgente + * - "scheduler/break_reminder" : Rappel pause -> notification normale + * - "ai/suggestion" : Suggestion IA -> notification info + * - "language/correction" : Correction langue -> notification légère + */ +class NotificationModule : public grove::IModule { +public: + enum class Priority { + LOW, // Info, peut être ignorée + NORMAL, // Rappels standards + HIGH, // Alertes importantes + URGENT // Hyperfocus, urgences + }; + + struct Notification { + std::string id; + std::string title; + std::string message; + Priority priority; + std::string language; // "fr", "en", "jp" + bool read; + float timestamp; + }; + + NotificationModule(); + ~NotificationModule() override = default; + + // IModule interface + void process(const grove::IDataNode& input) override; + void setConfiguration(const grove::IDataNode& configNode, grove::IIO* io, + grove::ITaskScheduler* scheduler) override; + const grove::IDataNode& getConfiguration() override; + std::unique_ptr getHealthStatus() override; + void shutdown() override; + std::unique_ptr getState() override; + void setState(const grove::IDataNode& state) override; + std::string getType() const override { return "NotificationModule"; } + bool isIdle() const override { return true; } + int getVersion() const override { return 1; } + + // API publique + void notify(const std::string& title, const std::string& message, Priority priority = Priority::NORMAL); + +private: + // Configuration + std::string m_language = "fr"; + bool m_silentMode = false; + bool m_ttsEnabled = false; // Text-to-Speech (future) + int m_maxQueueSize = 50; + + // État + std::queue m_pendingNotifications; + int m_notificationCount = 0; + int m_urgentCount = 0; + + // Services + grove::IIO* m_io = nullptr; + std::unique_ptr m_config; + std::shared_ptr m_logger; + + // Helpers + void processNotificationQueue(); + void displayNotification(const Notification& notif); + std::string priorityToString(Priority p); + std::string priorityToEmoji(Priority p); +}; + +} // namespace aissia + +extern "C" { + grove::IModule* createModule(); + void destroyModule(grove::IModule* module); +} diff --git a/src/modules/SchedulerModule.cpp b/src/modules/SchedulerModule.cpp index e2e2aea..caee06e 100644 --- a/src/modules/SchedulerModule.cpp +++ b/src/modules/SchedulerModule.cpp @@ -1,179 +1,179 @@ -#include "SchedulerModule.h" -#include - -namespace aissia { - -SchedulerModule::SchedulerModule() { - m_logger = spdlog::get("SchedulerModule"); - if (!m_logger) { - m_logger = spdlog::stdout_color_mt("SchedulerModule"); - } - m_config = std::make_unique("config"); -} - -void SchedulerModule::setConfiguration(const grove::IDataNode& configNode, - grove::IIO* io, - grove::ITaskScheduler* scheduler) { - m_io = io; - m_config = std::make_unique("config"); - - // Charger la configuration - m_hyperfocusThresholdMinutes = configNode.getInt("hyperfocusThresholdMinutes", 120); - m_breakReminderIntervalMinutes = configNode.getInt("breakReminderIntervalMinutes", 45); - m_breakDurationMinutes = configNode.getInt("breakDurationMinutes", 10); - - m_logger->info("SchedulerModule configuré: hyperfocus={}min, break_interval={}min", - m_hyperfocusThresholdMinutes, m_breakReminderIntervalMinutes); -} - -const grove::IDataNode& SchedulerModule::getConfiguration() { - return *m_config; -} - -void SchedulerModule::process(const grove::IDataNode& input) { - float currentTime = input.getDouble("gameTime", 0.0); - float dt = input.getDouble("deltaTime", 0.016); - - // Convertir le temps en minutes pour les calculs - float sessionMinutes = (currentTime - m_sessionStartTime) / 60.0f; - - // Vérifier l'hyperfocus - checkHyperfocus(currentTime); - - // Vérifier les rappels de pause - checkBreakReminder(currentTime); - - // Log périodique (toutes les 5 minutes simulées) - static float lastLog = 0; - if (currentTime - lastLog > 300.0f) { // 300 secondes = 5 minutes - lastLog = currentTime; - m_logger->debug("Session: {:.1f}min, Focus aujourd'hui: {}min, Tâche: {}", - sessionMinutes, m_totalFocusMinutesToday, - m_currentTaskId.empty() ? "(aucune)" : m_currentTaskId); - } -} - -void SchedulerModule::checkHyperfocus(float currentTime) { - if (m_currentTaskId.empty()) return; - - float sessionMinutes = (currentTime - m_sessionStartTime) / 60.0f; - - if (sessionMinutes >= m_hyperfocusThresholdMinutes && !m_hyperfocusAlertSent) { - m_hyperfocusAlertSent = true; - m_logger->warn("HYPERFOCUS DÉTECTÉ! Session de {:.0f} minutes sur '{}'", - sessionMinutes, m_currentTaskId); - - // Publier l'alerte (si IO disponible) - // Note: Dans une version complète, on publierait via m_io - } -} - -void SchedulerModule::checkBreakReminder(float currentTime) { - float timeSinceBreak = (currentTime - m_lastBreakTime) / 60.0f; - - if (timeSinceBreak >= m_breakReminderIntervalMinutes) { - m_lastBreakTime = currentTime; - m_logger->info("RAPPEL: Pause de {} minutes recommandée!", m_breakDurationMinutes); - - // Publier le rappel (si IO disponible) - } -} - -void SchedulerModule::startTask(const std::string& taskId) { - // Compléter la tâche précédente si nécessaire - if (!m_currentTaskId.empty()) { - completeCurrentTask(); - } - - m_currentTaskId = taskId; - m_sessionStartTime = m_lastActivityTime; - m_hyperfocusAlertSent = false; - - Task* task = findTask(taskId); - if (task) { - m_logger->info("Tâche démarrée: {} (estimé: {}min)", task->name, task->estimatedMinutes); - } -} - -void SchedulerModule::completeCurrentTask() { - if (m_currentTaskId.empty()) return; - - Task* task = findTask(m_currentTaskId); - if (task) { - float sessionMinutes = (m_lastActivityTime - m_sessionStartTime) / 60.0f; - task->actualMinutes = static_cast(sessionMinutes); - task->completed = true; - m_totalFocusMinutesToday += task->actualMinutes; - - m_logger->info("Tâche terminée: {} (réel: {}min vs estimé: {}min)", - task->name, task->actualMinutes, task->estimatedMinutes); - } - - m_currentTaskId.clear(); -} - -SchedulerModule::Task* SchedulerModule::findTask(const std::string& taskId) { - for (auto& task : m_tasks) { - if (task.id == taskId) return &task; - } - return nullptr; -} - -std::unique_ptr SchedulerModule::getHealthStatus() { - auto status = std::make_unique("status"); - status->setString("status", "running"); - status->setInt("taskCount", m_tasks.size()); - status->setString("currentTask", m_currentTaskId); - status->setInt("totalFocusMinutesToday", m_totalFocusMinutesToday); - status->setBool("hyperfocusAlertSent", m_hyperfocusAlertSent); - return status; -} - -void SchedulerModule::shutdown() { - if (!m_currentTaskId.empty()) { - completeCurrentTask(); - } - m_logger->info("SchedulerModule arrêté. Focus total: {}min", m_totalFocusMinutesToday); -} - -std::unique_ptr SchedulerModule::getState() { - auto state = std::make_unique("state"); - - state->setString("currentTaskId", m_currentTaskId); - state->setDouble("sessionStartTime", m_sessionStartTime); - state->setDouble("lastBreakTime", m_lastBreakTime); - state->setDouble("lastActivityTime", m_lastActivityTime); - state->setBool("hyperfocusAlertSent", m_hyperfocusAlertSent); - state->setInt("totalFocusMinutesToday", m_totalFocusMinutesToday); - state->setInt("taskCount", m_tasks.size()); - - m_logger->debug("État sauvegardé: {} tâches, focus={}min", m_tasks.size(), m_totalFocusMinutesToday); - return state; -} - -void SchedulerModule::setState(const grove::IDataNode& state) { - m_currentTaskId = state.getString("currentTaskId", ""); - m_sessionStartTime = state.getDouble("sessionStartTime", 0.0); - m_lastBreakTime = state.getDouble("lastBreakTime", 0.0); - m_lastActivityTime = state.getDouble("lastActivityTime", 0.0); - m_hyperfocusAlertSent = state.getBool("hyperfocusAlertSent", false); - m_totalFocusMinutesToday = state.getInt("totalFocusMinutesToday", 0); - - m_logger->info("État restauré: tâche='{}', focus={}min", - m_currentTaskId.empty() ? "(aucune)" : m_currentTaskId, - m_totalFocusMinutesToday); -} - -} // namespace aissia - -extern "C" { - -grove::IModule* createModule() { - return new aissia::SchedulerModule(); -} - -void destroyModule(grove::IModule* module) { - delete module; -} - -} +#include "SchedulerModule.h" +#include + +namespace aissia { + +SchedulerModule::SchedulerModule() { + m_logger = spdlog::get("SchedulerModule"); + if (!m_logger) { + m_logger = spdlog::stdout_color_mt("SchedulerModule"); + } + m_config = std::make_unique("config"); +} + +void SchedulerModule::setConfiguration(const grove::IDataNode& configNode, + grove::IIO* io, + grove::ITaskScheduler* scheduler) { + m_io = io; + m_config = std::make_unique("config"); + + // Charger la configuration + m_hyperfocusThresholdMinutes = configNode.getInt("hyperfocusThresholdMinutes", 120); + m_breakReminderIntervalMinutes = configNode.getInt("breakReminderIntervalMinutes", 45); + m_breakDurationMinutes = configNode.getInt("breakDurationMinutes", 10); + + m_logger->info("SchedulerModule configuré: hyperfocus={}min, break_interval={}min", + m_hyperfocusThresholdMinutes, m_breakReminderIntervalMinutes); +} + +const grove::IDataNode& SchedulerModule::getConfiguration() { + return *m_config; +} + +void SchedulerModule::process(const grove::IDataNode& input) { + float currentTime = input.getDouble("gameTime", 0.0); + float dt = input.getDouble("deltaTime", 0.016); + + // Convertir le temps en minutes pour les calculs + float sessionMinutes = (currentTime - m_sessionStartTime) / 60.0f; + + // Vérifier l'hyperfocus + checkHyperfocus(currentTime); + + // Vérifier les rappels de pause + checkBreakReminder(currentTime); + + // Log périodique (toutes les 5 minutes simulées) + static float lastLog = 0; + if (currentTime - lastLog > 300.0f) { // 300 secondes = 5 minutes + lastLog = currentTime; + m_logger->debug("Session: {:.1f}min, Focus aujourd'hui: {}min, Tâche: {}", + sessionMinutes, m_totalFocusMinutesToday, + m_currentTaskId.empty() ? "(aucune)" : m_currentTaskId); + } +} + +void SchedulerModule::checkHyperfocus(float currentTime) { + if (m_currentTaskId.empty()) return; + + float sessionMinutes = (currentTime - m_sessionStartTime) / 60.0f; + + if (sessionMinutes >= m_hyperfocusThresholdMinutes && !m_hyperfocusAlertSent) { + m_hyperfocusAlertSent = true; + m_logger->warn("HYPERFOCUS DÉTECTÉ! Session de {:.0f} minutes sur '{}'", + sessionMinutes, m_currentTaskId); + + // Publier l'alerte (si IO disponible) + // Note: Dans une version complète, on publierait via m_io + } +} + +void SchedulerModule::checkBreakReminder(float currentTime) { + float timeSinceBreak = (currentTime - m_lastBreakTime) / 60.0f; + + if (timeSinceBreak >= m_breakReminderIntervalMinutes) { + m_lastBreakTime = currentTime; + m_logger->info("RAPPEL: Pause de {} minutes recommandée!", m_breakDurationMinutes); + + // Publier le rappel (si IO disponible) + } +} + +void SchedulerModule::startTask(const std::string& taskId) { + // Compléter la tâche précédente si nécessaire + if (!m_currentTaskId.empty()) { + completeCurrentTask(); + } + + m_currentTaskId = taskId; + m_sessionStartTime = m_lastActivityTime; + m_hyperfocusAlertSent = false; + + Task* task = findTask(taskId); + if (task) { + m_logger->info("Tâche démarrée: {} (estimé: {}min)", task->name, task->estimatedMinutes); + } +} + +void SchedulerModule::completeCurrentTask() { + if (m_currentTaskId.empty()) return; + + Task* task = findTask(m_currentTaskId); + if (task) { + float sessionMinutes = (m_lastActivityTime - m_sessionStartTime) / 60.0f; + task->actualMinutes = static_cast(sessionMinutes); + task->completed = true; + m_totalFocusMinutesToday += task->actualMinutes; + + m_logger->info("Tâche terminée: {} (réel: {}min vs estimé: {}min)", + task->name, task->actualMinutes, task->estimatedMinutes); + } + + m_currentTaskId.clear(); +} + +SchedulerModule::Task* SchedulerModule::findTask(const std::string& taskId) { + for (auto& task : m_tasks) { + if (task.id == taskId) return &task; + } + return nullptr; +} + +std::unique_ptr SchedulerModule::getHealthStatus() { + auto status = std::make_unique("status"); + status->setString("status", "running"); + status->setInt("taskCount", m_tasks.size()); + status->setString("currentTask", m_currentTaskId); + status->setInt("totalFocusMinutesToday", m_totalFocusMinutesToday); + status->setBool("hyperfocusAlertSent", m_hyperfocusAlertSent); + return status; +} + +void SchedulerModule::shutdown() { + if (!m_currentTaskId.empty()) { + completeCurrentTask(); + } + m_logger->info("SchedulerModule arrêté. Focus total: {}min", m_totalFocusMinutesToday); +} + +std::unique_ptr SchedulerModule::getState() { + auto state = std::make_unique("state"); + + state->setString("currentTaskId", m_currentTaskId); + state->setDouble("sessionStartTime", m_sessionStartTime); + state->setDouble("lastBreakTime", m_lastBreakTime); + state->setDouble("lastActivityTime", m_lastActivityTime); + state->setBool("hyperfocusAlertSent", m_hyperfocusAlertSent); + state->setInt("totalFocusMinutesToday", m_totalFocusMinutesToday); + state->setInt("taskCount", m_tasks.size()); + + m_logger->debug("État sauvegardé: {} tâches, focus={}min", m_tasks.size(), m_totalFocusMinutesToday); + return state; +} + +void SchedulerModule::setState(const grove::IDataNode& state) { + m_currentTaskId = state.getString("currentTaskId", ""); + m_sessionStartTime = state.getDouble("sessionStartTime", 0.0); + m_lastBreakTime = state.getDouble("lastBreakTime", 0.0); + m_lastActivityTime = state.getDouble("lastActivityTime", 0.0); + m_hyperfocusAlertSent = state.getBool("hyperfocusAlertSent", false); + m_totalFocusMinutesToday = state.getInt("totalFocusMinutesToday", 0); + + m_logger->info("État restauré: tâche='{}', focus={}min", + m_currentTaskId.empty() ? "(aucune)" : m_currentTaskId, + m_totalFocusMinutesToday); +} + +} // namespace aissia + +extern "C" { + +grove::IModule* createModule() { + return new aissia::SchedulerModule(); +} + +void destroyModule(grove::IModule* module) { + delete module; +} + +} diff --git a/src/modules/SchedulerModule.h b/src/modules/SchedulerModule.h index 40e03ca..f25790f 100644 --- a/src/modules/SchedulerModule.h +++ b/src/modules/SchedulerModule.h @@ -1,94 +1,94 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace aissia { - -/** - * @brief Scheduler Module - Gestion du temps et détection d'hyperfocus - * - * Fonctionnalités: - * - Planning de tâches avec durées estimées - * - Détection d'hyperfocus (session > seuil configurable) - * - Rappels de pauses automatiques - * - Tracking du temps par tâche - * - * Publie sur: - * - "scheduler/hyperfocus_alert" : Alerte quand hyperfocus détecté - * - "scheduler/break_reminder" : Rappel de pause - * - "scheduler/task_started" : Début de tâche - * - "scheduler/task_completed" : Fin de tâche - * - * Souscrit à: - * - "user/activity" : Activité utilisateur (reset idle) - * - "user/task_switch" : Changement de tâche manuel - */ -class SchedulerModule : public grove::IModule { -public: - struct Task { - std::string id; - std::string name; - std::string category; - int estimatedMinutes; - int actualMinutes; - bool completed; - }; - - SchedulerModule(); - ~SchedulerModule() override = default; - - // IModule interface - void process(const grove::IDataNode& input) override; - void setConfiguration(const grove::IDataNode& configNode, grove::IIO* io, - grove::ITaskScheduler* scheduler) override; - const grove::IDataNode& getConfiguration() override; - std::unique_ptr getHealthStatus() override; - void shutdown() override; - std::unique_ptr getState() override; - void setState(const grove::IDataNode& state) override; - std::string getType() const override { return "SchedulerModule"; } - bool isIdle() const override { return true; } - int getVersion() const override { return 1; } - -private: - // Configuration - int m_hyperfocusThresholdMinutes = 120; // 2 heures par défaut - int m_breakReminderIntervalMinutes = 45; // Rappel toutes les 45 min - int m_breakDurationMinutes = 10; // Pause de 10 min suggérée - - // État - std::vector m_tasks; - std::string m_currentTaskId; - float m_sessionStartTime = 0.0f; // Temps de début de session (gameTime) - float m_lastBreakTime = 0.0f; // Dernier rappel de pause - float m_lastActivityTime = 0.0f; // Dernière activité utilisateur - bool m_hyperfocusAlertSent = false; - int m_totalFocusMinutesToday = 0; - - // Services - grove::IIO* m_io = nullptr; - std::unique_ptr m_config; - std::shared_ptr m_logger; - - // Helpers - void checkHyperfocus(float currentTime); - void checkBreakReminder(float currentTime); - void startTask(const std::string& taskId); - void completeCurrentTask(); - Task* findTask(const std::string& taskId); -}; - -} // namespace aissia - -extern "C" { - grove::IModule* createModule(); - void destroyModule(grove::IModule* module); -} +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace aissia { + +/** + * @brief Scheduler Module - Gestion du temps et détection d'hyperfocus + * + * Fonctionnalités: + * - Planning de tâches avec durées estimées + * - Détection d'hyperfocus (session > seuil configurable) + * - Rappels de pauses automatiques + * - Tracking du temps par tâche + * + * Publie sur: + * - "scheduler/hyperfocus_alert" : Alerte quand hyperfocus détecté + * - "scheduler/break_reminder" : Rappel de pause + * - "scheduler/task_started" : Début de tâche + * - "scheduler/task_completed" : Fin de tâche + * + * Souscrit à: + * - "user/activity" : Activité utilisateur (reset idle) + * - "user/task_switch" : Changement de tâche manuel + */ +class SchedulerModule : public grove::IModule { +public: + struct Task { + std::string id; + std::string name; + std::string category; + int estimatedMinutes; + int actualMinutes; + bool completed; + }; + + SchedulerModule(); + ~SchedulerModule() override = default; + + // IModule interface + void process(const grove::IDataNode& input) override; + void setConfiguration(const grove::IDataNode& configNode, grove::IIO* io, + grove::ITaskScheduler* scheduler) override; + const grove::IDataNode& getConfiguration() override; + std::unique_ptr getHealthStatus() override; + void shutdown() override; + std::unique_ptr getState() override; + void setState(const grove::IDataNode& state) override; + std::string getType() const override { return "SchedulerModule"; } + bool isIdle() const override { return true; } + int getVersion() const override { return 1; } + +private: + // Configuration + int m_hyperfocusThresholdMinutes = 120; // 2 heures par défaut + int m_breakReminderIntervalMinutes = 45; // Rappel toutes les 45 min + int m_breakDurationMinutes = 10; // Pause de 10 min suggérée + + // État + std::vector m_tasks; + std::string m_currentTaskId; + float m_sessionStartTime = 0.0f; // Temps de début de session (gameTime) + float m_lastBreakTime = 0.0f; // Dernier rappel de pause + float m_lastActivityTime = 0.0f; // Dernière activité utilisateur + bool m_hyperfocusAlertSent = false; + int m_totalFocusMinutesToday = 0; + + // Services + grove::IIO* m_io = nullptr; + std::unique_ptr m_config; + std::shared_ptr m_logger; + + // Helpers + void checkHyperfocus(float currentTime); + void checkBreakReminder(float currentTime); + void startTask(const std::string& taskId); + void completeCurrentTask(); + Task* findTask(const std::string& taskId); +}; + +} // namespace aissia + +extern "C" { + grove::IModule* createModule(); + void destroyModule(grove::IModule* module); +}