diff --git a/rmcs_ws/src/rmcs_core/src/broadcaster/tf_broadcaster.cpp b/rmcs_ws/src/rmcs_core/src/broadcaster/tf_broadcaster.cpp index 40dfe60a6..f3d13c700 100644 --- a/rmcs_ws/src/rmcs_core/src/broadcaster/tf_broadcaster.cpp +++ b/rmcs_ws/src/rmcs_core/src/broadcaster/tf_broadcaster.cpp @@ -26,6 +26,8 @@ class TfBroadcaster } ~TfBroadcaster() = default; + void before_updating() override { fast_tf::rcl::broadcast_all(*tf_); } + void update() override { using namespace std::chrono_literals; if (*update_count_ == 0) diff --git a/rmcs_ws/src/rmcs_core/src/broadcaster/value_broadcaster.cpp b/rmcs_ws/src/rmcs_core/src/broadcaster/value_broadcaster.cpp index b980136da..ec612065f 100644 --- a/rmcs_ws/src/rmcs_core/src/broadcaster/value_broadcaster.cpp +++ b/rmcs_ws/src/rmcs_core/src/broadcaster/value_broadcaster.cpp @@ -19,9 +19,11 @@ class ValueBroadcaster [this](const rclcpp::Parameter& para) { update_forward_list(para.as_string_array()); }); } - void before_pairing(const std::map& output_map) override { - for (const auto& [name, type] : output_map) { - if (type == typeid(double)) { + void before_pairing(const OutputInfoMap& output_map) override { + for (const auto& [name, output] : output_map) { + if (output.kind != rmcs_executor::InterfaceKind::Normal) + continue; + if (output.type.get() == typeid(double)) { forward_units_.emplace( name, std::make_unique>(this, name)); @@ -117,4 +119,4 @@ class ValueBroadcaster #include -PLUGINLIB_EXPORT_CLASS(rmcs_core::broadcaster::ValueBroadcaster, rmcs_executor::Component) \ No newline at end of file +PLUGINLIB_EXPORT_CLASS(rmcs_core::broadcaster::ValueBroadcaster, rmcs_executor::Component) diff --git a/rmcs_ws/src/rmcs_core/src/filter/imu_ekf.hpp b/rmcs_ws/src/rmcs_core/src/filter/imu_ekf.hpp new file mode 100644 index 000000000..e7e98cf04 --- /dev/null +++ b/rmcs_ws/src/rmcs_core/src/filter/imu_ekf.hpp @@ -0,0 +1,428 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace rmcs_core::filter { + +class ImuEkf { +public: + using Vec3 = Eigen::Vector3d; + using Vec4 = Eigen::Vector4d; + using Mat3 = Eigen::Matrix3d; + using Mat4 = Eigen::Matrix4d; + using Mat43 = Eigen::Matrix; + using Mat34 = Eigen::Matrix; + + struct Config { + Mat3 process_noise = 1.22e-3 * Mat3::Identity(); + Mat3 measurement_noise = 50.0 * Mat3::Identity(); + Mat3 gate_noise = 0.02 * Mat3::Identity(); + + Mat4 initial_covariance = 0.15 * Mat4::Identity(); + }; + + class AccelCorrection { + public: + [[nodiscard]] double chi_square() const noexcept { return chi_square_; } + + private: + friend class ImuEkf; + + AccelCorrection( + const std::uint64_t revision, Vec3 innovation, Mat34 jacobian_h_x, + Eigen::LDLT innovation_covariance_ldlt, const double chi_square) + : revision_(revision) + , innovation_(std::move(innovation)) + , jacobian_h_x_(std::move(jacobian_h_x)) + , innovation_covariance_ldlt_(std::move(innovation_covariance_ldlt)) + , chi_square_(chi_square) {} + + std::uint64_t revision_ = 0; + Vec3 innovation_ = Vec3::Zero(); + Mat34 jacobian_h_x_ = Mat34::Zero(); + Eigen::LDLT innovation_covariance_ldlt_; + double chi_square_ = std::numeric_limits::quiet_NaN(); + }; + + ImuEkf() + : ImuEkf(Config{}) {} + + explicit ImuEkf(Config config) + : config_(std::move(config)) { + reset(); + } + + [[nodiscard]] const Config& config() const noexcept { return config_; } + + void set_config(const Config& config) { + config_ = config; + reset(); + } + + void reset() { + state_ = identity_quaternion(); + covariance_ = projected_covariance(state_, config_.initial_covariance); + ++revision_; + } + + [[nodiscard]] bool reset_from_accel(const Vec3& accel_g, const double initial_yaw = 0.0) { + if (!accel_g.allFinite() || !std::isfinite(initial_yaw)) { + return false; + } + + const std::optional normalized_accel = normalize_vector(accel_g); + if (!normalized_accel.has_value()) { + return false; + } + + const double gx = (*normalized_accel)(0); + const double gy = (*normalized_accel)(1); + const double gz = std::clamp((*normalized_accel)(2), -1.0, 1.0); + + double q0_val, q1_val, q2_val, q3_val; + if (gz >= 0.0) { + const double s = std::sqrt(0.5 * (1.0 + gz)); + const double inv = 0.5 / s; + q0_val = s; + q1_val = gy * inv; + q2_val = -gx * inv; + q3_val = 0.0; + } else { + const double s = std::sqrt(0.5 * (1.0 - gz)); + const double inv = 0.5 / s; + q0_val = gy * inv; + q1_val = s; + q2_val = 0.0; + q3_val = gx * inv; + } + + const double cy = std::cos(initial_yaw * 0.5); + const double sy = std::sin(initial_yaw * 0.5); + + state_(0) = cy * q0_val - sy * q3_val; + state_(1) = cy * q1_val - sy * q2_val; + state_(2) = cy * q2_val + sy * q1_val; + state_(3) = cy * q3_val + sy * q0_val; + + state_ = normalize_quaternion(state_); + covariance_ = projected_covariance(state_, config_.initial_covariance); + ++revision_; + return true; + } + + void inflate_attitude_uncertainty_to_initial() { + covariance_ = covariance_with_psd_floor( + covariance_, projected_covariance(state_, config_.initial_covariance)); + ++revision_; + } + + [[nodiscard]] bool predict(const Vec3& gyro_rad_per_sec, const double dt_seconds) { + if (!gyro_rad_per_sec.allFinite()) { + return false; + } + + if (!std::isfinite(dt_seconds) || dt_seconds < 0.0) { + return false; + } + + const PropagationResult propagation = + propagate_quaternion(state_, gyro_rad_per_sec, dt_seconds); + if (!propagation.quaternion.allFinite() || !propagation.orthogonalization.allFinite()) { + return false; + } + const Vec4 prior_state = propagation.quaternion; + const Mat4& orthogonalization = propagation.orthogonalization; + + const Mat4 jacobian_f_x = state_jacobian(gyro_rad_per_sec, dt_seconds, orthogonalization); + const Mat43 jacobian_f_w = process_noise_jacobian(state_, orthogonalization); + Mat4 prior_cov = symmetrized_mat4( + jacobian_f_x * covariance_ * jacobian_f_x.transpose() + + dt_seconds * jacobian_f_w * config_.process_noise * jacobian_f_w.transpose()); + if (!is_positive_semidefinite(prior_cov)) { + return false; + } + + state_ = prior_state; + covariance_ = prior_cov; + ++revision_; + return true; + } + + [[nodiscard]] std::optional prepare_correction(const Vec3& accel_g) const { + if (!accel_g.allFinite()) + return std::nullopt; + + const std::optional normalized_accel = normalize_vector(accel_g); + if (!normalized_accel.has_value()) + return std::nullopt; + + const Vec3 predicted_accel = measurement_model(state_); + const Vec3 gate_innovation = accel_g - predicted_accel; + const Vec3 update_innovation = *normalized_accel - predicted_accel; + const Mat34 jacobian_h_x = measurement_jacobian(state_); + const Mat3 projected_covariance = jacobian_h_x * covariance_ * jacobian_h_x.transpose(); + const Mat3 gate_covariance = symmetrized_mat3(projected_covariance + config_.gate_noise); + + if (!is_positive_definite(gate_covariance)) { + return std::nullopt; + } + + const auto gate_ldt = gate_covariance.ldlt(); + if (gate_ldt.info() != Eigen::Success || !gate_ldt.isPositive()) { + return std::nullopt; + } + const Vec3 innovation_weighted = gate_ldt.solve(gate_innovation); + if (!innovation_weighted.allFinite()) { + return std::nullopt; + } + const double chi_square = gate_innovation.dot(innovation_weighted); + if (!std::isfinite(chi_square)) { + return std::nullopt; + } + + const Mat3 update_covariance = + symmetrized_mat3(projected_covariance + config_.measurement_noise); + if (!is_positive_definite(update_covariance)) { + return std::nullopt; + } + + const auto update_ldt = update_covariance.ldlt(); + if (update_ldt.info() != Eigen::Success || !update_ldt.isPositive()) { + return std::nullopt; + } + + return AccelCorrection(revision_, update_innovation, jacobian_h_x, update_ldt, chi_square); + } + + bool correct(const AccelCorrection& update) { + if (update.revision_ != revision_) { + return false; + } + const Vec4 state_prior = state_; + const Mat34 k_transpose = + update.innovation_covariance_ldlt_.solve(update.jacobian_h_x_ * covariance_); + if (!k_transpose.allFinite()) { + return false; + } + const Eigen::Matrix kalman_gain = k_transpose.transpose(); + Mat4 state_update_mask = Mat4::Identity(); + state_update_mask(3, 3) = 0.0; + const Eigen::Matrix effective_gain = state_update_mask * kalman_gain; + + const Vec4 quaternion_raw = state_prior + effective_gain * update.innovation_; + if (!quaternion_raw.allFinite() || quaternion_raw.squaredNorm() <= kEpsilon) { + return false; + } + + const Mat4 matrix_tmp = Mat4::Identity() - effective_gain * update.jacobian_h_x_; + Mat4 corrected_covariance = + matrix_tmp * covariance_ * matrix_tmp.transpose() + + effective_gain * config_.measurement_noise * effective_gain.transpose(); + + const Mat4 j_norm = orthogonalization_jacobian(quaternion_raw); + corrected_covariance = symmetrized_mat4(j_norm * corrected_covariance * j_norm.transpose()); + + if (!is_positive_semidefinite(corrected_covariance)) { + return false; + } + state_ = normalize_quaternion(quaternion_raw); + covariance_ = corrected_covariance; + ++revision_; + return true; + } + + bool correct(const Vec3& accel_mps2) { + const std::optional update = prepare_correction(accel_mps2); + return update.has_value() && correct(*update); + } + + [[nodiscard]] Eigen::Quaterniond quaternion() const noexcept { + return {state_[0], state_[1], state_[2], state_[3]}; + } + +private: + struct PropagationResult { + Vec4 quaternion = identity_quaternion(); + Mat4 orthogonalization = Mat4::Identity(); + }; + + [[nodiscard]] static Vec4 identity_quaternion() { + Vec4 q; + q << 1.0, 0.0, 0.0, 0.0; + return q; + } + + [[nodiscard]] static std::optional normalize_vector(const Vec3& vector) { + const double squared_norm = vector.squaredNorm(); + if (!std::isfinite(squared_norm) || squared_norm <= kEpsilon) { + return std::nullopt; + } + + return vector / std::sqrt(squared_norm); + } + + [[nodiscard]] static Vec4 normalize_quaternion(const Vec4& quaternion) { + const double squared_norm = quaternion.squaredNorm(); + if (!std::isfinite(squared_norm) || squared_norm <= kEpsilon) { + return identity_quaternion(); + } + + return quaternion / std::sqrt(squared_norm); + } + + [[nodiscard]] static Mat3 symmetrized_mat3(const Mat3& matrix) { + return 0.5 * (matrix + matrix.transpose()); + } + + [[nodiscard]] static Mat4 symmetrized_mat4(const Mat4& matrix) { + return 0.5 * (matrix + matrix.transpose()); + } + + [[nodiscard]] static bool is_positive_definite(const Mat3& matrix) { + if (!matrix.allFinite()) { + return false; + } + + const Eigen::SelfAdjointEigenSolver solver(symmetrized_mat3(matrix)); + if (solver.info() != Eigen::Success || !solver.eigenvalues().allFinite()) { + return false; + } + + const double max_abs_eigenvalue = solver.eigenvalues().cwiseAbs().maxCoeff(); + const double tolerance = kEpsilon * std::max(1.0, max_abs_eigenvalue); + return solver.eigenvalues().minCoeff() > tolerance; + } + + [[nodiscard]] static bool is_positive_semidefinite(const Mat4& matrix) { + if (!matrix.allFinite()) { + return false; + } + + const Eigen::SelfAdjointEigenSolver solver(symmetrized_mat4(matrix)); + if (solver.info() != Eigen::Success || !solver.eigenvalues().allFinite()) { + return false; + } + + const double max_abs_eigenvalue = solver.eigenvalues().cwiseAbs().maxCoeff(); + const double tolerance = kEpsilon * std::max(1.0, max_abs_eigenvalue); + return solver.eigenvalues().minCoeff() >= -tolerance; + } + + [[nodiscard]] static Mat4 positive_semidefinite_part(const Mat4& matrix) { + const Eigen::SelfAdjointEigenSolver solver(symmetrized_mat4(matrix)); + if (solver.info() != Eigen::Success || !solver.eigenvalues().allFinite()) { + return Mat4::Zero(); + } + + return symmetrized_mat4( + solver.eigenvectors() * solver.eigenvalues().cwiseMax(0.0).asDiagonal() + * solver.eigenvectors().transpose()); + } + + [[nodiscard]] static Mat4 + covariance_with_psd_floor(const Mat4& covariance, const Mat4& covariance_floor) { + Mat4 floor = symmetrized_mat4(covariance_floor); + Mat4 result = floor + positive_semidefinite_part(symmetrized_mat4(covariance) - floor); + result = symmetrized_mat4(result); + if (is_positive_semidefinite(result)) { + return result; + } + + return floor; + } + + [[nodiscard]] static Mat4 + projected_covariance(const Vec4& quaternion, const Mat4& raw_covariance) { + const Mat4 j_norm = orthogonalization_jacobian(quaternion); + Mat4 covariance = symmetrized_mat4(j_norm * raw_covariance * j_norm.transpose()); + if (is_positive_semidefinite(covariance)) { + return covariance; + } + + return symmetrized_mat4(j_norm * Mat4::Identity() * j_norm.transpose()); + } + + [[nodiscard]] static Mat4 omega_matrix(const Vec3& gyro_rad_per_sec) { + const double gx = gyro_rad_per_sec(0); + const double gy = gyro_rad_per_sec(1); + const double gz = gyro_rad_per_sec(2); + Mat4 matrix; + // clang-format off + matrix << 0, -gx, -gy, -gz, + gx, 0, gz, -gy, + gy, -gz, 0, gx, + gz, gy, -gx, 0; + // clang-format on + return matrix; + } + + [[nodiscard]] static Mat4 orthogonalization_jacobian(const Vec4& quaternion) { + const double norm = quaternion.norm(); + const double inv_norm = (std::isfinite(norm) && norm > kEpsilon) ? 1.0 / norm : 1.0; + return inv_norm + * (Mat4::Identity() - inv_norm * inv_norm * quaternion * quaternion.transpose()); + } + + [[nodiscard]] static PropagationResult + propagate_quaternion(const Vec4& state, const Vec3& gyro_rad_per_sec, const double dt) { + const Mat4 omega = omega_matrix(gyro_rad_per_sec); + const Vec4 quaternion_tmp = state + 0.5 * dt * omega * state; + + PropagationResult result; + result.orthogonalization = orthogonalization_jacobian(quaternion_tmp); + result.quaternion = normalize_quaternion(quaternion_tmp); + return result; + } + + [[nodiscard]] static Mat4 state_jacobian( + const Vec3& gyro_rad_per_sec, const double dt, const Mat4& orthogonalization) { + return orthogonalization * (Mat4::Identity() + 0.5 * dt * omega_matrix(gyro_rad_per_sec)); + } + + [[nodiscard]] static Mat43 + process_noise_jacobian(const Vec4& state, const Mat4& orthogonalization) { + Mat43 matrix_q; + // clang-format off + matrix_q << -state(1), -state(2), -state(3), + state(0), -state(3), state(2), + state(3), state(0), -state(1), + -state(2), state(1), state(0); + // clang-format on + return orthogonalization * (0.5 * matrix_q); + } + + [[nodiscard]] static Vec3 measurement_model(const Vec4& state) { + Vec3 result; + result(0) = 2.0 * (state(1) * state(3) - state(0) * state(2)); + result(1) = 2.0 * (state(2) * state(3) + state(0) * state(1)); + result(2) = + state(0) * state(0) - state(1) * state(1) - state(2) * state(2) + state(3) * state(3); + return result; + } + + [[nodiscard]] static Mat34 measurement_jacobian(const Vec4& state) { + Mat34 result; + // clang-format off + result << -2.0 * state(2), 2.0 * state(3), -2.0 * state(0), 2.0 * state(1), + 2.0 * state(1), 2.0 * state(0), 2.0 * state(3), 2.0 * state(2), + 2.0 * state(0), -2.0 * state(1), -2.0 * state(2), 2.0 * state(3); + // clang-format on + return result; + } + + static constexpr double kEpsilon = 1e-12; + + Config config_; + Vec4 state_ = identity_quaternion(); + Mat4 covariance_ = Mat4::Identity(); + std::uint64_t revision_ = 0; +}; + +} // namespace rmcs_core::filter diff --git a/rmcs_ws/src/rmcs_core/src/hardware/device/bmi088_ekf.hpp b/rmcs_ws/src/rmcs_core/src/hardware/device/bmi088_ekf.hpp new file mode 100644 index 000000000..83c1b2e32 --- /dev/null +++ b/rmcs_ws/src/rmcs_core/src/hardware/device/bmi088_ekf.hpp @@ -0,0 +1,172 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "filter/imu_ekf.hpp" + +namespace rmcs_core::hardware::device { + +class Bmi088Ekf { +public: + struct Config { + Eigen::Matrix3d body_to_sensor = Eigen::Matrix3d::Identity(); + }; + + using TimePoint = rmcs_msgs::BoardClock::time_point; + using Snapshot = rmcs_msgs::ImuSnapshot; + + explicit Bmi088Ekf(Config config) + : config_(std::move(config)) {} + + Bmi088Ekf() + : Bmi088Ekf(Config{}) {} + + void push_accelerometer_sample( + std::int16_t x, std::int16_t y, std::int16_t z, TimePoint sample_time) { + const Eigen::Vector3d accel_g = + config_.body_to_sensor.transpose() * convert_accelerometer(x, y, z); + + if (!initialized_) { + if (ekf_.reset_from_accel(accel_g)) { + ekf_state_time_ = sample_time; + latest_snapshot_ = { + ekf_.quaternion(), + Eigen::Vector3d::Zero(), + ekf_state_time_, + }; + const auto guard = std::scoped_lock{mutex_}; + initialized_ = true; + } + return; + } + + if (sample_time < ekf_state_time_) + return; + + if (pending_accel_sample_ && sample_time < pending_accel_sample_->sample_time) + return; + + pending_accel_sample_ = {accel_g, sample_time}; + } + + std::optional try_update_with_gyroscope_sample( + std::int16_t x, std::int16_t y, std::int16_t z, TimePoint sample_time) { + const Eigen::Vector3d gyro_rad_per_sec = + config_.body_to_sensor.transpose() * convert_gyroscope(x, y, z); + + if (!initialized_) + return std::nullopt; + + if (sample_time < ekf_state_time_) + return std::nullopt; + + if (is_gyro_saturated(gyro_rad_per_sec)) + ekf_.inflate_attitude_uncertainty_to_initial(); + + while (pending_accel_sample_) { + const auto& accel_sample_time = pending_accel_sample_->sample_time; + if (accel_sample_time < ekf_state_time_ || accel_sample_time > sample_time) + break; + + if (!ekf_.predict( + gyro_rad_per_sec, + std::chrono::duration{accel_sample_time - ekf_state_time_}.count())) + break; + ekf_state_time_ = accel_sample_time; + + const auto correction = ekf_.prepare_correction(pending_accel_sample_->accel_g); + if (!correction || correction->chi_square() >= 3.0) + break; + if (!ekf_.correct(*correction)) + break; + + pending_accel_sample_ = std::nullopt; + break; + } + + // Guard against stale IMU frames that librmcs may deliver right after reconnect before the + // device-side buffer is drained. Integrating a frame with a large timestamp jump can inject + // a huge bogus gyro delta, so drop it instead of advancing the filter. + // TODO: Remove this once librmcs guarantees buffered historical IMU frames are flushed on + // connection. + if (std::chrono::duration{sample_time - ekf_state_time_}.count() > 1 / 1000.0) { + ekf_state_time_ = sample_time; + return std::nullopt; + } + + if (!ekf_.predict( + gyro_rad_per_sec, + std::chrono::duration{sample_time - ekf_state_time_}.count())) + return std::nullopt; + ekf_state_time_ = sample_time; + + auto snapshot = Snapshot{ + ekf_.quaternion(), + gyro_rad_per_sec, + ekf_state_time_, + }; + { + const auto guard = std::scoped_lock{mutex_}; + latest_snapshot_ = snapshot; + } + return snapshot; + } + + [[nodiscard]] bool initialized() const noexcept { + const auto guard = std::scoped_lock{mutex_}; + return initialized_; + } + + [[nodiscard]] std::optional snapshot() const noexcept { + const auto guard = std::scoped_lock{mutex_}; + if (!initialized_) + return std::nullopt; + return latest_snapshot_; + } + +private: + [[nodiscard]] static Eigen::Vector3d + convert_accelerometer(std::int16_t x, std::int16_t y, std::int16_t z) noexcept { + return Eigen::Vector3d{ + static_cast(x), static_cast(y), static_cast(z)} + / 32767.0 * 6.0; + } + + [[nodiscard]] static Eigen::Vector3d + convert_gyroscope(std::int16_t x, std::int16_t y, std::int16_t z) noexcept { + return Eigen::Vector3d{ + static_cast(x), static_cast(y), static_cast(z)} + / 32767.0 * 2000.0 / 180.0 * std::numbers::pi; + } + + [[nodiscard]] static bool is_gyro_saturated(const Eigen::Vector3d& gyro_rad_per_sec) noexcept { + return gyro_rad_per_sec.cwiseAbs().maxCoeff() >= 0.98 * 2000.0 / 180.0 * std::numbers::pi; + } + + const Config config_; + + mutable std::mutex mutex_; + bool initialized_ = false; + + filter::ImuEkf ekf_; + TimePoint ekf_state_time_; + + struct AccelSample { + Eigen::Vector3d accel_g; + TimePoint sample_time; + }; + std::optional pending_accel_sample_; + + Snapshot latest_snapshot_; +}; + +} // namespace rmcs_core::hardware::device diff --git a/rmcs_ws/src/rmcs_core/src/hardware/device/board_clock_lifter.hpp b/rmcs_ws/src/rmcs_core/src/hardware/device/board_clock_lifter.hpp new file mode 100644 index 000000000..1706ea2a9 --- /dev/null +++ b/rmcs_ws/src/rmcs_core/src/hardware/device/board_clock_lifter.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +#include + +namespace rmcs_core::hardware::device { + +class BoardClockLifter { +public: + using time_point = rmcs_msgs::BoardClock::time_point; + + time_point advance_timebase(std::uint32_t raw_timestamp_quarter_us) { + if (!has_latest_timebase_) { + has_latest_timebase_ = true; + last_timebase_raw_ = raw_timestamp_quarter_us; + latest_timebase_timestamp_ = raw_timestamp_quarter_us; + } + + latest_timebase_timestamp_ += + static_cast(raw_timestamp_quarter_us - last_timebase_raw_); + last_timebase_raw_ = raw_timestamp_quarter_us; + + return time_point{rmcs_msgs::BoardClock::duration{latest_timebase_timestamp_}}; + } + + [[nodiscard]] auto timebase() const -> std::optional { + if (!has_latest_timebase_) + return std::nullopt; + return time_point{rmcs_msgs::BoardClock::duration{latest_timebase_timestamp_}}; + } + + [[nodiscard]] auto lift_timestamp(std::uint32_t timestamp_quarter_us) const + -> std::optional { + if (!has_latest_timebase_) + return std::nullopt; + + const auto latest_timestamp_low32 = static_cast(latest_timebase_timestamp_); + const auto signed_offset = + static_cast(timestamp_quarter_us - latest_timestamp_low32); + const auto lifted_timestamp = + latest_timebase_timestamp_ + static_cast(signed_offset); + return time_point{rmcs_msgs::BoardClock::duration{lifted_timestamp}}; + } + +private: + bool has_latest_timebase_ = false; + std::uint32_t last_timebase_raw_ = 0; + std::int64_t latest_timebase_timestamp_ = 0; +}; + +} // namespace rmcs_core::hardware::device diff --git a/rmcs_ws/src/rmcs_core/src/hardware/omni_infantry.cpp b/rmcs_ws/src/rmcs_core/src/hardware/omni_infantry.cpp index 7a4c5cd15..84b720776 100644 --- a/rmcs_ws/src/rmcs_core/src/hardware/omni_infantry.cpp +++ b/rmcs_ws/src/rmcs_core/src/hardware/omni_infantry.cpp @@ -2,12 +2,12 @@ #include #include #include -#include #include #include -#include +#include #include +#include #include #include #include @@ -16,11 +16,13 @@ #include #include #include +#include #include #include #include -#include "hardware/device/bmi088.hpp" +#include "hardware/device/bmi088_ekf.hpp" +#include "hardware/device/board_clock_lifter.hpp" #include "hardware/device/can_packet.hpp" #include "hardware/device/dji_motor.hpp" #include "hardware/device/dr16.hpp" @@ -32,13 +34,13 @@ namespace rmcs_core::hardware { class OmniInfantry : public rmcs_executor::Component , public rclcpp::Node - , private librmcs::agent::CBoard { + , private librmcs::agent::RmcsBoardLite { public: OmniInfantry() : Node{ get_component_name(), rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true)} - , librmcs::agent::CBoard{get_parameter("board_serial").as_string()} + , librmcs::agent::RmcsBoardLite{get_parameter("board_serial").as_string()} , logger_(get_logger()) , infantry_command_( create_partner_component(get_component_name() + "_command", *this)) @@ -53,8 +55,7 @@ class OmniInfantry , gimbal_left_friction_(*this, *infantry_command_, "/gimbal/left_friction") , gimbal_right_friction_(*this, *infantry_command_, "/gimbal/right_friction") , gimbal_bullet_feeder_(*this, *infantry_command_, "/gimbal/bullet_feeder") - , dr16_{*this} - , bmi088_(1000, 0.2, 0.0) { + , dr16_{*this} { for (auto& motor : chassis_wheel_motors_) motor.configure( @@ -85,19 +86,20 @@ class OmniInfantry register_output("/gimbal/yaw/velocity_imu", gimbal_yaw_velocity_imu_); register_output("/gimbal/pitch/velocity_imu", gimbal_pitch_velocity_imu_); + register_output("/gimbal/auto_aim/exposure_signal", camera_signal_output_); + register_output("/gimbal/auto_aim/imu_snapshot", imu_snapshot_output_); register_output("/tf", tf_); - bmi088_.set_coordinate_mapping([](double x, double y, double z) { - // Get the mapping with the following code. - // The rotation angle must be an exact multiple of 90 degrees, otherwise use a matrix. - - // Eigen::AngleAxisd pitch_link_to_imu_link{ - // std::numbers::pi / 2, Eigen::Vector3d::UnitZ()}; - // Eigen::Vector3d mapping = pitch_link_to_imu_link * Eigen::Vector3d{1, 2, 3}; - // std::cout << mapping << std::endl; - - return std::make_tuple(y, -x, z); - }); + start_transmit().gpio_digital_read( + librmcs::spec::rmcs_board_lite::kGpioDescriptors.kUart0Tx, + { + .period_ms = 0, + .asap = false, + .rising_edge = false, + .falling_edge = true, + .capture_timestamp = true, + .pull = librmcs::data::GpioPull::kUp, + }); using namespace rmcs_description; // NOLINT(google-build-using-namespace) tf_->set_transform(Eigen::Translation3d{0.06603, 0.0, 0.082}); @@ -217,14 +219,15 @@ class OmniInfantry } void update_imu() { - bmi088_.update_status(); - Eigen::Quaterniond const gimbal_imu_pose{ - bmi088_.q0(), bmi088_.q1(), bmi088_.q2(), bmi088_.q3()}; + const auto snapshot = bmi088_.snapshot(); + if (!snapshot) + return; + tf_->set_transform( - gimbal_imu_pose.conjugate()); + snapshot->orientation.conjugate()); - *gimbal_yaw_velocity_imu_ = bmi088_.gz(); - *gimbal_pitch_velocity_imu_ = bmi088_.gy(); + *gimbal_yaw_velocity_imu_ = snapshot->gyro_body.z(); + *gimbal_pitch_velocity_imu_ = snapshot->gyro_body.y(); } void gimbal_calibrate_subscription_callback(std_msgs::msg::Int32::UniquePtr) { @@ -276,6 +279,21 @@ class OmniInfantry } } + void gpio_digital_read_result_callback( + const librmcs::spec::rmcs_board_lite::GpioDescriptor& gpio, + const librmcs::data::GpioDigitalDataView& data) override { + if (gpio != librmcs::spec::rmcs_board_lite::kGpioDescriptors.kUart0Tx) + return; + if (!data.timestamp_quarter_us) + return; + + const auto timestamp = board_clock_lifter_.lift_timestamp(*data.timestamp_quarter_us); + if (!timestamp.has_value()) + return; + + camera_signal_output_.emit(*timestamp); + } + void uart1_receive_callback(const librmcs::data::UartDataView& data) override { const auto* uart_data = data.uart_data.data(); referee_ring_buffer_receive_.emplace_back_n( @@ -288,11 +306,21 @@ class OmniInfantry } void accelerometer_receive_callback(const librmcs::data::AccelerometerDataView& data) override { - bmi088_.store_accelerometer_status(data.x, data.y, data.z); + const auto timestamp = board_clock_lifter_.advance_timebase(data.timestamp_quarter_us); + bmi088_.push_accelerometer_sample(data.x, data.y, data.z, timestamp); } void gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data) override { - bmi088_.store_gyroscope_status(data.x, data.y, data.z); + const auto timestamp = board_clock_lifter_.lift_timestamp(data.timestamp_quarter_us); + if (!timestamp.has_value()) + return; + + auto snapshot = + bmi088_.try_update_with_gyroscope_sample(data.x, data.y, data.z, *timestamp); + if (!snapshot) + return; + + imu_snapshot_output_.emit(*snapshot); } private: @@ -323,10 +351,13 @@ class OmniInfantry device::DjiMotor gimbal_bullet_feeder_; device::Dr16 dr16_; - device::Bmi088 bmi088_; + device::Bmi088Ekf bmi088_; + device::BoardClockLifter board_clock_lifter_; OutputInterface gimbal_yaw_velocity_imu_; OutputInterface gimbal_pitch_velocity_imu_; + EventOutputInterface camera_signal_output_; + EventOutputInterface imu_snapshot_output_; OutputInterface tf_; diff --git a/rmcs_ws/src/rmcs_executor/include/rmcs_executor/component.hpp b/rmcs_ws/src/rmcs_executor/include/rmcs_executor/component.hpp index bfb102888..bd9ed067d 100644 --- a/rmcs_ws/src/rmcs_executor/include/rmcs_executor/component.hpp +++ b/rmcs_ws/src/rmcs_executor/include/rmcs_executor/component.hpp @@ -1,22 +1,56 @@ #pragma once +#include #include +#include +#include +#include #include #include +#include #include #include #include +#include #include #include #include +#include #include +#include +#include + namespace rmcs_executor { +enum class InterfaceKind { + Normal, + Event, +}; + +inline const char* interface_kind_name(InterfaceKind kind) { + switch (kind) { + case InterfaceKind::Normal: return "Normal"; + case InterfaceKind::Event: return "Event"; + } + + return "Unknown"; +} + +using EventState = std::uint32_t; +constexpr EventState EVENT_DISABLED_BIT = EventState{1} << 31; +constexpr EventState EVENT_ACTIVE_MASK = ~EVENT_DISABLED_BIT; + class Component { public: friend class Executor; + struct OutputInfo { + std::reference_wrapper type; + InterfaceKind kind; + }; + using OutputInfoMap = std::map; + Component(const Component&) = delete; Component& operator=(const Component&) = delete; Component(Component&&) = delete; @@ -24,9 +58,7 @@ class Component { virtual ~Component() = default; - virtual void before_pairing(const std::map& output_map) { - (void)output_map; - } + virtual void before_pairing(const OutputInfoMap& output_map) { (void)output_map; } virtual void before_updating() {} virtual void update() = 0; @@ -79,9 +111,9 @@ class Component { const T& operator*() const { return *data_pointer_; } private: - void** activate() { + void* activate() { activated = true; - return reinterpret_cast(&data_pointer_); + return reinterpret_cast(&data_pointer_); } T* data_pointer_ = nullptr; @@ -90,6 +122,139 @@ class Component { bool delete_data_when_deconstruct = false; }; + template + requires(!std::is_reference_v && !std::is_unbounded_array_v) class EventInputInterface { + public: + friend class Component; + + template + requires std::invocable + explicit EventInputInterface(Callback&& callback) + : callback_(std::forward(callback)) {} + + EventInputInterface(const EventInputInterface&) = delete; + EventInputInterface& operator=(const EventInputInterface&) = delete; + EventInputInterface(EventInputInterface&&) = delete; + EventInputInterface& operator=(EventInputInterface&&) = delete; + + [[nodiscard]] bool active() const { return activated; } + [[nodiscard]] bool ready() const { return static_cast(callback_); } + + private: + void* activate() { + if (!ready()) + throw std::runtime_error( + "The event input interface requires a callback before registration"); + + activated = true; + return reinterpret_cast(&callback_); + } + + std::function callback_; + bool activated = false; + }; + + template + requires( + !std::is_reference_v && !std::is_unbounded_array_v + && std::is_nothrow_copy_constructible_v && std::is_nothrow_destructible_v) + class QueuedEventInputInterface final : public EventInputInterface { + public: + template + requires std::invocable + QueuedEventInputInterface(size_t queue_depth, Callback&& callback) + : EventInputInterface([this](const T& event) { enqueue(event); }) + , user_callback_(std::forward(callback)) + , queue_(queue_depth) + , worker_(&QueuedEventInputInterface::worker_main, this) {} + + ~QueuedEventInputInterface() { + stop_requested_.store(true, std::memory_order::relaxed); + notify_event(); + if (worker_.joinable()) + worker_.join(); + } + + QueuedEventInputInterface(const QueuedEventInputInterface&) = delete; + QueuedEventInputInterface& operator=(const QueuedEventInputInterface&) = delete; + QueuedEventInputInterface(QueuedEventInputInterface&&) = delete; + QueuedEventInputInterface& operator=(QueuedEventInputInterface&&) = delete; + + private: + void enqueue(const T& event) { + if (stop_requested_.load(std::memory_order::relaxed)) + return; + + auto guard = std::scoped_lock{enqueue_mutex_}; + if (stop_requested_.load(std::memory_order::relaxed)) + return; + + if (!queue_.push_back(event)) { + const auto dropped_count = dropped_event_count_++; + if (dropped_count == 0) { + RCLCPP_WARN( + rclcpp::get_logger("rmcs_executor"), + "QueuedEventInputInterface started dropping events because the queue is " + "full"); + } + return; + } + + const auto dropped_count = dropped_event_count_; + dropped_event_count_ = 0; + if (dropped_count != 0) { + RCLCPP_WARN( + rclcpp::get_logger("rmcs_executor"), + "QueuedEventInputInterface resumed enqueueing after dropping %u events", + dropped_count); + } + + notify_event(); + } + + void notify_event() { + event_count_.fetch_add(1, std::memory_order::release); + event_count_.notify_one(); + } + + void worker_main() { + while (!stop_requested_.load(std::memory_order::relaxed)) { + if (auto* event = queue_.peek_front()) { + try { + user_callback_(std::move(*event)); + } catch (const std::exception& exception) { + RCLCPP_ERROR( + rclcpp::get_logger("rmcs_executor"), + "QueuedEventInputInterface worker terminated by exception: %s", + exception.what()); + return; + } catch (...) { + RCLCPP_ERROR( + rclcpp::get_logger("rmcs_executor"), + "QueuedEventInputInterface worker terminated by unknown exception"); + return; + } + + if (!queue_.pop_front([](T&&) noexcept {})) + std::terminate(); + continue; + } + + const auto old = event_count_.load(std::memory_order::relaxed); + if (!queue_.readable() && !stop_requested_.load(std::memory_order::relaxed)) + event_count_.wait(old, std::memory_order::acquire); + } + } + + std::function user_callback_; + rmcs_utility::RingBuffer queue_; + std::atomic stop_requested_{false}; + std::uint32_t dropped_event_count_ = 0; + std::atomic event_count_{0}; + std::mutex enqueue_mutex_; + std::thread worker_; + }; + template requires(!std::is_reference_v && !std::is_unbounded_array_v) class OutputInterface { public: @@ -104,25 +269,111 @@ class Component { ~OutputInterface() { if (active()) - std::destroy_at(std::launder(reinterpret_cast(&data_))); + std::destroy_at(storage_pointer()); }; [[nodiscard]] bool active() const { return activated; } - T* operator->() { return reinterpret_cast(&data_); } - const T* operator->() const { return reinterpret_cast(&data_); } - T& operator*() { return *reinterpret_cast(&data_); } - const T& operator*() const { return *reinterpret_cast(&data_); } + T* operator->() { return storage_pointer(); } + const T* operator->() const { return storage_pointer(); } + T& operator*() { return *storage_pointer(); } + const T& operator*() const { return *storage_pointer(); } private: template void* activate(Args&&... args) { - ::new (&data_) T(std::forward(args)...); + std::construct_at(raw_storage_pointer(), std::forward(args)...); + activated = true; + return data_; + } + + [[nodiscard]] T* raw_storage_pointer() { return reinterpret_cast(data_); } + + [[nodiscard]] T* storage_pointer() { return std::launder(raw_storage_pointer()); } + + [[nodiscard]] const T* storage_pointer() const { + return std::launder(reinterpret_cast(data_)); + } + + alignas(T) std::byte data_[sizeof(T)]; + bool activated = false; + }; + + template + requires(!std::is_reference_v && !std::is_unbounded_array_v) class EventOutputInterface { + public: + friend class Component; + + EventOutputInterface() = default; + + EventOutputInterface(const EventOutputInterface&) = delete; + EventOutputInterface& operator=(const EventOutputInterface&) = delete; + EventOutputInterface(EventOutputInterface&&) = delete; + EventOutputInterface& operator=(EventOutputInterface&&) = delete; + + [[nodiscard]] bool active() const { return activated; } + + void emit(const T& event) { + if (!active()) + throw std::runtime_error("The event output interface has not been activated"); + if (!try_enter_emit()) + return; + + struct LeaveEmitGuard { + EventOutputInterface& interface; + + ~LeaveEmitGuard() { interface.leave_emit(); } + } leave_emit_guard{*this}; + + for (const auto* callback : callback_list_) + (*callback)(event); + } + + private: + using EventCallback = std::function; + + void* activate() { activated = true; - return reinterpret_cast(&data_); + return this; } - std::aligned_storage_t data_; + void add_listener(EventCallback* callback) { callback_list_.emplace_back(callback); } + + bool try_enter_emit() { + auto state = state_.load(std::memory_order_relaxed); + while (true) { + if (state & EVENT_DISABLED_BIT) + return false; + if ((state & EVENT_ACTIVE_MASK) == EVENT_ACTIVE_MASK) + throw std::runtime_error("Too many active event emissions"); + + if (state_.compare_exchange_weak( + state, state + 1, std::memory_order_relaxed, std::memory_order_relaxed)) + return true; + } + } + + void leave_emit() { + const auto previous_state = state_.fetch_sub(1, std::memory_order_release); + const auto new_state = previous_state - 1; + if ((new_state & EVENT_DISABLED_BIT) != 0 && (new_state & EVENT_ACTIVE_MASK) == 0) + state_.notify_one(); + } + + void disable() { state_.fetch_or(EVENT_DISABLED_BIT, std::memory_order_relaxed); } + + void enable() { state_.fetch_and(EVENT_ACTIVE_MASK, std::memory_order_relaxed); } + + void wait_idle() { + auto state = state_.load(std::memory_order_acquire); + while ((state & EVENT_ACTIVE_MASK) != 0) { + state_.wait(state, std::memory_order_acquire); + state = state_.load(std::memory_order_acquire); + } + } + + std::vector callback_list_; + std::atomic state_{EVENT_DISABLED_BIT}; bool activated = false; }; @@ -133,7 +384,25 @@ class Component { const std::string& name, InputInterface& interface, bool required = true) { if (interface.active()) throw std::runtime_error("The interface has been activated"); - input_list_.emplace_back(typeid(T), name, required, interface.activate()); + + ensure_registration_name_is_available( + name, InterfaceKind::Normal, RegistrationDirection::Input); + input_list_.emplace_back( + typeid(T), name, InterfaceKind::Normal, required, interface.activate(), + &bind_input_interface); + } + + template + void register_input( + const std::string& name, EventInputInterface& interface, bool required = true) { + if (interface.active()) + throw std::runtime_error("The interface has been activated"); + + ensure_registration_name_is_available( + name, InterfaceKind::Event, RegistrationDirection::Input); + input_list_.emplace_back( + typeid(T), name, InterfaceKind::Event, required, interface.activate(), + &bind_event_input_interface); } template @@ -141,14 +410,31 @@ class Component { void register_output(const std::string& name, OutputInterface& interface, Args&&... args) { if (interface.active()) throw std::runtime_error("The interface has been activated"); + + ensure_registration_name_is_available( + name, InterfaceKind::Normal, RegistrationDirection::Output); output_list_.emplace_back( - typeid(T), name, interface.activate(std::forward(args)...), this); + typeid(T), name, InterfaceKind::Normal, interface.activate(std::forward(args)...), + nullptr, nullptr, nullptr, this); + } + + template + void register_output(const std::string& name, EventOutputInterface& interface) { + if (interface.active()) + throw std::runtime_error("The interface has been activated"); + + ensure_registration_name_is_available( + name, InterfaceKind::Event, RegistrationDirection::Output); + output_list_.emplace_back( + typeid(T), name, InterfaceKind::Event, interface.activate(), + &enable_event_output_interface, &disable_event_output_interface, + &wait_event_output_interface_idle, this); } template requires std::constructible_from std::shared_ptr create_partner_component(const std::string& name, Args&&... args) { - initializing_component_name = name.c_str(); + initializing_component_name = name; auto component = std::make_shared(std::forward(args)...); partner_component_list_.emplace_back(component); @@ -156,26 +442,104 @@ class Component { return component; } - static const char* initializing_component_name; + static std::string initializing_component_name; protected: Component() : component_name_(initializing_component_name) {} private: + enum class RegistrationDirection { + Input, + Output, + }; + + using BindFunction = void (*)(void* input_binding, void* output_binding); + using OutputLifecycleHook = void (*)(void* output_binding); + + static const char* registration_direction_name(RegistrationDirection direction) { + switch (direction) { + case RegistrationDirection::Input: return "input"; + case RegistrationDirection::Output: return "output"; + } + + return "interface"; + } + + void ensure_registration_name_is_available( + const std::string& name, InterfaceKind kind, RegistrationDirection direction) const { + auto check_declarations = [&](const auto& declarations, + RegistrationDirection existing_direction) { + for (const auto& declaration : declarations) { + if (declaration.name != name) + continue; + + if (declaration.kind != kind) { + throw std::runtime_error( + "Component [" + component_name_ + "] cannot register " + + interface_kind_name(kind) + " " + registration_direction_name(direction) + + " \"" + name + "\" because \"" + name + + "\" has already been registered as " + + interface_kind_name(declaration.kind) + " " + + registration_direction_name(existing_direction) + " in this component."); + } + + throw std::runtime_error( + "Component [" + component_name_ + "] registered " + interface_kind_name(kind) + + " " + registration_direction_name(direction) + " \"" + name + + "\" more than once."); + } + }; + + check_declarations(input_list_, RegistrationDirection::Input); + check_declarations(output_list_, RegistrationDirection::Output); + } + + template + static void bind_input_interface(void* input_binding, void* output_binding) { + *reinterpret_cast(input_binding) = reinterpret_cast(output_binding); + } + + template + static void bind_event_input_interface(void* input_binding, void* output_binding) { + auto& callback = *reinterpret_cast*>(input_binding); + reinterpret_cast*>(output_binding)->add_listener(&callback); + } + + template + static void enable_event_output_interface(void* output_binding) { + reinterpret_cast*>(output_binding)->enable(); + } + + template + static void disable_event_output_interface(void* output_binding) { + reinterpret_cast*>(output_binding)->disable(); + } + + template + static void wait_event_output_interface_idle(void* output_binding) { + reinterpret_cast*>(output_binding)->wait_idle(); + } + std::string component_name_; struct InputDeclaration { const std::type_info& type; std::string name; + InterfaceKind kind; bool required; - void** pointer_to_data_pointer; + void* binding; + BindFunction bind; }; struct OutputDeclaration { const std::type_info& type; std::string name; - void* data_pointer; + InterfaceKind kind; + void* binding; + OutputLifecycleHook enable; + OutputLifecycleHook disable; + OutputLifecycleHook wait_idle; Component* component; }; diff --git a/rmcs_ws/src/rmcs_executor/package.xml b/rmcs_ws/src/rmcs_executor/package.xml index a287fb498..5ed7bcd65 100644 --- a/rmcs_ws/src/rmcs_executor/package.xml +++ b/rmcs_ws/src/rmcs_executor/package.xml @@ -11,6 +11,7 @@ rclcpp pluginlib + rmcs_utility ament_cmake diff --git a/rmcs_ws/src/rmcs_executor/src/component.cpp b/rmcs_ws/src/rmcs_executor/src/component.cpp index 561ac7a28..181ae975f 100644 --- a/rmcs_ws/src/rmcs_executor/src/component.cpp +++ b/rmcs_ws/src/rmcs_executor/src/component.cpp @@ -2,6 +2,6 @@ namespace rmcs_executor { -const char* Component::initializing_component_name; +std::string Component::initializing_component_name; -} // namespace rmcs_executor \ No newline at end of file +} // namespace rmcs_executor diff --git a/rmcs_ws/src/rmcs_executor/src/executor.hpp b/rmcs_ws/src/rmcs_executor/src/executor.hpp index 49f03b1f9..eb454fd01 100644 --- a/rmcs_ws/src/rmcs_executor/src/executor.hpp +++ b/rmcs_ws/src/rmcs_executor/src/executor.hpp @@ -2,8 +2,10 @@ #include #include +#include #include #include +#include #include #include @@ -29,12 +31,14 @@ class Executor final : public rclcpp::Node { predefined_msg_provider_ = std::make_shared(); add_component(predefined_msg_provider_); } - ~Executor() { + ~Executor() override { + disable_event_outputs(); + wait_event_outputs_idle(); if (thread_.joinable()) thread_.join(); }; - void add_component(std::shared_ptr component) { + void add_component(const std::shared_ptr& component) { component_list_.emplace_back(component); if (auto node = std::dynamic_pointer_cast(component)) rcl_executor_.add_node(node); @@ -52,35 +56,195 @@ class Executor final : public rclcpp::Node { if (!get_parameter("update_rate", update_rate)) throw std::runtime_error{"Unable to get parameter update_rate"}; predefined_msg_provider_->set_update_rate(update_rate); + enable_event_outputs(); thread_ = std::thread{[update_rate, this]() { const auto period = std::chrono::nanoseconds( static_cast(std::round(1'000'000'000.0 / update_rate))); auto next_iteration_time = std::chrono::steady_clock::now(); - while (rclcpp::ok()) { - predefined_msg_provider_->set_timestamp(next_iteration_time); - next_iteration_time += period; - for (const auto& component : updating_order_) { - component->update(); + try { + while (rclcpp::ok()) { + predefined_msg_provider_->set_timestamp(next_iteration_time); + next_iteration_time += period; + for (const auto& component : updating_order_) { + component->update(); + } + std::this_thread::sleep_until(next_iteration_time); } - std::this_thread::sleep_until(next_iteration_time); + } catch (const std::exception& exception) { + RCLCPP_FATAL( + get_logger(), "Executor update thread terminated by exception: %s", + exception.what()); + rclcpp::shutdown(); + } catch (...) { + RCLCPP_FATAL( + get_logger(), "Executor update thread terminated by unknown exception"); + rclcpp::shutdown(); } }}; } private: + void enable_event_outputs() { + for (const auto& component : component_list_) { + for (const auto& output : component->output_list_) { + if (output.enable == nullptr) + continue; + output.enable(output.binding); + } + } + } + + void disable_event_outputs() { + for (const auto& component : component_list_) { + for (const auto& output : component->output_list_) { + if (output.disable == nullptr) + continue; + output.disable(output.binding); + } + } + } + + void wait_event_outputs_idle() { + for (const auto& component : component_list_) { + for (const auto& output : component->output_list_) { + if (output.wait_idle == nullptr) + continue; + output.wait_idle(output.binding); + } + } + } + + struct InterfaceKey { + std::string name; + InterfaceKind kind; + + bool operator==(const InterfaceKey& other) const { + return name == other.name && kind == other.kind; + } + }; + + struct InterfaceKeyHash { + std::size_t operator()(const InterfaceKey& key) const { + return std::hash{}(key.name) + ^ (std::hash{}(static_cast(key.kind)) << 1U); + } + }; + + using OutputRecord = Component::OutputDeclaration*; + + struct NameKindRecord { + InterfaceKind kind; + std::string component_name; + const char* direction; + }; + + static std::string describe_output( + const std::string& name, InterfaceKind kind, const std::type_info& type, + const std::string& component_name) { + std::ostringstream stream; + stream << "Component [" << component_name << "] registered " << interface_kind_name(kind) + << " output \"" << name << "\" with type \"" << type.name() << "\""; + return stream.str(); + } + + static std::string describe_output(const Component::OutputDeclaration& output) { + return describe_output( + output.name, output.kind, output.type, output.component->get_component_name()); + } + + static std::string describe_declaration( + const std::string& name, InterfaceKind kind, const std::string& component_name, + const char* direction) { + std::ostringstream stream; + stream << "Component [" << component_name << "] registered " << interface_kind_name(kind) + << ' ' << direction << " \"" << name << "\""; + return stream.str(); + } + void init() { updating_order_.clear(); - auto output_map = std::unordered_map{}; - auto user_output_map = std::map{}; + auto output_map = std::unordered_map{}; + auto output_by_name = std::unordered_map{}; + auto declaration_kind_map = std::unordered_map{}; + auto user_output_map = Component::OutputInfoMap{}; for (const auto& component : component_list_) { component->dependency_count_ = 0; component->wanted_by_.clear(); for (auto& output : component->output_list_) { - if (!output_map.emplace(output.name, &output).second) - throw std::runtime_error{"Duplicate names of output"}; - user_output_map.emplace(output.name, output.type); + auto declaration_iter = declaration_kind_map.find(output.name); + if (declaration_iter != declaration_kind_map.end() + && declaration_iter->second.kind != output.kind) { + const auto message = + std::string{"Conflicting interface kinds for name \""} + output.name + + "\": " + + describe_declaration( + output.name, declaration_iter->second.kind, + declaration_iter->second.component_name, + declaration_iter->second.direction) + + "; " + + describe_declaration( + output.name, output.kind, component->get_component_name(), "output") + + ". A name can only belong to one interface kind."; + RCLCPP_FATAL(get_logger(), "%s", message.c_str()); + throw std::runtime_error{message}; + } + declaration_kind_map.emplace( + output.name, + NameKindRecord{output.kind, component->get_component_name(), "output"}); + + auto existing_output_iter = output_by_name.find(output.name); + if (existing_output_iter != output_by_name.end() + && existing_output_iter->second->kind != output.kind) { + const auto& existing_output = *existing_output_iter->second; + const auto message = std::string{"Conflicting output kinds for name \""} + + output.name + "\": " + describe_output(existing_output) + + "; " + describe_output(output) + + ". A name can only belong to one interface kind."; + RCLCPP_FATAL(get_logger(), "%s", message.c_str()); + throw std::runtime_error{message}; + } + + output_by_name.emplace(output.name, &output); + + const auto [output_iter, inserted] = + output_map.emplace(InterfaceKey{output.name, output.kind}, &output); + if (!inserted) { + const auto& existing_output = *output_iter->second; + const auto message = + std::string{"Duplicate "} + interface_kind_name(output.kind) + + " output name \"" + output.name + + "\": " + describe_output(existing_output) + "; " + describe_output(output) + + ". Only one output may be registered for each (name, kind)."; + RCLCPP_FATAL(get_logger(), "%s", message.c_str()); + throw std::runtime_error{message}; + } + + user_output_map.emplace( + output.name, Component::OutputInfo{std::cref(output.type), output.kind}); + } + + for (const auto& input : component->input_list_) { + auto declaration_iter = declaration_kind_map.find(input.name); + if (declaration_iter != declaration_kind_map.end() + && declaration_iter->second.kind != input.kind) { + const auto message = + std::string{"Conflicting interface kinds for name \""} + input.name + "\": " + + describe_declaration( + input.name, declaration_iter->second.kind, + declaration_iter->second.component_name, + declaration_iter->second.direction) + + "; " + + describe_declaration( + input.name, input.kind, component->get_component_name(), "input") + + ". A name can only belong to one interface kind."; + RCLCPP_FATAL(get_logger(), "%s", message.c_str()); + throw std::runtime_error{message}; + } + declaration_kind_map.emplace( + input.name, + NameKindRecord{input.kind, component->get_component_name(), "input"}); } } @@ -90,36 +254,54 @@ class Executor final : public rclcpp::Node { for (const auto& component : component_list_) { for (const auto& input : component->input_list_) { - auto output_iter = output_map.find(input.name); + auto output_iter = output_map.find(InterfaceKey{input.name, input.kind}); if (output_iter == output_map.end()) { + auto output_by_name_iter = output_by_name.find(input.name); + if (output_by_name_iter != output_by_name.end()) { + const auto& available_output = *output_by_name_iter->second; + const auto message = + std::string{"Component ["} + component->get_component_name() + + "] requested " + interface_kind_name(input.kind) + " input \"" + + input.name + "\", but " + describe_output(available_output) + "."; + + RCLCPP_FATAL(get_logger(), "%s", message.c_str()); + throw std::runtime_error{message}; + } + if (!input.required) continue; - RCLCPP_FATAL( - get_logger(), - "Cannot find the corresponding output of input \"%s\" declared by " - "component [%s]", - input.name.c_str(), component->get_component_name().c_str()); - throw std::runtime_error{"Cannot find the corresponding output"}; + const auto message = std::string{"Cannot find "} + + interface_kind_name(input.kind) + " output \"" + input.name + + "\" required by component [" + + component->get_component_name() + "]."; + RCLCPP_FATAL(get_logger(), "%s", message.c_str()); + throw std::runtime_error{message}; } const auto& output = *output_iter->second; if (input.type != output.type) { - RCLCPP_FATAL(get_logger(), "With message \"%s\":", input.name.c_str()); + const auto message = std::string{"Type mismatch for "} + + interface_kind_name(input.kind) + " interface \"" + + input.name + "\": component [" + + output.component->get_component_name() + + "] declared output type \"" + output.type.name() + + "\", but component [" + component->get_component_name() + + "] requested input type \"" + input.type.name() + "\"."; + RCLCPP_FATAL(get_logger(), "%s", message.c_str()); RCLCPP_FATAL( get_logger(), " Component [%s] declared the output with type \"%s\"", output.component->get_component_name().c_str(), output.type.name()); RCLCPP_FATAL( get_logger(), " Component [%s] requested the input with type \"%s\"", component->get_component_name().c_str(), input.type.name()); - RCLCPP_FATAL(get_logger(), "Type not match."); - throw std::runtime_error{"Type not match"}; + throw std::runtime_error{message}; } if (output.component->wanted_by_.emplace(component.get()).second) component->dependency_count_++; - *input.pointer_to_data_pointer = output.data_pointer; + input.bind(input.binding, output.binding); } } @@ -142,12 +324,17 @@ class Executor final : public rclcpp::Node { RCLCPP_FATAL( get_logger(), "Component [%s]:", component->get_component_name().c_str()); for (const auto& input : component->input_list_) { - const auto& output = output_map[input.name]; + const auto output_iter = output_map.find(InterfaceKey{input.name, input.kind}); + if (output_iter == output_map.end()) + continue; + + const auto* output = output_iter->second; if (output->component->dependency_count_ == 0) continue; RCLCPP_FATAL( - get_logger(), " Depends on [%s] because requesting \"%s\"", - output->component->component_name_.c_str(), output->name.c_str()); + get_logger(), " Depends on [%s] because requesting %s interface \"%s\"", + output->component->component_name_.c_str(), interface_kind_name(input.kind), + output->name.c_str()); } } throw std::runtime_error{"Circular dependency found"}; @@ -184,4 +371,4 @@ class Executor final : public rclcpp::Node { size_t dependency_recursive_level_ = 0; }; -}; // namespace rmcs_executor \ No newline at end of file +} // namespace rmcs_executor diff --git a/rmcs_ws/src/rmcs_executor/src/main.cpp b/rmcs_ws/src/rmcs_executor/src/main.cpp index 99311f24d..da4c29fbf 100644 --- a/rmcs_ws/src/rmcs_executor/src/main.cpp +++ b/rmcs_ws/src/rmcs_executor/src/main.cpp @@ -61,7 +61,7 @@ int main(int argc, char** argv) { plugin_name = component_name = component_description; } - rmcs_executor::Component::initializing_component_name = component_name.c_str(); + rmcs_executor::Component::initializing_component_name = component_name; auto component = component_loader.createSharedInstance(plugin_name); executor->add_component(component); } @@ -70,4 +70,4 @@ int main(int argc, char** argv) { rcl_executor.spin(); rclcpp::shutdown(); -} \ No newline at end of file +} diff --git a/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/board_clock.hpp b/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/board_clock.hpp new file mode 100644 index 000000000..e96fa76c5 --- /dev/null +++ b/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/board_clock.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +namespace rmcs_msgs { + +struct BoardClock { + using rep = std::int64_t; + using period = std::ratio<1, 4'000'000>; + using duration = std::chrono::duration; + using time_point = std::chrono::time_point; + + static constexpr bool is_steady = true; +}; + +} // namespace rmcs_msgs diff --git a/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/camera_frame_raw.hpp b/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/camera_frame_raw.hpp new file mode 100644 index 000000000..526b79f24 --- /dev/null +++ b/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/camera_frame_raw.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace rmcs_msgs { + +struct CameraFrameRaw { + static constexpr std::uint32_t kWidth = 1440; + static constexpr std::uint32_t kHeight = 1080; + static constexpr std::size_t kFrameSize = + static_cast(kWidth) * static_cast(kHeight); + + std::array data; + int opencv_cvt_color_code; + + Eigen::Quaterniond imu_snapshot; + + std::chrono::steady_clock::time_point exposure_timestamp; + std::chrono::steady_clock::time_point image_reception_timestamp; + std::chrono::steady_clock::time_point sync_publish_timestamp; +}; + +} // namespace rmcs_msgs diff --git a/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/imu_snapshot.hpp b/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/imu_snapshot.hpp new file mode 100644 index 000000000..1c0747b3f --- /dev/null +++ b/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/imu_snapshot.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace rmcs_msgs { + +struct ImuSnapshot { + Eigen::Quaterniond orientation; + Eigen::Vector3d gyro_body; + rmcs_msgs::BoardClock::time_point timestamp; +}; + +} // namespace rmcs_msgs diff --git a/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/rmcs_msgs.hpp b/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/rmcs_msgs.hpp index 09925e7b1..b2ec7c371 100644 --- a/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/rmcs_msgs.hpp +++ b/rmcs_ws/src/rmcs_msgs/include/rmcs_msgs/rmcs_msgs.hpp @@ -9,18 +9,21 @@ # endif #endif -#include "chassis_mode.hpp" -#include "full_robot_id.hpp" -#include "game_stage.hpp" -#include "gimbal_mode.hpp" -#include "keyboard.hpp" -#include "mouse.hpp" -#include "robot_color.hpp" -#include "robot_id.hpp" -#include "serial_interface.hpp" -#include "shoot_mode.hpp" -#include "shoot_status.hpp" -#include "switch.hpp" +#include "board_clock.hpp" // IWYU pragma: export +#include "camera_frame_raw.hpp" // IWYU pragma: export +#include "chassis_mode.hpp" // IWYU pragma: export +#include "full_robot_id.hpp" // IWYU pragma: export +#include "game_stage.hpp" // IWYU pragma: export +#include "gimbal_mode.hpp" // IWYU pragma: export +#include "imu_snapshot.hpp" // IWYU pragma: export +#include "keyboard.hpp" // IWYU pragma: export +#include "mouse.hpp" // IWYU pragma: export +#include "robot_color.hpp" // IWYU pragma: export +#include "robot_id.hpp" // IWYU pragma: export +#include "serial_interface.hpp" // IWYU pragma: export +#include "shoot_mode.hpp" // IWYU pragma: export +#include "shoot_status.hpp" // IWYU pragma: export +#include "switch.hpp" // IWYU pragma: export namespace rmcs_msgs { diff --git a/rmcs_ws/src/rmcs_utility/include/rmcs_utility/atomic_futex.hpp b/rmcs_ws/src/rmcs_utility/include/rmcs_utility/atomic_futex.hpp new file mode 100644 index 000000000..78ed40620 --- /dev/null +++ b/rmcs_ws/src/rmcs_utility/include/rmcs_utility/atomic_futex.hpp @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace rmcs_utility { +namespace detail { + +inline auto atomic_futex_address(const std::atomic& atomic) noexcept -> uint32_t* { + return const_cast(reinterpret_cast(&atomic)); +} + +inline bool atomic_futex_wait_until_steady( + const std::atomic& atomic, uint32_t old_val, + std::chrono::steady_clock::time_point deadline, std::memory_order order) noexcept { + + if (atomic.load(order) != old_val) + return true; + + timespec abs_time; + clock_gettime(CLOCK_MONOTONIC, &abs_time); + + const auto remaining = + std::chrono::ceil(deadline - std::chrono::steady_clock::now()); + + if (remaining <= std::chrono::nanoseconds::zero()) + return atomic.load(order) != old_val; + + const auto secs = std::chrono::duration_cast(remaining); + const auto nsecs = std::chrono::duration_cast(remaining - secs); + + abs_time.tv_sec += static_cast(secs.count()); + abs_time.tv_nsec += static_cast(nsecs.count()); + + if (abs_time.tv_nsec >= 1'000'000'000) { + abs_time.tv_sec += 1; + abs_time.tv_nsec -= 1'000'000'000; + } + + auto* const ptr = atomic_futex_address(atomic); + + while (atomic.load(order) == old_val) { + if (syscall( + SYS_futex, ptr, FUTEX_WAIT_BITSET_PRIVATE, old_val, &abs_time, nullptr, + FUTEX_BITSET_MATCH_ANY) + == 0) + continue; + + if (errno == EAGAIN || errno == EINTR) + continue; + + if (errno == ETIMEDOUT) + return atomic.load(order) != old_val; + + return atomic.load(order) != old_val; + } + + return true; +} + +} // namespace detail + +/** + * @brief Waits until a 32-bit atomic value changes or the timeout expires. + * + * This helper uses Linux futex syscalls directly and must be paired with + * rmcs_utility::atomic_futex_notify_one() or rmcs_utility::atomic_futex_notify_all() + * on the same atomic object. + * + * @warning This implementation is not compatible with `std::atomic::notify_one()` or + * `std::atomic::notify_all()`. The standard library may track waiters in an internal pool, + * so mixing these APIs on the same atomic object can miss wakeups. + * + * @param atomic Atomic word used as the futex key. + * @param old_val Expected value observed before waiting. + * @param timeout Maximum time to wait. A non-positive timeout performs a non-blocking check. + * @param order Memory order used for polling loads around the futex wait. + * @return `true` if `atomic` no longer equals `old_val`, otherwise `false` when the timeout + * expires. + */ +inline bool atomic_futex_wait_for( + const std::atomic& atomic, uint32_t old_val, + std::chrono::steady_clock::duration timeout, + std::memory_order order = std::memory_order_seq_cst) noexcept { + + if (atomic.load(order) != old_val) + return true; + + if (timeout <= std::chrono::steady_clock::duration::zero()) + return false; + + return detail::atomic_futex_wait_until_steady( + atomic, old_val, std::chrono::steady_clock::now() + timeout, order); +} + +/** + * @brief Waits until a 32-bit atomic value changes or the steady-clock deadline is reached. + * + * This helper uses Linux futex syscalls directly and must be paired with + * rmcs_utility::atomic_futex_notify_one() or rmcs_utility::atomic_futex_notify_all() + * on the same atomic object. + * + * @warning This implementation is not compatible with `std::atomic::notify_one()` or + * `std::atomic::notify_all()`. The standard library may track waiters in an internal pool, + * so mixing these APIs on the same atomic object can miss wakeups. + * + * @param atomic Atomic word used as the futex key. + * @param old_val Expected value observed before waiting. + * @param deadline Steady-clock deadline after which the wait times out. + * @param order Memory order used for polling loads around the futex wait. + * @return `true` if `atomic` no longer equals `old_val`, otherwise `false` when the deadline + * is reached. + */ +inline bool atomic_futex_wait_until( + const std::atomic& atomic, uint32_t old_val, + std::chrono::steady_clock::time_point deadline, + std::memory_order order = std::memory_order_seq_cst) noexcept { + + return detail::atomic_futex_wait_until_steady(atomic, old_val, deadline, order); +} + +/** + * @brief Wakes one thread blocked in rmcs_utility::atomic_futex_wait_for() or + * rmcs_utility::atomic_futex_wait_until(). + * + * @warning This must not be mixed with `std::atomic::notify_one()` or + * `std::atomic::notify_all()` on the same atomic object. + * + * @param atomic Atomic word used as the futex key. + */ +inline void atomic_futex_notify_one(const std::atomic& atomic) noexcept { + syscall( + SYS_futex, detail::atomic_futex_address(atomic), FUTEX_WAKE_BITSET_PRIVATE, 1, nullptr, + nullptr, FUTEX_BITSET_MATCH_ANY); +} + +/** + * @brief Wakes all threads blocked in rmcs_utility::atomic_futex_wait_for() or + * rmcs_utility::atomic_futex_wait_until(). + * + * @warning This must not be mixed with `std::atomic::notify_one()` or + * `std::atomic::notify_all()` on the same atomic object. + * + * @param atomic Atomic word used as the futex key. + */ +inline void atomic_futex_notify_all(const std::atomic& atomic) noexcept { + syscall( + SYS_futex, detail::atomic_futex_address(atomic), FUTEX_WAKE_BITSET_PRIVATE, INT_MAX, + nullptr, nullptr, FUTEX_BITSET_MATCH_ANY); +} + +} // namespace rmcs_utility diff --git a/rmcs_ws/src/rmcs_utility/include/rmcs_utility/memory_pool.hpp b/rmcs_ws/src/rmcs_utility/include/rmcs_utility/memory_pool.hpp new file mode 100644 index 000000000..2044f4a13 --- /dev/null +++ b/rmcs_ws/src/rmcs_utility/include/rmcs_utility/memory_pool.hpp @@ -0,0 +1,196 @@ +#pragma once + +#include +#include +#include +#include + +namespace rmcs_utility { + +template +class MemoryPool { + static_assert(Size > 0, "MemoryPool slot size must be greater than zero"); + static_assert(Align > 0, "MemoryPool slot alignment must be greater than zero"); + static_assert((Align & (Align - 1)) == 0, "MemoryPool slot alignment must be a power of two"); + +public: + /*! + * @brief Construct a fixed-capacity raw storage pool + * @param capacity Number of storage slots to allocate + * @note This container is not thread-safe. + */ + explicit MemoryPool(size_t capacity) + : capacity_(capacity) + , storage_(new Storage[capacity]) + , free_stack_(new size_t[capacity]) + , occupied_(new bool[capacity]{}) { + reset_free_stack(); + } + + MemoryPool(const MemoryPool&) = delete; + MemoryPool& operator=(const MemoryPool&) = delete; + MemoryPool(MemoryPool&&) = delete; + MemoryPool& operator=(MemoryPool&&) = delete; + + /*! + * @brief Destructor + * @note All allocated slots must be returned to the pool before destruction. + * Violating this lifetime contract terminates the program. + */ + ~MemoryPool() noexcept { + if (!empty()) { + std::fprintf( + stderr, + "rmcs_utility::MemoryPool %p destroyed with outstanding slots: size=%zu, " + "capacity=%zu\n", + static_cast(this), size_, capacity_); + std::fflush(stderr); + std::terminate(); + } + } + + /*! + * @brief Payload size of each storage slot in bytes + */ + [[nodiscard]] static constexpr size_t slot_size() noexcept { return Size; } + + /*! + * @brief Alignment guarantee of each storage slot in bytes + */ + [[nodiscard]] static constexpr size_t slot_align() noexcept { return Align; } + + /*! + * @brief Capacity of the memory pool + * @return Total number of storage slots + */ + [[nodiscard]] size_t max_size() const noexcept { return capacity_; } + + /*! + * @brief Number of currently allocated storage slots + */ + [[nodiscard]] size_t size() const noexcept { return size_; } + + /*! + * @brief Number of free storage slots remaining + */ + [[nodiscard]] size_t available() const noexcept { return capacity_ - size_; } + + /*! + * @brief Check whether the pool contains no allocated slot + */ + [[nodiscard]] bool empty() const noexcept { return size_ == 0; } + + /*! + * @brief Check whether the pool has no free slot + */ + [[nodiscard]] bool full() const noexcept { return size_ == capacity_; } + + /*! + * @brief Allocate one raw storage slot from the pool + * @return Pointer to the slot's raw storage, or nullptr if the pool is full + */ + [[nodiscard]] void* allocate() noexcept { + if (full()) + return nullptr; + + const auto index = free_stack_[top_ - 1]; + occupied_[index] = true; + --top_; + ++size_; + + return static_cast(slot_pointer(index)); + } + + /*! + * @brief Release a raw storage slot back to the pool + * @param pointer Pointer previously returned by this pool + * @return true if the slot was released, false if the pointer was invalid + * @note The pointer must refer to the start address of a live slot. + */ + bool free(void* pointer) noexcept { + const auto index = pointer_to_index(pointer); + + if (index == capacity_ || !occupied_[index]) + return false; + + occupied_[index] = false; + free_stack_[top_] = index; + ++top_; + --size_; + + return true; + } + + /*! + * @brief Check whether a pointer refers to a live slot owned by this pool + */ + [[nodiscard]] bool contains(const void* pointer) const noexcept { + const auto index = pointer_to_index(pointer); + return index != capacity_ && occupied_[index]; + } + + /*! + * @brief Release every live slot and reset the pool + * @return Number of slots that were released + */ + size_t clear() noexcept { + size_t count = 0; + + for (size_t i = 0; i < capacity_; i++) { + if (occupied_[i]) { + occupied_[i] = false; + ++count; + } + } + + size_ = 0; + reset_free_stack(); + + return count; + } + +private: + struct Storage { + alignas(Align) std::byte data[Size]; + }; + + [[nodiscard]] std::byte* slot_pointer(size_t index) noexcept { return storage_[index].data; } + + [[nodiscard]] const std::byte* slot_pointer(size_t index) const noexcept { + return storage_[index].data; + } + + [[nodiscard]] size_t pointer_to_index(const void* pointer) const noexcept { + if (pointer == nullptr) + return capacity_; + + const auto begin = reinterpret_cast(storage_.get()); + const auto end = begin + sizeof(Storage) * capacity_; + const auto target = reinterpret_cast(pointer); + + if (target < begin || target >= end) + return capacity_; + + const auto offset = target - begin; + if (offset % sizeof(Storage) != 0) + return capacity_; + + return static_cast(offset / sizeof(Storage)); + } + + void reset_free_stack() noexcept { + for (size_t i = 0; i < capacity_; i++) + free_stack_[capacity_ - 1 - i] = i; + top_ = capacity_; + } + + size_t capacity_; + std::unique_ptr storage_; + std::unique_ptr free_stack_; + std::unique_ptr occupied_; + + size_t top_{0}; + size_t size_{0}; +}; + +} // namespace rmcs_utility diff --git a/rmcs_ws/src/rmcs_utility/include/rmcs_utility/pooled_shared_factory.hpp b/rmcs_ws/src/rmcs_utility/include/rmcs_utility/pooled_shared_factory.hpp new file mode 100644 index 000000000..c2e65a199 --- /dev/null +++ b/rmcs_ws/src/rmcs_utility/include/rmcs_utility/pooled_shared_factory.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "rmcs_utility/memory_pool.hpp" + +namespace rmcs_utility { + +template +requires std::is_nothrow_destructible_v class PooledSharedFactory { +public: + explicit PooledSharedFactory(size_t capacity) + : pool_(std::make_shared(capacity)) {} + + template + [[nodiscard]] std::shared_ptr make(Args&&... args) { + auto ptr = try_make(std::forward(args)...); + if (!ptr) + throw std::bad_alloc{}; + return ptr; + } + + template + [[nodiscard]] std::shared_ptr try_make(Args&&... args) { + void* storage = nullptr; + { + auto guard = std::scoped_lock{pool_->mutex}; + storage = pool_->allocate(); + } + + if (!storage) + return nullptr; + + try { + return std::shared_ptr( + std::construct_at(static_cast(storage), std::forward(args)...), + [pool = pool_](T* ptr) { + std::destroy_at(ptr); + auto guard = std::scoped_lock{pool->mutex}; + if (!pool->free(ptr)) { + std::fprintf( + stderr, + "rmcs_utility::PooledSharedFactory failed to return slot %p to pool " + "%p\n", + static_cast(ptr), static_cast(pool.get())); + std::fflush(stderr); + std::terminate(); + } + }); + } catch (...) { + auto guard = std::scoped_lock{pool_->mutex}; + pool_->free(storage); + throw; + } + } + + [[nodiscard]] size_t max_size() const noexcept { return pool_->max_size(); } + +private: + class PoolImpl final : public MemoryPool { + public: + using MemoryPool::MemoryPool; + + mutable std::mutex mutex; + }; + + std::shared_ptr pool_; +}; + +} // namespace rmcs_utility diff --git a/rmcs_ws/src/rmcs_utility/include/rmcs_utility/ring_buffer.hpp b/rmcs_ws/src/rmcs_utility/include/rmcs_utility/ring_buffer.hpp index 2c829c64a..d6c5ca5db 100644 --- a/rmcs_ws/src/rmcs_utility/include/rmcs_utility/ring_buffer.hpp +++ b/rmcs_ws/src/rmcs_utility/include/rmcs_utility/ring_buffer.hpp @@ -2,10 +2,13 @@ #include #include +#include #include +#include #include #include #include +#include #include namespace rmcs_utility { @@ -15,6 +18,179 @@ namespace rmcs_utility { template class RingBuffer { public: + template + class ReadableView; + + template + class BasicIterator { + using Buffer = std::conditional_t; + + public: + using iterator_category = std::random_access_iterator_tag; + using iterator_concept = std::random_access_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using reference = std::conditional_t; + using pointer = std::conditional_t; + + BasicIterator() = default; + BasicIterator(const BasicIterator&) = default; + BasicIterator(BasicIterator&&) = default; + BasicIterator& operator=(const BasicIterator&) = default; + BasicIterator& operator=(BasicIterator&&) = default; + + // NOLINTNEXTLINE(google-explicit-constructor) + BasicIterator(const BasicIterator& other) requires is_const + : buffer_(other.buffer_) + , origin_(other.origin_) + , offset_(other.offset_) {} + + reference operator*() const { return *ptr(); } + pointer operator->() const { return ptr(); } + reference operator[](difference_type n) const { return *(*this + n); } + + BasicIterator& operator++() { + offset_++; + return *this; + } + + BasicIterator operator++(int) { + auto old = *this; + ++*this; + return old; + } + + BasicIterator& operator--() { + offset_--; + return *this; + } + + BasicIterator operator--(int) { + auto old = *this; + --*this; + return old; + } + + BasicIterator& operator+=(difference_type n) { + if (n >= 0) + offset_ += static_cast(n); + else + offset_ -= static_cast(-(n + 1)) + 1; + return *this; + } + + BasicIterator& operator-=(difference_type n) { + if (n >= 0) + offset_ -= static_cast(n); + else + offset_ += static_cast(-(n + 1)) + 1; + return *this; + } + + friend BasicIterator operator+(BasicIterator it, difference_type n) { + it += n; + return it; + } + + friend BasicIterator operator+(difference_type n, BasicIterator it) { + it += n; + return it; + } + + friend BasicIterator operator-(BasicIterator it, difference_type n) { + it -= n; + return it; + } + + friend difference_type operator-(BasicIterator lhs, BasicIterator rhs) { + if (lhs.offset_ >= rhs.offset_) + return static_cast(lhs.offset_ - rhs.offset_); + return -static_cast(rhs.offset_ - lhs.offset_); + } + + friend bool operator==(BasicIterator lhs, BasicIterator rhs) { + return lhs.buffer_ == rhs.buffer_ && lhs.origin_ == rhs.origin_ + && lhs.offset_ == rhs.offset_; + } + + friend std::strong_ordering operator<=>(BasicIterator lhs, BasicIterator rhs) { + return lhs.offset_ <=> rhs.offset_; + } + + private: + friend class RingBuffer; + template + friend class BasicIterator; + template + friend class ReadableView; + + BasicIterator(Buffer* buffer, size_t origin, size_t offset) + : buffer_(buffer) + , origin_(origin) + , offset_(offset) {} + + pointer ptr() const { + return std::launder( + reinterpret_cast( + buffer_->storage_[(origin_ + offset_) & buffer_->mask_].data)); + } + + Buffer* buffer_ = nullptr; + size_t origin_ = 0; + size_t offset_ = 0; + }; + + using iterator = BasicIterator; + using const_iterator = BasicIterator; + + template + class ReadableView { + using Buffer = std::conditional_t; + + public: + using iterator = BasicIterator; + using value_type = T; + using difference_type = std::ptrdiff_t; + using size_type = size_t; + using reference = std::conditional_t; + + ReadableView() = default; + ReadableView(const ReadableView&) = default; + ReadableView(ReadableView&&) = default; + ReadableView& operator=(const ReadableView&) = default; + ReadableView& operator=(ReadableView&&) = default; + + // NOLINTNEXTLINE(google-explicit-constructor) + ReadableView(const ReadableView& other) requires is_const + : buffer_(other.buffer_) + , origin_(other.origin_) + , size_(other.size_) {} + + iterator begin() const { return iterator{buffer_, origin_, 0}; } + iterator end() const { return iterator{buffer_, origin_, size_}; } + + [[nodiscard]] bool empty() const { return size_ == 0; } + [[nodiscard]] size_type size() const { return size_; } + + reference front() const { return *begin(); } + reference back() const { return *(end() - 1); } + reference operator[](size_type index) const { return begin()[index]; } + + private: + friend class RingBuffer; + template + friend class ReadableView; + + ReadableView(Buffer* buffer, size_t origin, size_t size) + : buffer_(buffer) + , origin_(origin) + , size_(size) {} + + Buffer* buffer_ = nullptr; + size_t origin_ = 0; + size_t size_ = 0; + }; + /*! * @brief Construct an SPSC ring buffer * @param size Minimum capacity requested. Actual capacity is rounded up @@ -75,6 +251,31 @@ class RingBuffer { return max_size() - (in - out); } + /*! + * @brief Snapshot view of elements currently readable by the consumer + * @note Captures [out, in) once. Producer pushes do not extend the returned view. + * Consumer pops invalidate iterators to erased elements. + */ + ReadableView readable_view() noexcept { + const auto in = in_.load(std::memory_order::acquire); + const auto out = out_.load(std::memory_order::relaxed); + return {this, out, in - out}; + } + + /*! + * @brief Const snapshot view of elements currently readable by the consumer + */ + ReadableView readable_view() const noexcept { + const auto in = in_.load(std::memory_order::acquire); + const auto out = out_.load(std::memory_order::relaxed); + return {this, out, in - out}; + } + + /*! + * @brief Explicit const snapshot view for non-const buffers + */ + ReadableView const_readable_view() const noexcept { return readable_view(); } + /*! * @brief Peek the first element (consumer side) * @return Pointer to the first element, or nullptr if empty @@ -240,6 +441,24 @@ class RingBuffer { return pop_front_n(std::forward(callback_functor), 1); } + /*! + * @brief Pop readable elements before a snapshot iterator + * @return Number of elements erased from the front + * @note Consumer-only. `pos` must come from this buffer's current readable range. + */ + size_t pop_front_until(const_iterator pos) { + if (pos.buffer_ != this) + return 0; + + const auto in = in_.load(std::memory_order::acquire); + const auto out = out_.load(std::memory_order::relaxed); + const auto count = pos.origin_ + pos.offset_ - out; + if (count > in - out) + return 0; + + return pop_front_n([](const T&) noexcept {}, count); + } + /*! * @brief Clear the buffer by consuming all elements * @return Number of elements that were erased