From 99b0b62f1eba37d32d15a88aa65326f6966bcb4c Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Tue, 23 Sep 2025 18:42:52 +0200 Subject: [PATCH 01/12] chore(fish): Update env files --- .fish/activate.fish | 95 ++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/.fish/activate.fish b/.fish/activate.fish index d95b602..9355593 100755 --- a/.fish/activate.fish +++ b/.fish/activate.fish @@ -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 -- 2.39.5 From e3028a23cd1896982177b06903c753c326a65ac3 Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Tue, 23 Sep 2025 18:43:55 +0200 Subject: [PATCH 02/12] feat(exam_result_generator): Initial implementation --- src/config.toml | 12 ++ .../CMakeLists.txt | 44 +++- .../src/exam_result_generator/Main.cpp | 21 ++ .../config/ConfigManager.cpp | 112 ++++++++++ .../config/ConfigManager.hpp | 51 +++++ .../config/DatabaseConfig.hpp | 46 +++++ .../database/DatabaseManager.cpp | 192 ++++++++++++++++++ .../database/DatabaseManager.hpp | 60 ++++++ .../database/SQLQueries.hpp | 67 ++++++ .../database/StudentCourse.hpp | 23 +++ .../src/exam_result_generator/main.cpp | 40 ---- .../nodes/ExamResultGenerator.cpp | 116 +++++++++++ .../nodes/ExamResultGenerator.hpp | 51 +++++ src/g2_2025_interfaces/msg/Exam.msg | 2 +- 14 files changed, 790 insertions(+), 47 deletions(-) create mode 100644 src/config.toml create mode 100644 src/g2_2025_grade_calculator_pkg/src/exam_result_generator/Main.cpp create mode 100644 src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/ConfigManager.cpp create mode 100644 src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/ConfigManager.hpp create mode 100644 src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/DatabaseConfig.hpp create mode 100644 src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/DatabaseManager.cpp create mode 100644 src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/DatabaseManager.hpp create mode 100644 src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/SQLQueries.hpp create mode 100644 src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/StudentCourse.hpp delete mode 100644 src/g2_2025_grade_calculator_pkg/src/exam_result_generator/main.cpp create mode 100644 src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp create mode 100644 src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.hpp diff --git a/src/config.toml b/src/config.toml new file mode 100644 index 0000000..f20f203 --- /dev/null +++ b/src/config.toml @@ -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 diff --git a/src/g2_2025_grade_calculator_pkg/CMakeLists.txt b/src/g2_2025_grade_calculator_pkg/CMakeLists.txt index 2510274..70ca1f0 100644 --- a/src/g2_2025_grade_calculator_pkg/CMakeLists.txt +++ b/src/g2_2025_grade_calculator_pkg/CMakeLists.txt @@ -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/exam_result_generator/database/DatabaseManager.cpp + src/exam_result_generator/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 ( diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/Main.cpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/Main.cpp new file mode 100644 index 0000000..09b5eb9 --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/Main.cpp @@ -0,0 +1,21 @@ +/* main.cpp + * Entry point for the exam result generator node + * + * Reviewed by: + * 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(); + + rclcpp::spin(node); + rclcpp::shutdown(); + + return 0; +} diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/ConfigManager.cpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/ConfigManager.cpp new file mode 100644 index 0000000..a212081 --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/ConfigManager.cpp @@ -0,0 +1,112 @@ +#include "ConfigManager.hpp" + +#include +#include + +namespace assignments::one::exam_result_generator { + +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 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("localhost"); + db_config.port = database_section["port"].value_or(5432); + db_config.dbname = database_section["dbname"].value_or("grades"); + db_config.user = database_section["user"].value_or("postgres"); + db_config.password = database_section["password"].value_or("postgres"); + db_config.timeout = database_section["timeout"].value_or(30); + db_config.ssl = database_section["ssl"].value_or(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(1); + db_config.max_connections = pool_section["max_connections"].value_or(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::exam_result_generator diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/ConfigManager.hpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/ConfigManager.hpp new file mode 100644 index 0000000..4bf8214 --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/ConfigManager.hpp @@ -0,0 +1,51 @@ +/* ConfigManager.hpp + * Configuration management for the exam result generator + * Uses toml++ library to parse TOML configuration files + * + * Reviewed by: + * Changelog: + * [23-09-2025] Wessel T: Created configuration manager class for TOML config loading + */ +#pragma once + +#include +#include +#include +#include +#include "rclcpp/rclcpp.hpp" + +#include "DatabaseConfig.hpp" + +namespace assignments::one::exam_result_generator { + +class ConfigManager { +public: + explicit ConfigManager(rclcpp::Logger logger); + ~ConfigManager() = default; + + bool load_config(const std::string& config_file_path); + + std::optional get_database_config() const; + + bool is_loaded() const; + +private: + rclcpp::Logger logger_; + + std::optional config_; + + bool loaded_ { false }; + std::vector default_config_paths_ = { + "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::exam_result_generator diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/DatabaseConfig.hpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/DatabaseConfig.hpp new file mode 100644 index 0000000..9aec91a --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/DatabaseConfig.hpp @@ -0,0 +1,46 @@ +/* DatabaseConfig.hpp + * Database configuration structure for the exam result generator + * + * Reviewed by: + * Changelog: + * [23-09-2025] Wessel T: Create initial configuration structure + */ +#pragma once + +#include + +namespace assignments::one::exam_result_generator { + +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; + } +}; + +} diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/DatabaseManager.cpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/DatabaseManager.cpp new file mode 100644 index 0000000..d3c8d1e --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/DatabaseManager.cpp @@ -0,0 +1,192 @@ +#include "DatabaseManager.hpp" + +#include +#include "rclcpp/rclcpp.hpp" + +#include "SQLQueries.hpp" +#include "config/ConfigManager.hpp" + +namespace assignments::one::exam_result_generator { + +DatabaseManager::DatabaseManager(rclcpp::Logger logger) : logger_(logger) { + config_manager_ = std::make_unique(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(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 DatabaseManager::queue_pending_combinations() { + std::vector 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(); + sc.lecture_name = row[1].as(); + 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.lecture_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_tentamen_result(const std::string& student_name, const std::string& lecture_name, int grade) { + if (!is_connected()) return false; + + try { + pqxx::work txn(*conn_); + + txn.exec_params( + SQL_INSERT_EXAM_RESULT, + student_name, lecture_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; + } +} + +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_LECTURE_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(); + + 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::retrieve_grade_bonus(const std::string& student_name) const { + for (const auto& [name, bonus] : grade_bonuses_) { + if (name == student_name) { + return bonus; + } + } + return 0; +} + +} // namespace assignments::one::exam_result_generator diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/DatabaseManager.hpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/DatabaseManager.hpp new file mode 100644 index 0000000..d4e8930 --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/DatabaseManager.hpp @@ -0,0 +1,60 @@ +/* DatabaseManager.hpp + * Database manager for the exam result generator + * + * Reviewed by: + * Changelog: + * [23-09-2025] Wessel T: Created database manager class for all DB operations + */ +#pragma once + +#include +#include +#include +#include +#include "rclcpp/rclcpp.hpp" + +#include "StudentCourse.hpp" +#include "config/ConfigManager.hpp" + +namespace assignments::one::exam_result_generator { + +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 queue_pending_combinations(); + bool store_tentamen_result(const std::string& student_name, const std::string& lecture_name, int grade); + bool enroll_student_into_course(const StudentCourse& sc); + + // Utility functions + int retrieve_grade_bonus(const std::string& student_name) const; + +private: + rclcpp::Logger logger_; + + std::unique_ptr conn_; + std::unique_ptr config_manager_; + + std::vector> grade_bonuses_ = { + { "Wessel", 10 } + }; + + std::vector> sample_students_data_ = { + {"Wessel", "ROS2"}, + {"Vincent", "ROS2"}, + {"Mohammed", "Differentieren"}, + {"Tilmann", "Differentieren"}, + }; +}; + +} // namespace assignments::one::exam_result_generator diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/SQLQueries.hpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/SQLQueries.hpp new file mode 100644 index 0000000..219b588 --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/SQLQueries.hpp @@ -0,0 +1,67 @@ +/* SQLQueries.hpp + * SQL query definitions for the exam result generator + * + * Reviewed by: + * 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, + lecture_name VARCHAR(100) NOT NULL, + enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(student_name, lecture_name) + ); +)"; + +static const std::string SQL_CREATE_EXAM_RESULTS = R"( + CREATE TABLE IF NOT EXISTS tentamen_results ( + id SERIAL PRIMARY KEY, + student_name VARCHAR(100) NOT NULL, + lecture_name VARCHAR(100) NOT NULL, + tentamen_grade FLOAT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(student_name, lecture_name) + ); +)"; + + +static const std::string SQL_CREATE_LECTURE_RESULTS = R"( + CREATE TABLE IF NOT EXISTS final_course_results ( + id SERIAL PRIMARY KEY, + student_name VARCHAR(100) NOT NULL, + lecture_name VARCHAR(100) NOT NULL, + final_grade FLOAT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(student_name, lecture_name) + ); +)"; + +static const std::string SQL_SELECT_MISSING_RESULTS = R"( + SELECT se.student_name, se.lecture_name + FROM student_enrollments se + LEFT JOIN final_course_results fcr + ON se.student_name = fcr.student_name + AND se.lecture_name = fcr.lecture_name + WHERE fcr.id IS NULL +)"; + +static const std::string SQL_INSERT_EXAM_RESULT = R"( + INSERT INTO tentamen_results (student_name, lecture_name, tentamen_grade) + VALUES ($1, $2, $3) + ON CONFLICT (student_name, lecture_name) + DO UPDATE SET tentamen_grade = EXCLUDED.tentamen_grade, created_at = CURRENT_TIMESTAMP +)"; + +static const std::string SQL_INSERT_STUDENT_ENROLLMENT = R"( + INSERT INTO student_enrollments (student_name, lecture_name) + VALUES ($1, $2) + ON CONFLICT DO NOTHING +)"; + +static const std::string SQL_SELECT_STUDENT_LIST = R"( + SELECT COUNT(*) FROM student_enrollments +)"; diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/StudentCourse.hpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/StudentCourse.hpp new file mode 100644 index 0000000..a1fd46b --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/StudentCourse.hpp @@ -0,0 +1,23 @@ +/* StudentCourse.hpp + * Data structure for student-course combinations + * + * Reviewed by: + * Changelog: + * [23-09-2025] Wessel T: Created initial structure + */ +#pragma once + +#include + +namespace assignments::one::exam_result_generator { + +struct StudentCourse { + std::string student_name; + std::string lecture_name; + + bool operator==(const StudentCourse& other) const { + return student_name == other.student_name && lecture_name == other.lecture_name; + } +}; + +} diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/main.cpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/main.cpp deleted file mode 100644 index 7279717..0000000 --- a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/main.cpp +++ /dev/null @@ -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: - * Changelog: - * [04-09-2025] Wessel T: Implement template - */ - -#include - -#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(); - - rclcpp::spin(node); - rclcpp::shutdown(); - - return 0; -} diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp new file mode 100644 index 0000000..f6ae35b --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp @@ -0,0 +1,116 @@ +#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(this->get_logger()); + + // Load initial student/course combinations from database + queue_pending_combinations(); + + // Create publisher for exam results + exam_publisher_ = this->create_publisher("exam_results", 10); + + // Create subscriber for adding/removing student/course combinations + student_subscriber_ = this->create_subscription( + "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]; + operations_queue_.erase(operations_queue_.begin() + random_index); + + int grade = grade_dist_(gen_); + + int bonus = db_manager_->retrieve_grade_bonus(selected.student_name); + grade = std::min(grade + bonus, 100); + + db_manager_->store_tentamen_result(selected.student_name, selected.lecture_name, grade); + + // Publish exam result + auto exam_msg = g2_2025_interfaces::msg::Exam(); + exam_msg.lecture_name = selected.lecture_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.lecture_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.lecture_name = msg->lecture_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.lecture_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.lecture_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 diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.hpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.hpp new file mode 100644 index 0000000..443b061 --- /dev/null +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.hpp @@ -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 tentamen 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: + * Changelog: + * [23-09-2025] Wessel T: Initial Implementation + */ +#pragma once + +#include +#include +#include +#include + +#include "rclcpp/rclcpp.hpp" +#include "g2_2025_interfaces/msg/exam.hpp" +#include "g2_2025_interfaces/msg/student.hpp" + +#include "exam_result_generator/database/DatabaseManager.hpp" +#include "exam_result_generator/database/StudentCourse.hpp" + +namespace assignments::one::exam_result_generator { + +class ExamResultGenerator : public rclcpp::Node { +public: + ExamResultGenerator(); + +private: + rclcpp::TimerBase::SharedPtr timer_; + rclcpp::Publisher::SharedPtr exam_publisher_; + rclcpp::Subscription::SharedPtr student_subscriber_; + + std::unique_ptr db_manager_; + + std::random_device rd_; + std::mt19937 gen_; + std::uniform_int_distribution<> grade_dist_; + + std::vector 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 diff --git a/src/g2_2025_interfaces/msg/Exam.msg b/src/g2_2025_interfaces/msg/Exam.msg index 63d87a2..67ad017 100644 --- a/src/g2_2025_interfaces/msg/Exam.msg +++ b/src/g2_2025_interfaces/msg/Exam.msg @@ -1,2 +1,2 @@ string lecture_name -float32 result +int32 result -- 2.39.5 From 985edd83a3fba67f6bebdfc441debc1147d1587b Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 25 Sep 2025 09:25:17 +0200 Subject: [PATCH 03/12] feat(database,config): Move to be project-wide --- src/g2_2025_grade_calculator_pkg/CMakeLists.txt | 4 ++-- .../src/{exam_result_generator => }/config/ConfigManager.cpp | 0 .../src/{exam_result_generator => }/config/ConfigManager.hpp | 0 .../src/{exam_result_generator => }/config/DatabaseConfig.hpp | 0 .../{exam_result_generator => }/database/DatabaseManager.cpp | 0 .../{exam_result_generator => }/database/DatabaseManager.hpp | 0 .../src/{exam_result_generator => }/database/SQLQueries.hpp | 0 .../{exam_result_generator => }/database/StudentCourse.hpp | 0 .../src/exam_result_generator/nodes/ExamResultGenerator.hpp | 4 ++-- 9 files changed, 4 insertions(+), 4 deletions(-) rename src/g2_2025_grade_calculator_pkg/src/{exam_result_generator => }/config/ConfigManager.cpp (100%) rename src/g2_2025_grade_calculator_pkg/src/{exam_result_generator => }/config/ConfigManager.hpp (100%) rename src/g2_2025_grade_calculator_pkg/src/{exam_result_generator => }/config/DatabaseConfig.hpp (100%) rename src/g2_2025_grade_calculator_pkg/src/{exam_result_generator => }/database/DatabaseManager.cpp (100%) rename src/g2_2025_grade_calculator_pkg/src/{exam_result_generator => }/database/DatabaseManager.hpp (100%) rename src/g2_2025_grade_calculator_pkg/src/{exam_result_generator => }/database/SQLQueries.hpp (100%) rename src/g2_2025_grade_calculator_pkg/src/{exam_result_generator => }/database/StudentCourse.hpp (100%) diff --git a/src/g2_2025_grade_calculator_pkg/CMakeLists.txt b/src/g2_2025_grade_calculator_pkg/CMakeLists.txt index 70ca1f0..e773c79 100644 --- a/src/g2_2025_grade_calculator_pkg/CMakeLists.txt +++ b/src/g2_2025_grade_calculator_pkg/CMakeLists.txt @@ -23,8 +23,8 @@ find_package(g2_2025_interfaces REQUIRED) add_executable(exam_result_generator src/exam_result_generator/Main.cpp - src/exam_result_generator/database/DatabaseManager.cpp - src/exam_result_generator/config/ConfigManager.cpp + src/database/DatabaseManager.cpp + src/config/ConfigManager.cpp src/exam_result_generator/nodes/ExamResultGenerator.cpp ) target_include_directories(exam_result_generator PRIVATE diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/ConfigManager.cpp b/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.cpp similarity index 100% rename from src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/ConfigManager.cpp rename to src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.cpp diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/ConfigManager.hpp b/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.hpp similarity index 100% rename from src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/ConfigManager.hpp rename to src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.hpp diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/DatabaseConfig.hpp b/src/g2_2025_grade_calculator_pkg/src/config/DatabaseConfig.hpp similarity index 100% rename from src/g2_2025_grade_calculator_pkg/src/exam_result_generator/config/DatabaseConfig.hpp rename to src/g2_2025_grade_calculator_pkg/src/config/DatabaseConfig.hpp diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/DatabaseManager.cpp b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp similarity index 100% rename from src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/DatabaseManager.cpp rename to src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/DatabaseManager.hpp b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp similarity index 100% rename from src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/DatabaseManager.hpp rename to src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/SQLQueries.hpp b/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp similarity index 100% rename from src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/SQLQueries.hpp rename to src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/StudentCourse.hpp b/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp similarity index 100% rename from src/g2_2025_grade_calculator_pkg/src/exam_result_generator/database/StudentCourse.hpp rename to src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.hpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.hpp index 443b061..630e239 100644 --- a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.hpp @@ -20,8 +20,8 @@ #include "g2_2025_interfaces/msg/exam.hpp" #include "g2_2025_interfaces/msg/student.hpp" -#include "exam_result_generator/database/DatabaseManager.hpp" -#include "exam_result_generator/database/StudentCourse.hpp" +#include "database/DatabaseManager.hpp" +#include "database/StudentCourse.hpp" namespace assignments::one::exam_result_generator { -- 2.39.5 From d6ea47bdd71995c47f29573e9fcf719073040c15 Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 25 Sep 2025 09:27:27 +0200 Subject: [PATCH 04/12] fix(exam_result_generator): Not supposed to have bonus points --- .../src/database/DatabaseManager.cpp | 9 --------- .../src/database/DatabaseManager.hpp | 8 -------- .../exam_result_generator/nodes/ExamResultGenerator.cpp | 3 --- 3 files changed, 20 deletions(-) diff --git a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp index d3c8d1e..19a8786 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp @@ -180,13 +180,4 @@ void DatabaseManager::insert_sample_data() { } } -int DatabaseManager::retrieve_grade_bonus(const std::string& student_name) const { - for (const auto& [name, bonus] : grade_bonuses_) { - if (name == student_name) { - return bonus; - } - } - return 0; -} - } // namespace assignments::one::exam_result_generator diff --git a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp index d4e8930..ff18586 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp @@ -35,20 +35,12 @@ public: std::vector queue_pending_combinations(); bool store_tentamen_result(const std::string& student_name, const std::string& lecture_name, int grade); bool enroll_student_into_course(const StudentCourse& sc); - - // Utility functions - int retrieve_grade_bonus(const std::string& student_name) const; - private: rclcpp::Logger logger_; std::unique_ptr conn_; std::unique_ptr config_manager_; - std::vector> grade_bonuses_ = { - { "Wessel", 10 } - }; - std::vector> sample_students_data_ = { {"Wessel", "ROS2"}, {"Vincent", "ROS2"}, diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp index f6ae35b..3c54df1 100644 --- a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp @@ -62,9 +62,6 @@ void ExamResultGenerator::generate_random_result() { int grade = grade_dist_(gen_); - int bonus = db_manager_->retrieve_grade_bonus(selected.student_name); - grade = std::min(grade + bonus, 100); - db_manager_->store_tentamen_result(selected.student_name, selected.lecture_name, grade); // Publish exam result -- 2.39.5 From 13eb01bdf8a47247354097a29e8e93d1ceefe99a Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 25 Sep 2025 09:28:50 +0200 Subject: [PATCH 05/12] fix(exam_result_generator): Translate `tentamen` -> `exam` --- .../src/database/DatabaseManager.cpp | 2 +- .../src/database/DatabaseManager.hpp | 2 +- .../src/database/SQLQueries.hpp | 8 ++++---- .../exam_result_generator/nodes/ExamResultGenerator.cpp | 2 +- .../exam_result_generator/nodes/ExamResultGenerator.hpp | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp index 19a8786..13eee4f 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp @@ -110,7 +110,7 @@ bool DatabaseManager::enroll_student_into_course(const StudentCourse& sc) { } } -bool DatabaseManager::store_tentamen_result(const std::string& student_name, const std::string& lecture_name, int grade) { +bool DatabaseManager::store_exam_result(const std::string& student_name, const std::string& lecture_name, int grade) { if (!is_connected()) return false; try { diff --git a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp index ff18586..3bb1869 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp @@ -33,7 +33,7 @@ public: // Data operations std::vector queue_pending_combinations(); - bool store_tentamen_result(const std::string& student_name, const std::string& lecture_name, int grade); + bool store_exam_result(const std::string& student_name, const std::string& lecture_name, int grade); bool enroll_student_into_course(const StudentCourse& sc); private: rclcpp::Logger logger_; diff --git a/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp b/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp index 219b588..1ff0186 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp @@ -18,11 +18,11 @@ static const std::string SQL_CREATE_ENROLLMENTS_TABLE = R"( )"; static const std::string SQL_CREATE_EXAM_RESULTS = R"( - CREATE TABLE IF NOT EXISTS tentamen_results ( + CREATE TABLE IF NOT EXISTS exam_results ( id SERIAL PRIMARY KEY, student_name VARCHAR(100) NOT NULL, lecture_name VARCHAR(100) NOT NULL, - tentamen_grade FLOAT NOT NULL, + exam_grade FLOAT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(student_name, lecture_name) ); @@ -50,10 +50,10 @@ static const std::string SQL_SELECT_MISSING_RESULTS = R"( )"; static const std::string SQL_INSERT_EXAM_RESULT = R"( - INSERT INTO tentamen_results (student_name, lecture_name, tentamen_grade) + INSERT INTO exam_results (student_name, lecture_name, exam_grade) VALUES ($1, $2, $3) ON CONFLICT (student_name, lecture_name) - DO UPDATE SET tentamen_grade = EXCLUDED.tentamen_grade, created_at = CURRENT_TIMESTAMP + DO UPDATE SET exam_grade = EXCLUDED.exam_grade, created_at = CURRENT_TIMESTAMP )"; static const std::string SQL_INSERT_STUDENT_ENROLLMENT = R"( diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp index 3c54df1..ddedf9e 100644 --- a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp @@ -62,7 +62,7 @@ void ExamResultGenerator::generate_random_result() { int grade = grade_dist_(gen_); - db_manager_->store_tentamen_result(selected.student_name, selected.lecture_name, grade); + db_manager_->store_exam_result(selected.student_name, selected.lecture_name, grade); // Publish exam result auto exam_msg = g2_2025_interfaces::msg::Exam(); diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.hpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.hpp index 630e239..cbb8a21 100644 --- a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.hpp @@ -1,7 +1,7 @@ /* nodes/ExamResultGenerator.hpp * Exam result generator node for the first assignment of the course EE at InHolland. * - * Collects student/course combinations from database where tentamen results need to be generated. + * 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. * -- 2.39.5 From 04cd7afb7b11aa9377d3a9fb5412227e992a6b20 Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 25 Sep 2025 09:32:04 +0200 Subject: [PATCH 06/12] fix(exam_result_generator): Keep generating grades unless told elsewise --- src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp | 4 ++-- .../src/exam_result_generator/nodes/ExamResultGenerator.cpp | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp b/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp index 1ff0186..4e8af88 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp @@ -52,9 +52,9 @@ static const std::string SQL_SELECT_MISSING_RESULTS = R"( static const std::string SQL_INSERT_EXAM_RESULT = R"( INSERT INTO exam_results (student_name, lecture_name, exam_grade) VALUES ($1, $2, $3) - ON CONFLICT (student_name, lecture_name) - DO UPDATE SET exam_grade = EXCLUDED.exam_grade, created_at = CURRENT_TIMESTAMP )"; +// ON CONFLICT (student_name, lecture_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, lecture_name) diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp index ddedf9e..2962602 100644 --- a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp @@ -58,7 +58,6 @@ void ExamResultGenerator::generate_random_result() { std::uniform_int_distribution<> index_dist(0, operations_queue_.size() - 1); int random_index = index_dist(gen_); auto selected = operations_queue_[random_index]; - operations_queue_.erase(operations_queue_.begin() + random_index); int grade = grade_dist_(gen_); -- 2.39.5 From 1cbb4d69b17c78451cf5bfdd0e911b0734c1b8db Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 25 Sep 2025 09:54:40 +0200 Subject: [PATCH 07/12] chore(interfaces): Make according to standard --- src/g2_2025_interfaces/CMakeLists.txt | 4 +++- src/g2_2025_interfaces/msg/Exam.msg | 1 + src/g2_2025_interfaces/msg/Student.msg | 1 + src/g2_2025_interfaces/package.xml | 2 ++ src/g2_2025_interfaces/srv/Exam.srv | 5 ----- src/g2_2025_interfaces/srv/Exams.srv | 7 +++++++ 6 files changed, 14 insertions(+), 6 deletions(-) delete mode 100644 src/g2_2025_interfaces/srv/Exam.srv create mode 100644 src/g2_2025_interfaces/srv/Exams.srv diff --git a/src/g2_2025_interfaces/CMakeLists.txt b/src/g2_2025_interfaces/CMakeLists.txt index 9bb5c01..a97f387 100644 --- a/src/g2_2025_interfaces/CMakeLists.txt +++ b/src/g2_2025_interfaces/CMakeLists.txt @@ -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) diff --git a/src/g2_2025_interfaces/msg/Exam.msg b/src/g2_2025_interfaces/msg/Exam.msg index 67ad017..8d1beb9 100644 --- a/src/g2_2025_interfaces/msg/Exam.msg +++ b/src/g2_2025_interfaces/msg/Exam.msg @@ -1,2 +1,3 @@ string lecture_name int32 result +builtin_interfaces/Time timestamp diff --git a/src/g2_2025_interfaces/msg/Student.msg b/src/g2_2025_interfaces/msg/Student.msg index 566c1cc..fa576b2 100644 --- a/src/g2_2025_interfaces/msg/Student.msg +++ b/src/g2_2025_interfaces/msg/Student.msg @@ -1,2 +1,3 @@ string student_name string lecture_name +builtin_interfaces/Time timestamp diff --git a/src/g2_2025_interfaces/package.xml b/src/g2_2025_interfaces/package.xml index e596ef9..42e2940 100644 --- a/src/g2_2025_interfaces/package.xml +++ b/src/g2_2025_interfaces/package.xml @@ -10,7 +10,9 @@ ament_cmake rosidl_default_generators + builtin_interfaces rosidl_default_runtime + builtin_interfaces rosidl_interface_packages ament_lint_auto diff --git a/src/g2_2025_interfaces/srv/Exam.srv b/src/g2_2025_interfaces/srv/Exam.srv deleted file mode 100644 index 8e9945c..0000000 --- a/src/g2_2025_interfaces/srv/Exam.srv +++ /dev/null @@ -1,5 +0,0 @@ -# Request -string student_name -string lecture_name ---- -float32 result diff --git a/src/g2_2025_interfaces/srv/Exams.srv b/src/g2_2025_interfaces/srv/Exams.srv new file mode 100644 index 0000000..d5f62f3 --- /dev/null +++ b/src/g2_2025_interfaces/srv/Exams.srv @@ -0,0 +1,7 @@ +# Request +string student_name +string lecture_name +int32[] exam_grades +--- +# Response +int32 result # Final result -- 2.39.5 From 9c6b194b3a88f2cf12a80ddb3c2e253ff773ad6b Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 25 Sep 2025 11:51:46 +0200 Subject: [PATCH 08/12] feat(database): Missing queries, rename `lecture` -> `course` --- .../src/config/ConfigManager.hpp | 1 + .../src/database/DatabaseManager.cpp | 37 +++++++++++++--- .../src/database/DatabaseManager.hpp | 3 +- .../src/database/SQLQueries.hpp | 43 +++++++++++-------- .../src/database/StudentCourse.hpp | 4 +- .../nodes/ExamResultGenerator.cpp | 12 +++--- src/g2_2025_interfaces/action/Retake.action | 2 +- src/g2_2025_interfaces/msg/Exam.msg | 2 +- src/g2_2025_interfaces/msg/Student.msg | 2 +- src/g2_2025_interfaces/srv/Exams.srv | 2 +- 10 files changed, 73 insertions(+), 35 deletions(-) diff --git a/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.hpp b/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.hpp index 4bf8214..cd585b9 100644 --- a/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.hpp @@ -37,6 +37,7 @@ private: bool loaded_ { false }; std::vector default_config_paths_ = { "config.toml", + "./src/config.toml", "../config.toml", "../../config.toml", "../../../config.toml", diff --git a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp index 13eee4f..706e24c 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp @@ -75,7 +75,7 @@ std::vector DatabaseManager::queue_pending_combinations() { for (const auto& row : result) { StudentCourse sc; sc.student_name = row[0].as(); - sc.lecture_name = row[1].as(); + sc.course_name = row[1].as(); combinations.push_back(sc); } @@ -96,7 +96,7 @@ bool DatabaseManager::enroll_student_into_course(const StudentCourse& sc) { try { pqxx::work txn(*conn_); - txn.exec_params(SQL_INSERT_STUDENT_ENROLLMENT, sc.student_name, sc.lecture_name); + txn.exec_params(SQL_INSERT_STUDENT_ENROLLMENT, sc.student_name, sc.course_name); txn.commit(); return true; @@ -110,7 +110,7 @@ bool DatabaseManager::enroll_student_into_course(const StudentCourse& sc) { } } -bool DatabaseManager::store_exam_result(const std::string& student_name, const std::string& lecture_name, int grade) { +bool DatabaseManager::store_exam_result(const std::string& student_name, const std::string& course_name, int grade) { if (!is_connected()) return false; try { @@ -118,7 +118,7 @@ bool DatabaseManager::store_exam_result(const std::string& student_name, const s txn.exec_params( SQL_INSERT_EXAM_RESULT, - student_name, lecture_name, grade + student_name, course_name, grade ); txn.commit(); @@ -133,6 +133,33 @@ bool DatabaseManager::store_exam_result(const std::string& student_name, const s } } +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; @@ -141,7 +168,7 @@ void DatabaseManager::create_tables() { txn.exec(SQL_CREATE_ENROLLMENTS_TABLE); txn.exec(SQL_CREATE_EXAM_RESULTS); - txn.exec(SQL_CREATE_LECTURE_RESULTS); + txn.exec(SQL_CREATE_COURSE_RESULTS); txn.commit(); diff --git a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp index 3bb1869..ba2dc40 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp @@ -33,8 +33,9 @@ public: // Data operations std::vector queue_pending_combinations(); - bool store_exam_result(const std::string& student_name, const std::string& lecture_name, int grade); + 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); private: rclcpp::Logger logger_; diff --git a/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp b/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp index 4e8af88..1848728 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp @@ -11,53 +11,55 @@ 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, - lecture_name VARCHAR(100) NOT NULL, - enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(student_name, lecture_name) + 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, - lecture_name VARCHAR(100) NOT NULL, - exam_grade FLOAT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(student_name, lecture_name) + 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_LECTURE_RESULTS = R"( +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, - lecture_name VARCHAR(100) NOT NULL, - final_grade FLOAT 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, lecture_name) + UNIQUE(student_name, course_name) ); )"; static const std::string SQL_SELECT_MISSING_RESULTS = R"( - SELECT se.student_name, se.lecture_name + 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.lecture_name = fcr.lecture_name + AND se.course_name = fcr.course_name WHERE fcr.id IS NULL )"; static const std::string SQL_INSERT_EXAM_RESULT = R"( - INSERT INTO exam_results (student_name, lecture_name, exam_grade) + INSERT INTO exam_results (student_name, course_name, exam_grade) VALUES ($1, $2, $3) )"; -// ON CONFLICT (student_name, lecture_name) +// 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, lecture_name) + INSERT INTO student_enrollments (student_name, course_name) VALUES ($1, $2) ON CONFLICT DO NOTHING )"; @@ -65,3 +67,10 @@ static const std::string SQL_INSERT_STUDENT_ENROLLMENT = R"( 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 diff --git a/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp b/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp index a1fd46b..009dd58 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp @@ -13,10 +13,10 @@ namespace assignments::one::exam_result_generator { struct StudentCourse { std::string student_name; - std::string lecture_name; + std::string course_name; bool operator==(const StudentCourse& other) const { - return student_name == other.student_name && lecture_name == other.lecture_name; + return student_name == other.student_name && course_name == other.course_name; } }; diff --git a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp index 2962602..3ba346f 100644 --- a/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp +++ b/src/g2_2025_grade_calculator_pkg/src/exam_result_generator/nodes/ExamResultGenerator.cpp @@ -61,37 +61,37 @@ void ExamResultGenerator::generate_random_result() { int grade = grade_dist_(gen_); - db_manager_->store_exam_result(selected.student_name, selected.lecture_name, grade); + 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.lecture_name = selected.lecture_name; + 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.lecture_name.c_str() + 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.lecture_name = msg->lecture_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.lecture_name.c_str() + 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.lecture_name.c_str() + sc.student_name.c_str(), sc.course_name.c_str() ); } } diff --git a/src/g2_2025_interfaces/action/Retake.action b/src/g2_2025_interfaces/action/Retake.action index 66ebe9a..bc8d36b 100644 --- a/src/g2_2025_interfaces/action/Retake.action +++ b/src/g2_2025_interfaces/action/Retake.action @@ -1,5 +1,5 @@ string student_name -string lecture_name +string course_name --- --- float32 result diff --git a/src/g2_2025_interfaces/msg/Exam.msg b/src/g2_2025_interfaces/msg/Exam.msg index 8d1beb9..a37853e 100644 --- a/src/g2_2025_interfaces/msg/Exam.msg +++ b/src/g2_2025_interfaces/msg/Exam.msg @@ -1,3 +1,3 @@ -string lecture_name +string course_name int32 result builtin_interfaces/Time timestamp diff --git a/src/g2_2025_interfaces/msg/Student.msg b/src/g2_2025_interfaces/msg/Student.msg index fa576b2..ddbc0b0 100644 --- a/src/g2_2025_interfaces/msg/Student.msg +++ b/src/g2_2025_interfaces/msg/Student.msg @@ -1,3 +1,3 @@ string student_name -string lecture_name +string course_name builtin_interfaces/Time timestamp diff --git a/src/g2_2025_interfaces/srv/Exams.srv b/src/g2_2025_interfaces/srv/Exams.srv index d5f62f3..87dc142 100644 --- a/src/g2_2025_interfaces/srv/Exams.srv +++ b/src/g2_2025_interfaces/srv/Exams.srv @@ -1,6 +1,6 @@ # Request string student_name -string lecture_name +string course_name int32[] exam_grades --- # Response -- 2.39.5 From 743611a889a8b4df306df8aa61f6392aea9b9aee Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 25 Sep 2025 11:58:23 +0200 Subject: [PATCH 09/12] feat: Add docker-compose --- docker-compose.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0e0cbbd --- /dev/null +++ b/docker-compose.yml @@ -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" -- 2.39.5 From 2089ea7c87ec6a88af5e40b25219ca5c4aad551b Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 25 Sep 2025 11:58:34 +0200 Subject: [PATCH 10/12] feat(database): Add query to fetch all grades --- .../src/database/DatabaseManager.cpp | 28 +++++++++++++++++++ .../src/database/DatabaseManager.hpp | 3 ++ .../src/database/SQLQueries.hpp | 6 ++++ 3 files changed, 37 insertions(+) diff --git a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp index 706e24c..fa0d313 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp @@ -207,4 +207,32 @@ void DatabaseManager::insert_sample_data() { } } +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(); + if (exam_count == 0) { + return -1; // No exams taken + } + int avg_grade = static_cast(result[0][1].as() + 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::exam_result_generator diff --git a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp index ba2dc40..d961337 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp @@ -36,6 +36,9 @@ public: 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_; diff --git a/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp b/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp index 1848728..f1549c4 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/SQLQueries.hpp @@ -51,6 +51,12 @@ static const std::string SQL_SELECT_MISSING_RESULTS = R"( 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) -- 2.39.5 From 2f29d3539c7ec3b5aafb251d9807fca3868f08d1 Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 25 Sep 2025 13:04:49 +0200 Subject: [PATCH 11/12] docs: Add initial documentation --- doc/ConfigManager.md | 134 +++++++++++++++++++++++++++++++ doc/DatabaseManager.md | 125 ++++++++++++++++++++++++++++ doc/interfaces/interfaces.md | 16 ++++ doc/nodes/ExamResultGenerator.md | 38 +++++++++ 4 files changed, 313 insertions(+) create mode 100644 doc/ConfigManager.md create mode 100644 doc/DatabaseManager.md create mode 100644 doc/interfaces/interfaces.md create mode 100644 doc/nodes/ExamResultGenerator.md diff --git a/doc/ConfigManager.md b/doc/ConfigManager.md new file mode 100644 index 0000000..b47924a --- /dev/null +++ b/doc/ConfigManager.md @@ -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 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 get_database_config() const`** +> Returns `std::optional` 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 + +``` diff --git a/doc/DatabaseManager.md b/doc/DatabaseManager.md new file mode 100644 index 0000000..a4bc1d1 --- /dev/null +++ b/doc/DatabaseManager.md @@ -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 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"); + } +} +``` diff --git a/doc/interfaces/interfaces.md b/doc/interfaces/interfaces.md new file mode 100644 index 0000000..823e3bc --- /dev/null +++ b/doc/interfaces/interfaces.md @@ -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 diff --git a/doc/nodes/ExamResultGenerator.md b/doc/nodes/ExamResultGenerator.md new file mode 100644 index 0000000..6b75456 --- /dev/null +++ b/doc/nodes/ExamResultGenerator.md @@ -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 -- 2.39.5 From 81d999fea4a8bf6a15347fe35ed900500be895b0 Mon Sep 17 00:00:00 2001 From: Wessel Tip Date: Thu, 25 Sep 2025 13:05:40 +0200 Subject: [PATCH 12/12] fix(database,config): Use correct namespace --- src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.cpp | 4 ++-- src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.hpp | 4 ++-- .../src/config/DatabaseConfig.hpp | 4 ++-- .../src/database/DatabaseManager.cpp | 4 ++-- .../src/database/DatabaseManager.hpp | 4 ++-- .../src/database/StudentCourse.hpp | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.cpp b/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.cpp index a212081..e576f53 100644 --- a/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.cpp +++ b/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.cpp @@ -3,7 +3,7 @@ #include #include -namespace assignments::one::exam_result_generator { +namespace assignments::one { ConfigManager::ConfigManager(rclcpp::Logger logger) : logger_(logger), loaded_(false) @@ -109,4 +109,4 @@ DatabaseConfig ConfigManager::parse_database_config(const toml::table& config) c return db_config; } -} // namespace assignments::one::exam_result_generator +} // namespace assignments::one diff --git a/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.hpp b/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.hpp index cd585b9..25fd3c7 100644 --- a/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/config/ConfigManager.hpp @@ -16,7 +16,7 @@ #include "DatabaseConfig.hpp" -namespace assignments::one::exam_result_generator { +namespace assignments::one { class ConfigManager { public: @@ -49,4 +49,4 @@ private: DatabaseConfig parse_database_config(const toml::table& config) const; }; -} // namespace assignments::one::exam_result_generator +} // namespace assignments::one diff --git a/src/g2_2025_grade_calculator_pkg/src/config/DatabaseConfig.hpp b/src/g2_2025_grade_calculator_pkg/src/config/DatabaseConfig.hpp index 9aec91a..e9a75f8 100644 --- a/src/g2_2025_grade_calculator_pkg/src/config/DatabaseConfig.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/config/DatabaseConfig.hpp @@ -9,7 +9,7 @@ #include -namespace assignments::one::exam_result_generator { +namespace assignments::one { struct DatabaseConfig { std::string host; @@ -43,4 +43,4 @@ struct DatabaseConfig { } }; -} +} // namespace assignments::one diff --git a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp index fa0d313..1e2c15c 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.cpp @@ -6,7 +6,7 @@ #include "SQLQueries.hpp" #include "config/ConfigManager.hpp" -namespace assignments::one::exam_result_generator { +namespace assignments::one { DatabaseManager::DatabaseManager(rclcpp::Logger logger) : logger_(logger) { config_manager_ = std::make_unique(logger_); @@ -235,4 +235,4 @@ int DatabaseManager::get_final_course_grade(const StudentCourse& sc) { } } -} // namespace assignments::one::exam_result_generator +} // namespace assignments::one diff --git a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp index d961337..1d3b2d2 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/DatabaseManager.hpp @@ -16,7 +16,7 @@ #include "StudentCourse.hpp" #include "config/ConfigManager.hpp" -namespace assignments::one::exam_result_generator { +namespace assignments::one { class DatabaseManager { public: @@ -53,4 +53,4 @@ private: }; }; -} // namespace assignments::one::exam_result_generator +} // namespace assignments::one diff --git a/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp b/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp index 009dd58..2e7f2e6 100644 --- a/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp +++ b/src/g2_2025_grade_calculator_pkg/src/database/StudentCourse.hpp @@ -9,7 +9,7 @@ #include -namespace assignments::one::exam_result_generator { +namespace assignments::one { struct StudentCourse { std::string student_name; @@ -20,4 +20,4 @@ struct StudentCourse { } }; -} +} // namespace assignments::one -- 2.39.5