generated from wessel/boilerplate
[PR] Implement ConfigManager, DatabaseManager and ExamResultGenerator node #1
@@ -1,70 +1,69 @@
|
||||
# Self-contained environment: ros2-assignments
|
||||
# Exported on: Tue Sep 23 09:41:57 AM CEST 2025
|
||||
# Exported on: Tue Sep 23 10:20:42 AM CEST 2025
|
||||
# Original environment from: /home/wessel/.config/fish/environments/configs/ros2-assignments
|
||||
|
||||
# ROS2 development environment (requires distrobox)
|
||||
# Environment: ros2-assignments
|
||||
|
||||
# First check if running inside distrobox
|
||||
if not test -f /run/.containerenv; and test -z "$CONTAINER_ID"
|
||||
echo (set_color red)"This ROS2 environment should only be run inside a distrobox container"(set_color normal)
|
||||
return 1
|
||||
end
|
||||
|
||||
# Check if a previous initialization has occurred
|
||||
if test -n "$__ENV_INITIALIZED"
|
||||
echo (set_color yellow)"Environment already initialized"(set_color normal)
|
||||
return 0
|
||||
end
|
||||
|
||||
# Mark as initialized
|
||||
# Mark as initialized (only after distrobox check passes)
|
||||
set -gx __ENV_INITIALIZED "1"
|
||||
set -gx CURRENT_ENV "ros2-assignments"
|
||||
|
||||
# Check if running inside distrobox
|
||||
if test -f /run/.containerenv; or test -n "$CONTAINER_ID"
|
||||
# Source ROS2 setup files using bass
|
||||
if type -q bass
|
||||
bass source /opt/ros/jazzy/setup.bash
|
||||
if test -f ./install/setup.bash
|
||||
bass source ./install/setup.bash
|
||||
end
|
||||
else
|
||||
echo (set_color red)"Error: bass is required for ROS2 environment. Install with: fisher install edc/bass"(set_color normal)
|
||||
return 1
|
||||
# Source ROS2 setup files using bass
|
||||
if type -q bass
|
||||
bass source /opt/ros/jazzy/setup.bash
|
||||
if test -f ./install/setup.bash
|
||||
bass source ./install/setup.bash
|
||||
end
|
||||
|
||||
# Set environment variable for the prompt prefix
|
||||
set -gx ROS2_ACTIVE 1
|
||||
|
||||
# Save the original prompt function if it exists
|
||||
# Only save if we don't already have a backup or if current prompt is not from an environment
|
||||
if not functions -q __env_orig_prompt
|
||||
if functions -q fish_prompt
|
||||
functions -c fish_prompt __env_orig_prompt
|
||||
else
|
||||
function __env_orig_prompt
|
||||
echo -n (whoami)'@'(prompt_hostname)' '(set_color $fish_color_cwd)(prompt_pwd)(set_color normal)'> '
|
||||
end
|
||||
end
|
||||
else
|
||||
# If we already have a backup, we're switching environments
|
||||
# No need to create a new backup
|
||||
end
|
||||
|
||||
# Define new prompt with ROS2 prefix
|
||||
function fish_prompt
|
||||
echo -n (set_color magenta)'(ros2-assignments)'(set_color normal)' '
|
||||
__env_orig_prompt
|
||||
end
|
||||
|
||||
# ROS2 aliases and functions
|
||||
alias cb="colcon build"
|
||||
alias cbs="colcon build --symlink-install"
|
||||
alias cbt="colcon build --packages-select"
|
||||
alias ct="colcon test"
|
||||
alias ctr="colcon test-result"
|
||||
|
||||
echo (set_color green)"Activated ROS2 environment: ros2-assignments"(set_color normal)
|
||||
else
|
||||
echo (set_color red)"This ROS2 environment should only be run inside a distrobox container"(set_color normal)
|
||||
echo (set_color red)"Error: bass is required for ROS2 environment. Install with: fisher install edc/bass"(set_color normal)
|
||||
return 1
|
||||
end
|
||||
|
||||
# Set environment variable for the prompt prefix
|
||||
set -gx ROS2_ACTIVE 1
|
||||
|
||||
# Save the original prompt function if it exists
|
||||
# Only save if we don't already have a backup or if current prompt is not from an environment
|
||||
if not functions -q __env_orig_prompt
|
||||
if functions -q fish_prompt
|
||||
functions -c fish_prompt __env_orig_prompt
|
||||
else
|
||||
function __env_orig_prompt
|
||||
echo -n (whoami)'@'(prompt_hostname)' '(set_color $fish_color_cwd)(prompt_pwd)(set_color normal)'> '
|
||||
end
|
||||
end
|
||||
else
|
||||
# If we already have a backup, we're switching environments
|
||||
# No need to create a new backup
|
||||
end
|
||||
|
||||
# Define new prompt with ROS2 prefix
|
||||
function fish_prompt
|
||||
echo -n (set_color magenta)'(ros2-assignments)'(set_color normal)' '
|
||||
__env_orig_prompt
|
||||
end
|
||||
|
||||
# ROS2 aliases and functions
|
||||
alias cb="colcon build"
|
||||
alias cbs="colcon build --symlink-install"
|
||||
alias cbt="colcon build --packages-select"
|
||||
alias ct="colcon test"
|
||||
alias ctr="colcon test-result"
|
||||
|
||||
echo (set_color green)"Activated ROS2 environment: ros2-assignments"(set_color normal)
|
||||
|
||||
# Custom deactivation function
|
||||
function __env_custom_deactivate
|
||||
# Remove ROS2-specific variables and aliases
|
||||
|
||||
134
doc/ConfigManager.md
Normal file
134
doc/ConfigManager.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# ConfigManager (`assignments::one::ConfigManager`)
|
||||
|
||||
## Overview
|
||||
|
||||
The `ConfigManager` class is used to be able to store configuration values in a `TOML` file making it
|
||||
possible to change project settings without the need of recompiling the codebase.
|
||||
|
||||
The [`toml++`](https://marzer.github.io/tomlplusplus/) library is used to parse TOML configuration
|
||||
files and provides type-safe access to configuration parameters. It is automatically installed
|
||||
using the `FetchContent_Declare` macro inside of the `CMakeLists.txt`.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Dependencies
|
||||
- **toml++**: Modern header-only C++17 TOML parser and serializer
|
||||
- **rclcpp**: ROS2 C++ client library for logging
|
||||
- **filesystem**: C++17 filesystem library for file operations
|
||||
- **DatabaseConfig**: Custom data structure for database configuration
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Constructor
|
||||
```cpp
|
||||
ConfigManager(rclcpp::Logger logger)
|
||||
```
|
||||
- Initializes the configuration manager making use of a ROS2 logger
|
||||
- Calls `find_config_file()` and `load_config()` for automatic setup
|
||||
- Automatically attempts to locate and load configuration from a list of pre-defined paths.
|
||||
|
||||
#### Configuration Loading
|
||||
|
||||
**`bool load_config(const std::string& config_file_path)`**
|
||||
> Returns `true` on successful load, `false` on failure
|
||||
|
||||
- Uses `toml::parse_file()`
|
||||
- Loads TOML configuration from specified file path
|
||||
- Validates file existence before attempting to parse
|
||||
- Error handling for:
|
||||
- File not found errors
|
||||
- TOML parsing errors
|
||||
- General I/O exceptions
|
||||
- Updates internal `loaded_` state flag
|
||||
|
||||
**`std::string find_config_file() const`**
|
||||
> Returns first found configuration file path
|
||||
> Returns empty string if no configuration file is found
|
||||
|
||||
- Automatically searches for configuration files in predefined locations
|
||||
- Search paths (in order):
|
||||
```cpp
|
||||
std::vector<std::string> default_config_paths_ = {
|
||||
"config.toml",
|
||||
"./src/config.toml",
|
||||
"../config.toml",
|
||||
"../../config.toml",
|
||||
"../../../config.toml",
|
||||
"../../../../config.toml",
|
||||
"/etc/ros2_grade_calculator/config.toml"
|
||||
};
|
||||
```
|
||||
#### Configuration Access
|
||||
|
||||
**`std::optional<DatabaseConfig> get_database_config() const`**
|
||||
> Returns `std::optional<DatabaseConfig>` for safe null handling
|
||||
> Returns `std::nullopt` if:
|
||||
> - Configuration is not loaded
|
||||
> - Database section is missing
|
||||
> - Parsing fails due to invalid format
|
||||
|
||||
- Calls `parse_database_config()` for actual parsing
|
||||
|
||||
**`bool is_loaded() const`**
|
||||
> Returns `true` if configuration has been successfully loaded and parsed
|
||||
|
||||
- Getter for configuration load status
|
||||
- Used by other components to verify configuration availability
|
||||
|
||||
### Configuration Parsing
|
||||
|
||||
**`DatabaseConfig parse_database_config(const toml::table& config) const`**
|
||||
- parser for database configuration section
|
||||
- Extracts all database-related parameters with defaults
|
||||
|
||||
#### Default Values and Fallbacks
|
||||
- **host**: `"localhost"` - Local database server
|
||||
- **port**: `5432` - Standard PostgreSQL port
|
||||
- **dbname**: `"grades"` - Application-specific database
|
||||
- **user**: `"postgres"` - Default PostgreSQL user
|
||||
- **password**: `"postgres"` - Default PostgreSQL password
|
||||
- **timeout**: `30` seconds - connection timeout
|
||||
- **ssl**: `false` - Disabled by default for development
|
||||
- **min_connections**: `1` - Minimal connection pool
|
||||
- **max_connections**: `10` - connection pool limit
|
||||
|
||||
### Logging
|
||||
- Uses ROS2 logger with `[CFG]` prefix for configuration operations
|
||||
- Includes file paths in log messages
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Automatic Configuration Loading
|
||||
```cpp
|
||||
// ConfigManager automatically finds and loads configuration
|
||||
ConfigManager config_manager(node->get_logger());
|
||||
|
||||
if (config_manager.is_loaded()) {
|
||||
auto db_config = config_manager.get_database_config();
|
||||
if (db_config.has_value()) {
|
||||
// Use database configuration
|
||||
DatabaseManager db_manager(db_config.value());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration File Format
|
||||
|
||||
Example complete TOML configuration:
|
||||
|
||||
```toml
|
||||
# Database connection settings
|
||||
[database]
|
||||
host = "localhost"
|
||||
port = 5432
|
||||
dbname = "grades"
|
||||
user = "postgres"
|
||||
password = "postgres"
|
||||
timeout = 30
|
||||
ssl = false
|
||||
|
||||
[database.pool]
|
||||
min_connections = 1
|
||||
max_connections = 10
|
||||
|
||||
```
|
||||
125
doc/DatabaseManager.md
Normal file
125
doc/DatabaseManager.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# DatabaseManager (`assignments::one::DatabaseManager`)
|
||||
|
||||
## Overview
|
||||
|
||||
The `DatabaseManager` class is a PostgreSQL database interface for the ROS2 grade calculator.
|
||||
It handles all database operations including connection management, table creation and data insertion.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Dependencies
|
||||
- **pqxx**: Modern C++ PostgreSQL library for database connectivity
|
||||
- **rclcpp**: ROS2 C++ client library for logging
|
||||
- **ConfigManager**: Configuration manager for database settings
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Constructor
|
||||
```cpp
|
||||
DatabaseManager(rclcpp::Logger logger)
|
||||
```
|
||||
- Initializes the database manager with ROS2 logging
|
||||
- Creates a ConfigManager instance for configuration handling
|
||||
- Automatically calls `init_database()` to establish connection and setup
|
||||
|
||||
#### Connection Management
|
||||
|
||||
**`bool connect(const std::string& connection_string)`**
|
||||
> Returns `true` on successful connection, `false` on failure
|
||||
|
||||
- Establishes connection to PostgreSQL database using connection information from the config TOML
|
||||
- Connection string format: `"host=localhost port=5432 dbname=grades user=postgres password=postgres"`
|
||||
|
||||
**`bool is_connected() const`**
|
||||
> Returns `true` if connection exists and is open
|
||||
|
||||
- Check for active database connection status
|
||||
|
||||
#### Database Initialization
|
||||
|
||||
**`void init_database()`**
|
||||
- Database setup process
|
||||
- Loads configuration from ConfigManager
|
||||
- Establishes connection using configuration settings
|
||||
- Creates necessary tables and inserts sample data
|
||||
- Error handling for configuration and connection failures
|
||||
|
||||
**`void create_tables()`**
|
||||
- Creates all required database tables using SQL queries from `SQLQueries.hpp`
|
||||
- Tables created:
|
||||
- `enrollments`: Student course enrollments
|
||||
- `exam_results`: Individual exam scores
|
||||
- `course_results`: Final course grades and statistics
|
||||
- Uses transactions for atomic table creation
|
||||
|
||||
**`void insert_sample_data()`**
|
||||
- Inserts predefined sample student data
|
||||
|
||||
### Data Operations
|
||||
|
||||
#### Student Course Management
|
||||
|
||||
**`std::vector<StudentCourse> queue_pending_combinations()`**
|
||||
> Returns vector of StudentCourse objects for processing queue
|
||||
|
||||
- Gets all student-course combinations that need exam results generated
|
||||
- Executes complex SQL query to find missing exam results
|
||||
|
||||
**`bool enroll_student_into_course(const StudentCourse& sc)`**
|
||||
> Returns `true` on successful enrollment, `false` on failure
|
||||
|
||||
- Enrolls a student into a specific course
|
||||
|
||||
#### Exam Result Processing
|
||||
|
||||
**`bool store_exam_result(const std::string& student_name, const std::string& course_name, int grade)`**
|
||||
- Stores individual exam results in the database
|
||||
- Parameters:
|
||||
- `student_name`: Name of the student
|
||||
- `course_name`: Name of the course
|
||||
- `grade`: Exam score (10-100)
|
||||
|
||||
**`bool store_final_course_result(const StudentCourse& sc, int exam_count, int final_grade)`**
|
||||
- Stores calculated final course results
|
||||
- Parameters:
|
||||
- `sc`: StudentCourse object containing student and course names
|
||||
- `exam_count`: Number of exams taken
|
||||
- `final_grade`: Calculated final grade
|
||||
- Used by grade calculation nodes for final result storage
|
||||
|
||||
#### Grade Retrieval
|
||||
|
||||
**`int get_final_course_grade(const StudentCourse& sc)`**
|
||||
> Returns:
|
||||
> - `> 0`: Valid final grade (rounded average)
|
||||
> - `-1`: No exams taken or no results found
|
||||
|
||||
- Gets final calculated grade for a student-course combination
|
||||
- Performs average calculation with proper rounding
|
||||
- Used by nodes to check if final grading is complete
|
||||
|
||||
### Logging
|
||||
|
||||
- Uses ROS2 logger with `[DBS]` prefix for database operations
|
||||
- Different log levels:
|
||||
- `INFO`: Successful operations, connection status
|
||||
- `ERROR`: SQL errors, connection failures, configuration issues
|
||||
- `WARN`: Non-critical issues, missing configurations
|
||||
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Basic Database Setup
|
||||
```cpp
|
||||
// Create DatabaseManager with ROS2 logger
|
||||
DatabaseManager db_manager(node->get_logger());
|
||||
|
||||
// Check connection status
|
||||
if (db_manager.is_connected()) {
|
||||
// Database ready for operations
|
||||
bool success = db_manager.store_exam_result("Wessel", "ROS2", 85);
|
||||
if (success) {
|
||||
RCLCPP_INFO(logger, "Exam result stored successfully");
|
||||
}
|
||||
}
|
||||
```
|
||||
16
doc/interfaces/interfaces.md
Normal file
16
doc/interfaces/interfaces.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Interface Overview
|
||||
|
||||
**Publishers**
|
||||
- Topic: `exam_results`
|
||||
- Message Type: `g2_2025_interfaces::msg::Exam`
|
||||
- Publishes generated exam results to downstream nodes
|
||||
|
||||
**Subscribers**
|
||||
- Topic: `student_course_management`
|
||||
- Message Type: `g2_2025_interfaces::msg::Student`
|
||||
- Handles requests to add/remove student-course combinations
|
||||
|
||||
**Timers**
|
||||
- Interval: 2 seconds
|
||||
- Function: `generate_random_result()`
|
||||
- Generates and publishes exam results at regular intervals
|
||||
38
doc/nodes/ExamResultGenerator.md
Normal file
38
doc/nodes/ExamResultGenerator.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# ExamResultGenerator (`assignments::one::exam_result_generator`)
|
||||
|
||||
## Overview
|
||||
The `ExamResultGenerator` is the core node responsible for simulating exam result generation.
|
||||
It maintains a queue of student-course combinations that need exam results, generates random
|
||||
grades, and publishes them to the system.
|
||||
|
||||
#### Implementation Details
|
||||
|
||||
**Constructor**
|
||||
```cpp
|
||||
ExamResultGenerator()
|
||||
```
|
||||
- Initializes ROS2 node with name `exam_result_generator`
|
||||
- Sets up random number generation infrastructure
|
||||
- Creates DatabaseManager instance with node logger
|
||||
- Establishes ROS2 publishers, subscribers, and timers
|
||||
- Loads initial pending combinations from database
|
||||
|
||||
**Core Functions**
|
||||
|
||||
**`void queue_pending_combinations()`**
|
||||
- Gets all student-course combinations needing exam results
|
||||
- Populates operations queue from database
|
||||
- Called at initialization and when database state changes
|
||||
|
||||
**`void generate_random_result()`**
|
||||
- Main exam result generation executed by timer
|
||||
- Selects random student-course combination from queue
|
||||
- Stores result in database and publishes to ROS2 topic
|
||||
|
||||
**`void student_management_callback(const g2_2025_interfaces::msg::Student::SharedPtr msg)`**
|
||||
- Toggles combinations in/out of processing queue
|
||||
- Updates database with new enrollments
|
||||
|
||||
**`void add_student_course_combination(const StudentCourse& sc)`**
|
||||
- Enrolls student into course via database
|
||||
- Adds combination to processing queue
|
||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
postgres-db:
|
||||
image: postgres:15
|
||||
container_name: postgres-db
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_DB=grades
|
||||
ports:
|
||||
- "5432:5432"
|
||||
12
src/config.toml
Normal file
12
src/config.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[database]
|
||||
host = "localhost"
|
||||
port = 5432
|
||||
dbname = "grades"
|
||||
user = "postgres"
|
||||
password = "postgres"
|
||||
timeout = 30
|
||||
ssl = false
|
||||
|
||||
[database.pool]
|
||||
min_connections = 1
|
||||
max_connections = 10
|
||||
@@ -5,21 +5,53 @@ if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
# external packages
|
||||
include(FetchContent)
|
||||
|
||||
FetchContent_Declare(
|
||||
tomlplusplus
|
||||
GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git
|
||||
GIT_TAG v3.4.0
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(tomlplusplus)
|
||||
|
||||
# find dependencies
|
||||
find_package(ament_cmake REQUIRED)
|
||||
find_package(rclcpp REQUIRED)
|
||||
find_package(g2_2025_interfaces REQUIRED)
|
||||
|
||||
add_executable(exam_result_generator src/exam_result_generator/main.cpp)
|
||||
add_executable(final_grade_determinator src/final_grade_determinator/main.cpp)
|
||||
add_executable(grade_calculator src/grade_calculator/main.cpp)
|
||||
add_executable(retake_grade_determinator src/retake_grade_determinator/main.cpp)
|
||||
add_executable(retake_scheduler src/retake_scheduler/main.cpp)
|
||||
|
||||
add_executable(exam_result_generator
|
||||
src/exam_result_generator/Main.cpp
|
||||
src/database/DatabaseManager.cpp
|
||||
src/config/ConfigManager.cpp
|
||||
src/exam_result_generator/nodes/ExamResultGenerator.cpp
|
||||
)
|
||||
target_include_directories(exam_result_generator PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/exam_result_generator
|
||||
)
|
||||
ament_target_dependencies(exam_result_generator rclcpp g2_2025_interfaces)
|
||||
target_link_libraries(exam_result_generator pqxx pq tomlplusplus::tomlplusplus)
|
||||
|
||||
add_executable(final_grade_determinator
|
||||
src/final_grade_determinator/main.cpp
|
||||
)
|
||||
ament_target_dependencies(final_grade_determinator rclcpp g2_2025_interfaces)
|
||||
|
||||
add_executable(grade_calculator
|
||||
src/grade_calculator/main.cpp
|
||||
)
|
||||
ament_target_dependencies(grade_calculator rclcpp g2_2025_interfaces)
|
||||
|
||||
add_executable(retake_grade_determinator
|
||||
src/retake_grade_determinator/main.cpp
|
||||
)
|
||||
ament_target_dependencies(retake_grade_determinator rclcpp g2_2025_interfaces)
|
||||
|
||||
add_executable(retake_scheduler
|
||||
src/retake_scheduler/main.cpp
|
||||
)
|
||||
ament_target_dependencies(retake_scheduler rclcpp g2_2025_interfaces)
|
||||
|
||||
install (
|
||||
|
||||
112
src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.cpp
Normal file
112
src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
#include "ConfigManager.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
|
||||
namespace assignments::one {
|
||||
|
||||
ConfigManager::ConfigManager(rclcpp::Logger logger)
|
||||
: logger_(logger), loaded_(false)
|
||||
{
|
||||
// Try to auto-load config from default location
|
||||
std::string config_path = find_config_file();
|
||||
if (!config_path.empty()) {
|
||||
load_config(config_path);
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfigManager::load_config(const std::string& config_file_path) {
|
||||
try {
|
||||
RCLCPP_INFO(logger_, "[CFG] '%s': loading configuration", config_file_path.c_str());
|
||||
|
||||
if (!std::filesystem::exists(config_file_path)) {
|
||||
RCLCPP_ERROR(logger_, "[CFG] '%s': file does not exist", config_file_path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
config_ = toml::parse_file(config_file_path);
|
||||
loaded_ = true;
|
||||
|
||||
RCLCPP_INFO(logger_, "[CFG] '%s': configuration loaded", config_file_path.c_str());
|
||||
return true;
|
||||
|
||||
} catch (const toml::parse_error& e) {
|
||||
RCLCPP_ERROR(logger_, "[CFG] '%s': failed to parse: %s", config_file_path.c_str(), e.what());
|
||||
loaded_ = false;
|
||||
return false;
|
||||
} catch (const std::exception& e) {
|
||||
RCLCPP_ERROR(logger_, "[CFG] '%s': failed to load: %s", config_file_path.c_str(), e.what());
|
||||
loaded_ = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<DatabaseConfig> ConfigManager::get_database_config() const {
|
||||
if (!loaded_ || !config_.has_value()) {
|
||||
RCLCPP_ERROR(logger_, "[CFG] database configuration not loaded");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
try {
|
||||
return parse_database_config(config_.value());
|
||||
} catch (const std::exception& e) {
|
||||
RCLCPP_ERROR(logger_, "[CFG] database configuration failed to parse: %s", e.what());
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfigManager::is_loaded() const {
|
||||
return loaded_;
|
||||
}
|
||||
|
||||
std::string ConfigManager::find_config_file() const {
|
||||
// Look for config file in several locations
|
||||
for (const auto& path : default_config_paths_) {
|
||||
if (std::filesystem::exists(path)) {
|
||||
RCLCPP_INFO(logger_, "[CFG] '%s': found configuration file", path.c_str());
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
RCLCPP_WARN(logger_, "[CFG] no configuration file found at default locations");
|
||||
return "";
|
||||
}
|
||||
|
||||
DatabaseConfig ConfigManager::parse_database_config(const toml::table& config) const {
|
||||
DatabaseConfig db_config;
|
||||
|
||||
// Parse database section
|
||||
auto database_section = config["database"];
|
||||
if (!database_section) {
|
||||
throw std::runtime_error("missing [database] section in configuration");
|
||||
}
|
||||
|
||||
db_config.host = database_section["host"].value_or<std::string>("localhost");
|
||||
db_config.port = database_section["port"].value_or<int>(5432);
|
||||
db_config.dbname = database_section["dbname"].value_or<std::string>("grades");
|
||||
db_config.user = database_section["user"].value_or<std::string>("postgres");
|
||||
db_config.password = database_section["password"].value_or<std::string>("postgres");
|
||||
db_config.timeout = database_section["timeout"].value_or<int>(30);
|
||||
db_config.ssl = database_section["ssl"].value_or<bool>(false);
|
||||
|
||||
// Parse pool section if present
|
||||
auto pool_section = database_section["pool"];
|
||||
if (pool_section) {
|
||||
db_config.min_connections = pool_section["min_connections"].value_or<int>(1);
|
||||
db_config.max_connections = pool_section["max_connections"].value_or<int>(10);
|
||||
} else {
|
||||
db_config.min_connections = 1;
|
||||
db_config.max_connections = 10;
|
||||
}
|
||||
|
||||
RCLCPP_INFO(logger_, "[CFG] database config parsed - %s:%s@%s:%d",
|
||||
db_config.user.c_str(),
|
||||
db_config.dbname.c_str(),
|
||||
db_config.host.c_str(),
|
||||
db_config.port
|
||||
);
|
||||
|
||||
return db_config;
|
||||
}
|
||||
|
||||
} // namespace assignments::one
|
||||
@@ -0,0 +1,52 @@
|
||||
/* ConfigManager.hpp
|
||||
* Configuration management for the exam result generator
|
||||
* Uses toml++ library to parse TOML configuration files
|
||||
*
|
||||
* Reviewed by: <x>
|
||||
* Changelog:
|
||||
* [23-09-2025] Wessel T: Created configuration manager class for TOML config loading
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <filesystem>
|
||||
#include <toml++/toml.h>
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
#include "DatabaseConfig.hpp"
|
||||
|
||||
namespace assignments::one {
|
||||
|
||||
class ConfigManager {
|
||||
public:
|
||||
explicit ConfigManager(rclcpp::Logger logger);
|
||||
~ConfigManager() = default;
|
||||
|
||||
bool load_config(const std::string& config_file_path);
|
||||
|
||||
std::optional<DatabaseConfig> get_database_config() const;
|
||||
|
||||
bool is_loaded() const;
|
||||
|
||||
private:
|
||||
rclcpp::Logger logger_;
|
||||
|
||||
std::optional<toml::table> config_;
|
||||
|
||||
bool loaded_ { false };
|
||||
std::vector<std::string> default_config_paths_ = {
|
||||
"config.toml",
|
||||
"./src/config.toml",
|
||||
"../config.toml",
|
||||
"../../config.toml",
|
||||
"../../../config.toml",
|
||||
"../../../../config.toml",
|
||||
"/etc/ros2_grade_calculator/config.toml"
|
||||
};
|
||||
|
||||
std::string find_config_file() const;
|
||||
DatabaseConfig parse_database_config(const toml::table& config) const;
|
||||
};
|
||||
|
||||
} // namespace assignments::one
|
||||
@@ -0,0 +1,46 @@
|
||||
/* DatabaseConfig.hpp
|
||||
* Database configuration structure for the exam result generator
|
||||
*
|
||||
* Reviewed by: <x>
|
||||
* Changelog:
|
||||
* [23-09-2025] Wessel T: Create initial configuration structure
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace assignments::one {
|
||||
|
||||
struct DatabaseConfig {
|
||||
std::string host;
|
||||
int port;
|
||||
std::string dbname;
|
||||
std::string user;
|
||||
std::string password;
|
||||
int timeout;
|
||||
bool ssl;
|
||||
|
||||
// Pool settings
|
||||
int min_connections;
|
||||
int max_connections;
|
||||
|
||||
std::string to_connection_string() const {
|
||||
std::string conn_str =
|
||||
"host=" + host +
|
||||
" port=" + std::to_string(port) +
|
||||
" dbname=" + dbname +
|
||||
" user=" + user +
|
||||
" password=" + password +
|
||||
" connect_timeout=" + std::to_string(timeout);
|
||||
|
||||
if (ssl) {
|
||||
conn_str += " sslmode=require";
|
||||
} else {
|
||||
conn_str += " sslmode=disable";
|
||||
}
|
||||
|
||||
return conn_str;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace assignments::one
|
||||
@@ -0,0 +1,238 @@
|
||||
#include "DatabaseManager.hpp"
|
||||
|
||||
#include <pqxx/pqxx>
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
#include "SQLQueries.hpp"
|
||||
#include "config/ConfigManager.hpp"
|
||||
|
||||
namespace assignments::one {
|
||||
|
||||
DatabaseManager::DatabaseManager(rclcpp::Logger logger) : logger_(logger) {
|
||||
config_manager_ = std::make_unique<ConfigManager>(logger_);
|
||||
init_database();
|
||||
}
|
||||
|
||||
bool DatabaseManager::is_connected() const {
|
||||
return conn_ && conn_->is_open();
|
||||
}
|
||||
|
||||
bool DatabaseManager::connect(const std::string& connection_string) {
|
||||
try {
|
||||
RCLCPP_INFO(logger_, "[DBS] connecting to PostgreSQL database...");
|
||||
|
||||
conn_ = std::make_unique<pqxx::connection>(connection_string);
|
||||
|
||||
if (conn_->is_open()) {
|
||||
RCLCPP_INFO(logger_, "[DBS] successfully connected to database (%s)", conn_->dbname());
|
||||
return true;
|
||||
} else {
|
||||
RCLCPP_ERROR(logger_, "[DBS] failed to open database connection");
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (const pqxx::sql_error &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] sql error: %s", e.what());
|
||||
return false;
|
||||
} catch (const std::exception &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] connection error: %s", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseManager::init_database() {
|
||||
if (!config_manager_ || !config_manager_->is_loaded()) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] configuration not loaded, cannot initialize database");
|
||||
return;
|
||||
}
|
||||
|
||||
auto db_config = config_manager_->get_database_config();
|
||||
if (!db_config.has_value()) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] failed to get database configuration");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string connection_string = db_config->to_connection_string();
|
||||
|
||||
if (connect(connection_string)) {
|
||||
create_tables();
|
||||
insert_sample_data();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<StudentCourse> DatabaseManager::queue_pending_combinations() {
|
||||
std::vector<StudentCourse> combinations;
|
||||
|
||||
if (!is_connected()) {
|
||||
return combinations;
|
||||
}
|
||||
|
||||
try {
|
||||
pqxx::work txn(*conn_);
|
||||
|
||||
auto result = txn.exec(SQL_SELECT_MISSING_RESULTS);
|
||||
|
||||
for (const auto& row : result) {
|
||||
StudentCourse sc;
|
||||
sc.student_name = row[0].as<std::string>();
|
||||
sc.course_name = row[1].as<std::string>();
|
||||
combinations.push_back(sc);
|
||||
}
|
||||
|
||||
RCLCPP_INFO(logger_, "[DBS] queued %zu pending combinations", combinations.size());
|
||||
|
||||
} catch (const pqxx::sql_error &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] queueing combinations failed: %s", e.what());
|
||||
} catch (const std::exception &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
|
||||
}
|
||||
|
||||
return combinations;
|
||||
}
|
||||
|
||||
bool DatabaseManager::enroll_student_into_course(const StudentCourse& sc) {
|
||||
if (!is_connected()) return false;
|
||||
|
||||
try {
|
||||
pqxx::work txn(*conn_);
|
||||
|
||||
txn.exec_params(SQL_INSERT_STUDENT_ENROLLMENT, sc.student_name, sc.course_name);
|
||||
|
||||
txn.commit();
|
||||
return true;
|
||||
|
||||
} catch (const pqxx::sql_error &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] enroll student into course failed: %s", e.what());
|
||||
return false;
|
||||
} catch (const std::exception &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool DatabaseManager::store_exam_result(const std::string& student_name, const std::string& course_name, int grade) {
|
||||
if (!is_connected()) return false;
|
||||
|
||||
try {
|
||||
pqxx::work txn(*conn_);
|
||||
|
||||
txn.exec_params(
|
||||
SQL_INSERT_EXAM_RESULT,
|
||||
student_name, course_name, grade
|
||||
);
|
||||
|
||||
txn.commit();
|
||||
return true;
|
||||
|
||||
} catch (const pqxx::sql_error &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] store result failed: %s", e.what());
|
||||
return false;
|
||||
} catch (const std::exception &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool DatabaseManager::store_final_course_result(
|
||||
const StudentCourse& sc,
|
||||
int exam_count,
|
||||
int final_grade
|
||||
) {
|
||||
if (!is_connected()) return false;
|
||||
|
||||
try {
|
||||
pqxx::work txn(*conn_);
|
||||
|
||||
txn.exec_params(
|
||||
SQL_INSERT_FINAL_COURSE_RESULT,
|
||||
sc.student_name, sc.course_name, exam_count, final_grade
|
||||
);
|
||||
|
||||
txn.commit();
|
||||
return true;
|
||||
|
||||
} catch (const pqxx::sql_error &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] store final course result failed: %s", e.what());
|
||||
return false;
|
||||
} catch (const std::exception &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseManager::create_tables() {
|
||||
if (!conn_ || !conn_->is_open()) return;
|
||||
|
||||
try {
|
||||
pqxx::work txn(*conn_);
|
||||
|
||||
txn.exec(SQL_CREATE_ENROLLMENTS_TABLE);
|
||||
txn.exec(SQL_CREATE_EXAM_RESULTS);
|
||||
txn.exec(SQL_CREATE_COURSE_RESULTS);
|
||||
|
||||
txn.commit();
|
||||
|
||||
RCLCPP_INFO(logger_, "[DBS] database tables ready");
|
||||
|
||||
} catch (const pqxx::sql_error &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] CREATE TABLE failed: %s", e.what());
|
||||
} catch (const std::exception &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseManager::insert_sample_data() {
|
||||
if (!is_connected()) return;
|
||||
|
||||
try {
|
||||
pqxx::work txn(*conn_);
|
||||
|
||||
// Check if we already have data
|
||||
auto result = txn.exec(SQL_SELECT_STUDENT_LIST);
|
||||
int count = result[0][0].as<int>();
|
||||
|
||||
if (count == 0) {
|
||||
for (const auto& [student, course] : sample_students_data_) {
|
||||
txn.exec_params(SQL_INSERT_STUDENT_ENROLLMENT, student, course);
|
||||
}
|
||||
|
||||
txn.commit();
|
||||
RCLCPP_INFO(logger_, "[DBS] inserted sample student enrollment data");
|
||||
}
|
||||
|
||||
} catch (const pqxx::sql_error &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] insert sample data failed: %s", e.what());
|
||||
} catch (const std::exception &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
int DatabaseManager::get_final_course_grade(const StudentCourse& sc) {
|
||||
if (!is_connected()) return -1;
|
||||
|
||||
try {
|
||||
pqxx::work txn(*conn_);
|
||||
|
||||
auto result = txn.exec_params(SQL_SELECT_STUDENT_COURSE_RESULTS, sc.student_name, sc.course_name);
|
||||
|
||||
if (result.size() == 1) {
|
||||
int exam_count = result[0][0].as<int>();
|
||||
if (exam_count == 0) {
|
||||
return -1; // No exams taken
|
||||
}
|
||||
int avg_grade = static_cast<int>(result[0][1].as<double>() + 0.5);
|
||||
return avg_grade;
|
||||
} else {
|
||||
return -1; // No results found
|
||||
}
|
||||
|
||||
} catch (const pqxx::sql_error &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] get final course grade failed: %s", e.what());
|
||||
return -1;
|
||||
} catch (const std::exception &e) {
|
||||
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace assignments::one
|
||||
@@ -0,0 +1,56 @@
|
||||
/* DatabaseManager.hpp
|
||||
* Database manager for the exam result generator
|
||||
*
|
||||
* Reviewed by: <x>
|
||||
* Changelog:
|
||||
* [23-09-2025] Wessel T: Created database manager class for all DB operations
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <pqxx/pqxx>
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
#include "StudentCourse.hpp"
|
||||
#include "config/ConfigManager.hpp"
|
||||
|
||||
namespace assignments::one {
|
||||
|
||||
class DatabaseManager {
|
||||
public:
|
||||
explicit DatabaseManager(rclcpp::Logger logger);
|
||||
~DatabaseManager() = default;
|
||||
|
||||
bool connect(const std::string& connection_string);
|
||||
bool is_connected() const;
|
||||
|
||||
// Table operations
|
||||
void init_database();
|
||||
void create_tables();
|
||||
void insert_sample_data();
|
||||
|
||||
// Data operations
|
||||
std::vector<StudentCourse> queue_pending_combinations();
|
||||
bool store_exam_result(const std::string& student_name, const std::string& course_name, int grade);
|
||||
bool enroll_student_into_course(const StudentCourse& sc);
|
||||
bool store_final_course_result(const StudentCourse& sc, int exam_count, int final_grade);
|
||||
|
||||
int get_final_course_grade(const StudentCourse& sc);
|
||||
|
||||
private:
|
||||
rclcpp::Logger logger_;
|
||||
|
||||
std::unique_ptr<pqxx::connection> conn_;
|
||||
std::unique_ptr<ConfigManager> config_manager_;
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> sample_students_data_ = {
|
||||
{"Wessel", "ROS2"},
|
||||
{"Vincent", "ROS2"},
|
||||
{"Mohammed", "Differentieren"},
|
||||
{"Tilmann", "Differentieren"},
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace assignments::one
|
||||
82
src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp
Normal file
82
src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp
Normal file
@@ -0,0 +1,82 @@
|
||||
/* SQLQueries.hpp
|
||||
* SQL query definitions for the exam result generator
|
||||
*
|
||||
* Reviewed by: <x>
|
||||
* Changelog:
|
||||
* [23-09-2025] Wessel T: Created initial database state
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
static const std::string SQL_CREATE_ENROLLMENTS_TABLE = R"(
|
||||
CREATE TABLE IF NOT EXISTS student_enrollments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
student_name VARCHAR(100) NOT NULL,
|
||||
course_name VARCHAR(100) NOT NULL,
|
||||
is_retake BOOLEAN DEFAULT FALSE,
|
||||
enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
)";
|
||||
// UNIQUE(student_name, course_name)
|
||||
|
||||
static const std::string SQL_CREATE_EXAM_RESULTS = R"(
|
||||
CREATE TABLE IF NOT EXISTS exam_results (
|
||||
id SERIAL PRIMARY KEY,
|
||||
student_name VARCHAR(100) NOT NULL,
|
||||
course_name VARCHAR(100) NOT NULL,
|
||||
exam_grade INTEGER NOT NULL,
|
||||
is_retake BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
)";
|
||||
// UNIQUE(student_name, course_name)
|
||||
|
||||
static const std::string SQL_CREATE_COURSE_RESULTS = R"(
|
||||
CREATE TABLE IF NOT EXISTS final_course_results (
|
||||
id SERIAL PRIMARY KEY,
|
||||
student_name VARCHAR(100) NOT NULL,
|
||||
course_name VARCHAR(100) NOT NULL,
|
||||
exam_count INTEGER NOT NULL,
|
||||
final_grade INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(student_name, course_name)
|
||||
);
|
||||
)";
|
||||
|
||||
static const std::string SQL_SELECT_MISSING_RESULTS = R"(
|
||||
SELECT se.student_name, se.course_name
|
||||
FROM student_enrollments se
|
||||
LEFT JOIN final_course_results fcr
|
||||
ON se.student_name = fcr.student_name
|
||||
AND se.course_name = fcr.course_name
|
||||
WHERE fcr.id IS NULL
|
||||
)";
|
||||
|
||||
static const std::string SQL_SELECT_STUDENT_COURSE_RESULTS = R"(
|
||||
SELECT COUNT(*), AVG(exam_grade)
|
||||
FROM exam_results
|
||||
WHERE student_name = $1 AND course_name = $2
|
||||
)";
|
||||
|
||||
static const std::string SQL_INSERT_EXAM_RESULT = R"(
|
||||
INSERT INTO exam_results (student_name, course_name, exam_grade)
|
||||
VALUES ($1, $2, $3)
|
||||
)";
|
||||
// ON CONFLICT (student_name, course_name)
|
||||
// DO UPDATE SET exam_grade = EXCLUDED.exam_grade, created_at = CURRENT_TIMESTAMP
|
||||
|
||||
static const std::string SQL_INSERT_STUDENT_ENROLLMENT = R"(
|
||||
INSERT INTO student_enrollments (student_name, course_name)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
)";
|
||||
|
||||
static const std::string SQL_SELECT_STUDENT_LIST = R"(
|
||||
SELECT COUNT(*) FROM student_enrollments
|
||||
)";
|
||||
|
||||
static const std::string SQL_INSERT_FINAL_COURSE_RESULT = R"(
|
||||
INSERT INTO final_course_results (student_name, course_name, exam_count, final_grade)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
)";
|
||||
// ON CONFLICT (student_name, course_name)
|
||||
// DO UPDATE SET exam_count = EXCLUDED.exam_count, final_grade = EXCLUDED.final_grade, created_at = CURRENT_TIMESTAMP
|
||||
@@ -0,0 +1,23 @@
|
||||
/* StudentCourse.hpp
|
||||
* Data structure for student-course combinations
|
||||
*
|
||||
* Reviewed by: <x>
|
||||
* Changelog:
|
||||
* [23-09-2025] Wessel T: Created initial structure
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace assignments::one {
|
||||
|
||||
struct StudentCourse {
|
||||
std::string student_name;
|
||||
std::string course_name;
|
||||
|
||||
bool operator==(const StudentCourse& other) const {
|
||||
return student_name == other.student_name && course_name == other.course_name;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace assignments::one
|
||||
@@ -0,0 +1,21 @@
|
||||
/* main.cpp
|
||||
* Entry point for the exam result generator node
|
||||
*
|
||||
* Reviewed by: <x>
|
||||
* Changelog:
|
||||
* [23-09-2025] Wessel T: Simplified main.cpp to entry point only
|
||||
*/
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "nodes/ExamResultGenerator.hpp"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
rclcpp::init(argc, argv);
|
||||
|
||||
auto node = std::make_shared<assignments::one::exam_result_generator::ExamResultGenerator>();
|
||||
|
||||
rclcpp::spin(node);
|
||||
rclcpp::shutdown();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/* node_template.cpp
|
||||
* Action server node template for ROS2
|
||||
*
|
||||
* Node description:
|
||||
* Template action server that demonstrates action server implementation
|
||||
* with goal handling, feedback publishing, and cancellation support
|
||||
*
|
||||
* Reviewed by: <x>
|
||||
* Changelog:
|
||||
* [04-09-2025] Wessel T: Implement template
|
||||
*/
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
namespace lessons::zero::tmp {
|
||||
|
||||
class NodeTemplate : public rclcpp::Node {
|
||||
public:
|
||||
NodeTemplate()
|
||||
: Node("node_template")
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
} // namespace lessons::zero::template
|
||||
|
||||
int main(int argc,char *argv[]) {
|
||||
rclcpp::init(argc,argv);
|
||||
|
||||
auto node = std::make_shared<lessons::zero::tmp::NodeTemplate>();
|
||||
|
||||
rclcpp::spin(node);
|
||||
rclcpp::shutdown();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
#include "ExamResultGenerator.hpp"
|
||||
|
||||
namespace assignments::one::exam_result_generator {
|
||||
|
||||
ExamResultGenerator::ExamResultGenerator()
|
||||
: Node("exam_result_generator"),
|
||||
gen_(rd_()),
|
||||
grade_dist_(10, 100)
|
||||
{
|
||||
db_manager_ = std::make_unique<DatabaseManager>(this->get_logger());
|
||||
|
||||
// Load initial student/course combinations from database
|
||||
queue_pending_combinations();
|
||||
|
||||
// Create publisher for exam results
|
||||
exam_publisher_ = this->create_publisher<g2_2025_interfaces::msg::Exam>("exam_results", 10);
|
||||
|
||||
// Create subscriber for adding/removing student/course combinations
|
||||
student_subscriber_ = this->create_subscription<g2_2025_interfaces::msg::Student>(
|
||||
"student_course_management", 10,
|
||||
std::bind(
|
||||
&ExamResultGenerator::student_management_callback,
|
||||
this,
|
||||
std::placeholders::_1
|
||||
)
|
||||
);
|
||||
|
||||
// Create timer to generate random exam results every 2 seconds
|
||||
timer_ = this->create_wall_timer(
|
||||
std::chrono::seconds(2),
|
||||
std::bind(&ExamResultGenerator::generate_random_result, this)
|
||||
);
|
||||
|
||||
RCLCPP_INFO(this->get_logger(),
|
||||
"exam_result_generator started, %zu pending operations",
|
||||
operations_queue_.size()
|
||||
);
|
||||
}
|
||||
|
||||
void ExamResultGenerator::queue_pending_combinations() {
|
||||
if (!db_manager_ || !db_manager_->is_connected()) return;
|
||||
|
||||
operations_queue_ = db_manager_->queue_pending_combinations();
|
||||
}
|
||||
|
||||
void ExamResultGenerator::generate_random_result() {
|
||||
if (operations_queue_.empty()) {
|
||||
RCLCPP_WARN_THROTTLE(this->get_logger(), *this->get_clock(), 10000, "queue is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!db_manager_ || !db_manager_->is_connected()) {
|
||||
RCLCPP_WARN(this->get_logger(), "no database connection");
|
||||
return;
|
||||
}
|
||||
|
||||
// Select random student/course combination
|
||||
std::uniform_int_distribution<> index_dist(0, operations_queue_.size() - 1);
|
||||
int random_index = index_dist(gen_);
|
||||
auto selected = operations_queue_[random_index];
|
||||
|
||||
int grade = grade_dist_(gen_);
|
||||
|
||||
db_manager_->store_exam_result(selected.student_name, selected.course_name, grade);
|
||||
|
||||
// Publish exam result
|
||||
auto exam_msg = g2_2025_interfaces::msg::Exam();
|
||||
exam_msg.course_name = selected.course_name;
|
||||
exam_msg.result = grade;
|
||||
|
||||
exam_publisher_->publish(exam_msg);
|
||||
|
||||
RCLCPP_INFO(this->get_logger(),
|
||||
"generated grade: (%d) %s in %s",
|
||||
grade, selected.student_name.c_str(), selected.course_name.c_str()
|
||||
);
|
||||
}
|
||||
|
||||
void ExamResultGenerator::student_management_callback(const g2_2025_interfaces::msg::Student::SharedPtr msg) {
|
||||
StudentCourse sc;
|
||||
sc.student_name = msg->student_name;
|
||||
sc.course_name = msg->course_name;
|
||||
|
||||
auto it = std::find(operations_queue_.begin(), operations_queue_.end(), sc);
|
||||
|
||||
if (it != operations_queue_.end()) {
|
||||
operations_queue_.erase(it);
|
||||
RCLCPP_INFO(this->get_logger(), "removed from queue: %s - %s",
|
||||
sc.student_name.c_str(), sc.course_name.c_str()
|
||||
);
|
||||
} else {
|
||||
add_student_course_combination(sc);
|
||||
RCLCPP_INFO(this->get_logger(), "added to queue: %s - %s",
|
||||
sc.student_name.c_str(), sc.course_name.c_str()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void ExamResultGenerator::add_student_course_combination(const StudentCourse& sc) {
|
||||
if (!db_manager_ || !db_manager_->is_connected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (db_manager_->enroll_student_into_course(sc)) {
|
||||
auto it = std::find(operations_queue_.begin(), operations_queue_.end(), sc);
|
||||
if (it == operations_queue_.end()) {
|
||||
operations_queue_.push_back(sc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace assignments::one::exam_result_generator
|
||||
@@ -0,0 +1,51 @@
|
||||
/* nodes/ExamResultGenerator.hpp
|
||||
* Exam result generator node for the first assignment of the course EE at InHolland.
|
||||
*
|
||||
* Collects student/course combinations from database where exam results need to be generated.
|
||||
* Randomly generates and broadcasts exam marks (10-100) every 2 seconds.
|
||||
* Can receive messages to add/remove student/course combinations.
|
||||
*
|
||||
* Reviewed by: <x>
|
||||
* Changelog:
|
||||
* [23-09-2025] Wessel T: Initial Implementation
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "g2_2025_interfaces/msg/exam.hpp"
|
||||
#include "g2_2025_interfaces/msg/student.hpp"
|
||||
|
||||
#include "database/DatabaseManager.hpp"
|
||||
#include "database/StudentCourse.hpp"
|
||||
|
||||
namespace assignments::one::exam_result_generator {
|
||||
|
||||
class ExamResultGenerator : public rclcpp::Node {
|
||||
public:
|
||||
ExamResultGenerator();
|
||||
|
||||
private:
|
||||
rclcpp::TimerBase::SharedPtr timer_;
|
||||
rclcpp::Publisher<g2_2025_interfaces::msg::Exam>::SharedPtr exam_publisher_;
|
||||
rclcpp::Subscription<g2_2025_interfaces::msg::Student>::SharedPtr student_subscriber_;
|
||||
|
||||
std::unique_ptr<DatabaseManager> db_manager_;
|
||||
|
||||
std::random_device rd_;
|
||||
std::mt19937 gen_;
|
||||
std::uniform_int_distribution<> grade_dist_;
|
||||
|
||||
std::vector<StudentCourse> operations_queue_;
|
||||
|
||||
void queue_pending_combinations();
|
||||
void generate_random_result();
|
||||
void student_management_callback(const g2_2025_interfaces::msg::Student::SharedPtr msg);
|
||||
void add_student_course_combination(const StudentCourse& sc);
|
||||
};
|
||||
|
||||
} // namespace lessons::zero::tmp
|
||||
@@ -8,12 +8,14 @@ endif()
|
||||
# find dependencies
|
||||
find_package(ament_cmake REQUIRED)
|
||||
find_package(rosidl_default_generators REQUIRED)
|
||||
find_package(builtin_interfaces REQUIRED)
|
||||
|
||||
rosidl_generate_interfaces(${PROJECT_NAME}
|
||||
"msg/Student.msg"
|
||||
"msg/Exam.msg"
|
||||
"srv/Exam.srv"
|
||||
"srv/Exams.srv"
|
||||
"action/Retake.action"
|
||||
DEPENDENCIES builtin_interfaces
|
||||
)
|
||||
|
||||
ament_export_dependencies(rosidl_default_runtime)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
string student_name
|
||||
string lecture_name
|
||||
string course_name
|
||||
---
|
||||
---
|
||||
float32 result
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
string lecture_name
|
||||
float32 result
|
||||
string course_name
|
||||
int32 result
|
||||
builtin_interfaces/Time timestamp
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
string student_name
|
||||
string lecture_name
|
||||
string course_name
|
||||
builtin_interfaces/Time timestamp
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
<buildtool_depend>ament_cmake</buildtool_depend>
|
||||
|
||||
<build_depend>rosidl_default_generators</build_depend>
|
||||
<build_depend>builtin_interfaces</build_depend>
|
||||
<exec_depend>rosidl_default_runtime</exec_depend>
|
||||
<exec_depend>builtin_interfaces</exec_depend>
|
||||
<member_of_group>rosidl_interface_packages</member_of_group>
|
||||
|
||||
<test_depend>ament_lint_auto</test_depend>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Request
|
||||
string student_name
|
||||
string lecture_name
|
||||
---
|
||||
float32 result
|
||||
7
src/g2_2025_interfaces/srv/Exams.srv
Normal file
7
src/g2_2025_interfaces/srv/Exams.srv
Normal file
@@ -0,0 +1,7 @@
|
||||
# Request
|
||||
string student_name
|
||||
string course_name
|
||||
int32[] exam_grades
|
||||
---
|
||||
# Response
|
||||
int32 result # Final result
|
||||
Reference in New Issue
Block a user