generated from wessel/boilerplate
Compare commits
22 Commits
2-imu-read
...
96d94861c1
| Author | SHA1 | Date | |
|---|---|---|---|
|
96d94861c1
|
|||
|
78712262fe
|
|||
|
bb14d770cf
|
|||
|
2155a913e7
|
|||
|
f45f410433
|
|||
| df9bafef0e | |||
|
45f5397d58
|
|||
|
881346b284
|
|||
|
4d8f84c094
|
|||
|
1a3d92158d
|
|||
|
9eba61eb6e
|
|||
|
ca2c08eb10
|
|||
|
2728baf8a5
|
|||
|
d06a33b90c
|
|||
|
aef18c1370
|
|||
|
6763e9d764
|
|||
|
085bb2f80b
|
|||
|
6525c6a1fb
|
|||
|
334af2da82
|
|||
|
c6175fe044
|
|||
|
40f8d7db5f
|
|||
| 7e3eb63199 |
119
doc/architecture/classes/Simulator.md
Normal file
119
doc/architecture/classes/Simulator.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Simulator (`assignments::three::Simulator`)
|
||||||
|
|
||||||
|
The `Simulator` class provides a flexible time-based value generation engine that supports multiple interpolation types. It is used by both the IMU and Wheel data simulator nodes to generate configurable sensor data patterns.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
**Namespace**: `assignments::three`
|
||||||
|
|
||||||
|
**Header**: `simulator/Simulator.hpp`
|
||||||
|
|
||||||
|
### Data Structures
|
||||||
|
|
||||||
|
**SimType Enum**
|
||||||
|
```cpp
|
||||||
|
enum class SimType {
|
||||||
|
CONSTANT, // y = c (constant value)
|
||||||
|
LINEAR, // y = y0 + (y1-y0) * (t-t0)/(t1-t0)
|
||||||
|
QUADRATIC // Lagrange interpolation through 3 points
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**IntervalConfig Struct**
|
||||||
|
```cpp
|
||||||
|
struct IntervalConfig {
|
||||||
|
SimType type; // Interpolation type
|
||||||
|
double t_start; // Interval start time
|
||||||
|
double t_end; // Interval end time
|
||||||
|
double y_start; // Start value
|
||||||
|
double y_end; // End value
|
||||||
|
double t_mid; // Mid-point time (quadratic only)
|
||||||
|
double y_mid; // Mid-point value (quadratic only)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Constructor
|
||||||
|
```cpp
|
||||||
|
Simulator(rclcpp::Node* node, const std::vector<std::string>& objects)
|
||||||
|
```
|
||||||
|
- Takes a ROS2 node pointer for parameter access
|
||||||
|
- Takes a list of object/channel names to configure
|
||||||
|
- Loads interval configurations from ROS2 parameters
|
||||||
|
- Validates intervals for overlaps (throws `std::runtime_error` if detected)
|
||||||
|
|
||||||
|
## Core Functionality
|
||||||
|
|
||||||
|
**`double get_object_value(const std::string& object, double t)`**
|
||||||
|
- Returns the simulated value for a given object at time `t`
|
||||||
|
- If `t` is within an interval, computes the interpolated value
|
||||||
|
- If `t` is after all intervals, holds the last interval's end value
|
||||||
|
- If `t` is before all intervals, returns 0.0
|
||||||
|
- If object doesn't exist, returns 0.0
|
||||||
|
|
||||||
|
**`double compute_value(double t, const IntervalConfig& interval)`** (private)
|
||||||
|
- Computes the interpolated value based on interval type:
|
||||||
|
- **CONSTANT**: Returns `y_start`
|
||||||
|
- **LINEAR**: Lagrange interpolation between 2 points
|
||||||
|
- **QUADRATIC**: Lagrange interpolation through 3 points
|
||||||
|
|
||||||
|
**`void load_intervals(rclcpp::Node* node, const std::vector<std::string>& objects)`** (private)
|
||||||
|
- Declares and loads parameters for each object
|
||||||
|
- Validates that intervals don't overlap
|
||||||
|
- Respects `max_intervals` limit
|
||||||
|
|
||||||
|
## Parameter Configuration
|
||||||
|
|
||||||
|
For each object, the following parameters are used:
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `max_intervals` | int | Global maximum intervals per object |
|
||||||
|
| `<object>.num_intervals` | int | Number of intervals for this object |
|
||||||
|
| `<object>.interval_<n>.type` | string | "constant", "linear", or "quadratic" |
|
||||||
|
| `<object>.interval_<n>.t_start` | double | Interval start time |
|
||||||
|
| `<object>.interval_<n>.t_end` | double | Interval end time |
|
||||||
|
| `<object>.interval_<n>.y_start` | double | Value at start |
|
||||||
|
| `<object>.interval_<n>.y_end` | double | Value at end |
|
||||||
|
| `<object>.interval_<n>.t_mid` | double | Mid-point time (quadratic) |
|
||||||
|
| `<object>.interval_<n>.y_mid` | double | Mid-point value (quadratic) |
|
||||||
|
|
||||||
|
## Example Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Constant acceleration of 5.0 m/s² from t=0 to t=10
|
||||||
|
linear_x:
|
||||||
|
num_intervals: 1
|
||||||
|
interval_0:
|
||||||
|
type: "constant"
|
||||||
|
t_start: 0.0
|
||||||
|
t_end: 10.0
|
||||||
|
y_start: 5.0
|
||||||
|
|
||||||
|
# Linear ramp from 0 to 10 over 5 seconds
|
||||||
|
wheel_fl:
|
||||||
|
num_intervals: 1
|
||||||
|
interval_0:
|
||||||
|
type: "linear"
|
||||||
|
t_start: 0.0
|
||||||
|
t_end: 5.0
|
||||||
|
y_start: 0.0
|
||||||
|
y_end: 10.0
|
||||||
|
|
||||||
|
# Quadratic curve peaking at t=5
|
||||||
|
angular_z:
|
||||||
|
num_intervals: 1
|
||||||
|
interval_0:
|
||||||
|
type: "quadratic"
|
||||||
|
t_start: 0.0
|
||||||
|
t_end: 10.0
|
||||||
|
y_start: 0.0
|
||||||
|
y_end: 0.0
|
||||||
|
t_mid: 5.0
|
||||||
|
y_mid: 3.14
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Throws `std::runtime_error` if overlapping intervals are detected for the same object
|
||||||
|
- Logs warning for unknown interval types, defaults to CONSTANT
|
||||||
|
- Returns 0.0 for non-existent objects (graceful degradation)
|
||||||
41
doc/architecture/nodes/DatabaseHandlerNode.md
Normal file
41
doc/architecture/nodes/DatabaseHandlerNode.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# DatabaseHandlerNode (`assignments::three::g2_2025_database_node`)
|
||||||
|
|
||||||
|
The `DatabaseHandlerNode` subscribes to position and velocity topics and stores the data in a
|
||||||
|
PostgreSQL database via the `DatabaseManager`.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `save_position_data` | bool | true | Enable storage of position |
|
||||||
|
| `save_velocity_data` | bool | true | Enable storage of velocity |
|
||||||
|
|
||||||
|
**Constructor**
|
||||||
|
```cpp
|
||||||
|
DatabaseHandlerNode()
|
||||||
|
```
|
||||||
|
- Initializes ROS2 node with name `database_handler_node`
|
||||||
|
- Creates subscriptions to `estimated_position` and/or `estimated_velocity` based on parameter configuration
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
**`void position_callback(const geometry_msgs::msg::Pose2D::SharedPtr msg)`**
|
||||||
|
- Primary callback invoked whenever a position message is received
|
||||||
|
- Forwards: x, y, and theta to `DatabaseManager::store_position_data`
|
||||||
|
- Throttled warning (5 second interval) on storage failures
|
||||||
|
|
||||||
|
**`void velocity_callback(const geometry_msgs::msg::Twist::SharedPtr msg)`**
|
||||||
|
- Primary callback invoked whenever a velocity message is received
|
||||||
|
- Forwards: linear velocity (x, y, z) and angular velocity (z) to `DatabaseManager::store_velocity_data`
|
||||||
|
- Throttled warning (5 second interval) on storage failures
|
||||||
|
|
||||||
|
## ROS2 Interface
|
||||||
|
|
||||||
|
**Subscriptions**
|
||||||
|
- `estimated_position` (geometry_msgs/msg/Pose2D)
|
||||||
|
- Receives calculated position estimates (only subscribed if `save_position_data=true`)
|
||||||
|
|
||||||
|
- `estimated_velocity` (geometry_msgs/msg/Twist)
|
||||||
|
- Receives calculated velocity estimates (only subscribed if `save_velocity_data=true`)
|
||||||
64
doc/architecture/nodes/IMUDataSimulator.md
Normal file
64
doc/architecture/nodes/IMUDataSimulator.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# IMUDataSimulator (`assignments::three::data_simulator_node`)
|
||||||
|
|
||||||
|
The `IMUDataSimulator` node generates simulated IMU sensor data (`sensor_msgs/msg/Imu`) based on configurable time-varying intervals. It publishes linear acceleration and angular velocity values that can follow constant, linear, or quadratic trajectories over time.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `publish_rate` | double | 10.0 | Publishing frequency in Hz |
|
||||||
|
| `max_intervals` | int | 4 | Maximum number of intervals per axis |
|
||||||
|
| `<axis>.num_intervals` | int | 0 | Number of intervals for each axis |
|
||||||
|
| `<axis>.interval_<n>.*` | various | - | Interval configuration (see Simulator) |
|
||||||
|
|
||||||
|
**Axes Configured:**
|
||||||
|
- `linear_x`, `linear_y`, `linear_z` - Linear acceleration axes
|
||||||
|
- `angular_x`, `angular_y`, `angular_z` - Angular velocity axes
|
||||||
|
|
||||||
|
**Constructor**
|
||||||
|
```cpp
|
||||||
|
DataSimulator()
|
||||||
|
```
|
||||||
|
- Initializes ROS2 node with name `imu_data_simulator`
|
||||||
|
- Creates `Simulator` instance for value generation
|
||||||
|
- Creates publisher for `simulated_imu_data` topic
|
||||||
|
- Sets up timer for periodic publishing
|
||||||
|
|
||||||
|
## Core Functionality
|
||||||
|
|
||||||
|
**`void publish_imu_data()`**
|
||||||
|
- Timer callback invoked at the configured publish rate
|
||||||
|
- Calculates elapsed time since node start
|
||||||
|
- Queries `Simulator` for current values of all 6 axes
|
||||||
|
- Populates IMU message with:
|
||||||
|
- Header timestamp and frame_id (`imu_link`)
|
||||||
|
- Linear acceleration (x, y, z)
|
||||||
|
- Angular velocity (x, y, z)
|
||||||
|
- Publishes message to topic
|
||||||
|
|
||||||
|
## ROS2 Interface
|
||||||
|
|
||||||
|
**Publications**
|
||||||
|
- `simulated_imu_data` (sensor_msgs/msg/Imu)
|
||||||
|
- Publishes simulated IMU data at configured rate
|
||||||
|
- Frame ID: `imu_link`
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ros2 run g2_2025_odometry_pkg imu_data_simulator_node --ros-args \
|
||||||
|
-p publish_rate:=20.0 \
|
||||||
|
-p linear_x.num_intervals:=1 \
|
||||||
|
-p linear_x.interval_0.type:=constant \
|
||||||
|
-p linear_x.interval_0.t_start:=0.0 \
|
||||||
|
-p linear_x.interval_0.t_end:=10.0 \
|
||||||
|
-p linear_x.interval_0.y_start:=9.81
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `rclcpp` - ROS2 C++ client library
|
||||||
|
- `sensor_msgs` - Standard sensor message types
|
||||||
|
- `Simulator` - Internal simulation engine
|
||||||
58
doc/architecture/nodes/IMUPositionApproximator.md
Normal file
58
doc/architecture/nodes/IMUPositionApproximator.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# IMUPositionApproximator (`assignments::three::g2_2025_imu_position_approximator_node`)
|
||||||
|
|
||||||
|
The `IMUPositionApproximator` node calculates position and velocity estimates by integrating
|
||||||
|
acceleration data from an IMU sensor. It transforms sensor data from the robot frame to the
|
||||||
|
map frame, accounting for rotations and centripetal effects, and provides a position reset
|
||||||
|
topic.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `initial_x` | double | 0.0 | Initial x position in map frame |
|
||||||
|
| `initial_y` | double | 0.0 | Initial y position in map frame |
|
||||||
|
| `initial_z` | double | 0.0 | Initial z position in map frame |
|
||||||
|
| `initial_theta` | double | 0.0 | Initial orientation angle (radians) |
|
||||||
|
| `imu_topic` | string | "imu_data" | Topic name for IMU sensor data |
|
||||||
|
|
||||||
|
**Constructor**
|
||||||
|
```cpp
|
||||||
|
IMUPositionApproximator()
|
||||||
|
```
|
||||||
|
- Initializes ROS2 node with name `imu_position_approximator`
|
||||||
|
- Creates subscription to IMU topic (configurable) and `position_reset` topic
|
||||||
|
- Creates publishers for `estimated_position` and `estimated_velocity`
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
**`void imu_callback(const sensor_msgs::msg::Imu::SharedPtr msg)`**
|
||||||
|
- Primary callback invoked whenever an IMU message is received
|
||||||
|
- Calculates time delta `dt` between measurements; skips invalid deltas (≤0 or >1 second)
|
||||||
|
- Updates orientation: `theta += omega_z * dt` and normalizes to [-pi, pi]
|
||||||
|
- Applies gravity compensation: subtracts 9.81 m/s^2 from z-axis acceleration
|
||||||
|
- Transforms acceleration from robot frame to map frame using rotation matrix
|
||||||
|
- Corrects for centripetal acceleration due to rotation
|
||||||
|
- Integrates acceleration to update velocity, then integrates velocity to update position
|
||||||
|
- Publishes `estimated_position` (Pose2D) and `estimated_velocity` (Twist)
|
||||||
|
|
||||||
|
**`void position_reset_callback(const geometry_msgs::msg::Pose2D::SharedPtr msg)`**
|
||||||
|
- Updates position (x, y) and orientation (theta) to provided values
|
||||||
|
- Resets all velocities (vx, vy, vz) to zero
|
||||||
|
|
||||||
|
## ROS2 Interface
|
||||||
|
|
||||||
|
**Subscribers**
|
||||||
|
- `imu_data` (sensor_msgs/msg/Imu) [configurable via `imu_topic` parameter]
|
||||||
|
- Receives raw IMU sensor data with linear acceleration and angular velocity
|
||||||
|
|
||||||
|
- `position_reset` (geometry_msgs/msg/Pose2D)
|
||||||
|
- Receives position reset commands from external position determinator
|
||||||
|
|
||||||
|
**Publishers**
|
||||||
|
- `estimated_position` (geometry_msgs/msg/Pose2D)
|
||||||
|
- Publishes calculated position (x, y, theta) in map frame
|
||||||
|
|
||||||
|
- `estimated_velocity` (geometry_msgs/msg/Twist)
|
||||||
|
- Publishes calculated linear velocities (x, y, z) and angular velocity (z)
|
||||||
64
doc/architecture/nodes/WheelDataSimulator.md
Normal file
64
doc/architecture/nodes/WheelDataSimulator.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# WheelDataSimulator (`assignments::three::wheel_data_simulator_node`)
|
||||||
|
|
||||||
|
The `WheelDataSimulator` node generates simulated wheel encoder/velocity data (`std_msgs/msg/Float64MultiArray`) based on configurable time-varying intervals. It publishes wheel values for a 4-wheel robot configuration that can follow constant, linear, or quadratic trajectories over time.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `publish_rate` | double | 10.0 | Publishing frequency in Hz |
|
||||||
|
| `max_intervals` | int | 4 | Maximum number of intervals per wheel |
|
||||||
|
| `<wheel>.num_intervals` | int | 0 | Number of intervals for each wheel |
|
||||||
|
| `<wheel>.interval_<n>.*` | various | - | Interval configuration (see Simulator) |
|
||||||
|
|
||||||
|
**Wheels Configured:**
|
||||||
|
- `wheel_fl` - Front Left wheel
|
||||||
|
- `wheel_fr` - Front Right wheel
|
||||||
|
- `wheel_rl` - Rear Left wheel
|
||||||
|
- `wheel_rr` - Rear Right wheel
|
||||||
|
|
||||||
|
**Constructor**
|
||||||
|
```cpp
|
||||||
|
DataSimulator()
|
||||||
|
```
|
||||||
|
- Initializes ROS2 node with name `wheel_data_simulator`
|
||||||
|
- Creates `Simulator` instance for value generation
|
||||||
|
- Creates publisher for `simulated_wheel_data` topic
|
||||||
|
- Sets up timer for periodic publishing
|
||||||
|
|
||||||
|
## Core Functionality
|
||||||
|
|
||||||
|
**`void publish_wheel_data()`**
|
||||||
|
- Timer callback invoked at the configured publish rate
|
||||||
|
- Calculates elapsed time since node start
|
||||||
|
- Queries `Simulator` for current values of all 4 wheels
|
||||||
|
- Populates Float64MultiArray message with wheel values in order: [FL, FR, RL, RR]
|
||||||
|
- Publishes message to topic
|
||||||
|
|
||||||
|
## ROS2 Interface
|
||||||
|
|
||||||
|
**Publications**
|
||||||
|
- `simulated_wheel_data` (std_msgs/msg/Float64MultiArray)
|
||||||
|
- Publishes simulated wheel data at configured rate
|
||||||
|
- Array contains 4 values: [front_left, front_right, rear_left, rear_right]
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ros2 run g2_2025_odometry_pkg wheel_data_simulator_node --ros-args \
|
||||||
|
-p publish_rate:=50.0 \
|
||||||
|
-p wheel_fl.num_intervals:=1 \
|
||||||
|
-p wheel_fl.interval_0.type:=linear \
|
||||||
|
-p wheel_fl.interval_0.t_start:=0.0 \
|
||||||
|
-p wheel_fl.interval_0.t_end:=10.0 \
|
||||||
|
-p wheel_fl.interval_0.y_start:=0.0 \
|
||||||
|
-p wheel_fl.interval_0.y_end:=5.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `rclcpp` - ROS2 C++ client library
|
||||||
|
- `std_msgs` - Standard message types
|
||||||
|
- `Simulator` - Internal simulation engine
|
||||||
67
doc/tests/IMUDataSimulator.md
Normal file
67
doc/tests/IMUDataSimulator.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# IMU Data Simulator Unit Tests
|
||||||
|
|
||||||
|
Unit tests for the `DataSimulator` (IMU) node are implemented in `src/g2_2025_odometry_pkg/test/test_imu_simulator.cpp` using Google Test and ROS2 test utilities. The tests validate node initialization, message publishing, and data correctness.
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
### 1. NodeInitialization
|
||||||
|
|
||||||
|
**Description:** Verifies that the DataSimulator node can be created without errors.
|
||||||
|
|
||||||
|
- **Test Action:** Create DataSimulator instance
|
||||||
|
- **Expected Result:** No exceptions thrown during construction
|
||||||
|
|
||||||
|
### 2. MessagePublishing
|
||||||
|
|
||||||
|
**Description:** Tests that IMU messages are published on the correct topic.
|
||||||
|
|
||||||
|
- **Test Action:**
|
||||||
|
- Create subscription to `simulated_imu_data` topic
|
||||||
|
- Create DataSimulator node
|
||||||
|
- Spin both nodes for a short period
|
||||||
|
- **Expected Result:** At least one `sensor_msgs/msg/Imu` message is received
|
||||||
|
|
||||||
|
### 3. MessageFrameId
|
||||||
|
|
||||||
|
**Description:** Verifies that published IMU messages have the correct frame_id.
|
||||||
|
|
||||||
|
- **Test Action:**
|
||||||
|
- Subscribe to `simulated_imu_data`
|
||||||
|
- Receive a message
|
||||||
|
- **Expected Result:** `header.frame_id` equals `"imu_link"`
|
||||||
|
|
||||||
|
### 4. LinearAccelerationValues
|
||||||
|
|
||||||
|
**Description:** Tests that linear acceleration values are valid (finite numbers).
|
||||||
|
|
||||||
|
- **Test Action:**
|
||||||
|
- Subscribe and receive IMU message
|
||||||
|
- Check linear_acceleration fields
|
||||||
|
- **Expected Result:**
|
||||||
|
- `linear_acceleration.x` is finite
|
||||||
|
- `linear_acceleration.y` is finite
|
||||||
|
- `linear_acceleration.z` is finite
|
||||||
|
|
||||||
|
### 5. AngularVelocityValues
|
||||||
|
|
||||||
|
**Description:** Tests that angular velocity values are valid (finite numbers).
|
||||||
|
|
||||||
|
- **Test Action:**
|
||||||
|
- Subscribe and receive IMU message
|
||||||
|
- Check angular_velocity fields
|
||||||
|
- **Expected Result:**
|
||||||
|
- `angular_velocity.x` is finite
|
||||||
|
- `angular_velocity.y` is finite
|
||||||
|
- `angular_velocity.z` is finite
|
||||||
|
|
||||||
|
## Test Infrastructure
|
||||||
|
|
||||||
|
The test class `IMUSimulatorTest` provides:
|
||||||
|
- Static `SetUpTestSuite()` and `TearDownTestSuite()` for ROS2 init/shutdown
|
||||||
|
- Helper method `setupBasicIMUParams()` for setting up default test parameters
|
||||||
|
|
||||||
|
## ROS2 Topics Used
|
||||||
|
|
||||||
|
| Topic | Message Type | Direction |
|
||||||
|
|-------|--------------|-----------|
|
||||||
|
| `simulated_imu_data` | sensor_msgs/msg/Imu | Published by node under test |
|
||||||
98
doc/tests/Simulator.md
Normal file
98
doc/tests/Simulator.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Simulator Unit Tests
|
||||||
|
|
||||||
|
Unit tests for the `Simulator` class are implemented in `src/g2_2025_odometry_pkg/test/test_simulator.cpp` using Google Test and ROS2 test utilities. The tests validate interval-based value generation with different interpolation types.
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
### 1. ConstantInterval
|
||||||
|
|
||||||
|
**Description:** Verifies constant interpolation returns the same value throughout the interval.
|
||||||
|
|
||||||
|
- **Test Configuration:**
|
||||||
|
- Single interval: t=[0, 10], y_start=5.0
|
||||||
|
- Type: constant
|
||||||
|
- **Expected Result:**
|
||||||
|
- Value is 5.0 at t=0, t=5, t=10
|
||||||
|
- Value holds at 5.0 after interval (t=15)
|
||||||
|
|
||||||
|
### 2. LinearInterval
|
||||||
|
|
||||||
|
**Description:** Tests linear interpolation between start and end values.
|
||||||
|
|
||||||
|
- **Test Configuration:**
|
||||||
|
- Single interval: t=[0, 10], y_start=0.0, y_end=10.0
|
||||||
|
- Type: linear
|
||||||
|
- **Expected Result:**
|
||||||
|
- Value is 0.0 at t=0
|
||||||
|
- Value is 5.0 at t=5
|
||||||
|
- Value is 10.0 at t=10
|
||||||
|
- Value holds at 10.0 after interval (t=15)
|
||||||
|
|
||||||
|
### 3. QuadraticInterval
|
||||||
|
|
||||||
|
**Description:** Tests quadratic (Lagrange) interpolation through 3 points.
|
||||||
|
|
||||||
|
- **Test Configuration:**
|
||||||
|
- Single interval: t=[0, 10], y_start=0.0, y_end=0.0, t_mid=5.0, y_mid=10.0
|
||||||
|
- Type: quadratic
|
||||||
|
- **Expected Result:**
|
||||||
|
- Value is 0.0 at t=0
|
||||||
|
- Value is 10.0 at t=5 (peak)
|
||||||
|
- Value is 0.0 at t=10
|
||||||
|
- Value holds at 0.0 after interval (t=15)
|
||||||
|
|
||||||
|
### 4. MultipleIntervals
|
||||||
|
|
||||||
|
**Description:** Tests behavior with multiple non-overlapping intervals and gaps.
|
||||||
|
|
||||||
|
- **Test Configuration:**
|
||||||
|
- Interval 0: t=[0, 5], constant 5.0
|
||||||
|
- Interval 1: t=[10, 15], constant 10.0
|
||||||
|
- Gap between t=5 and t=10
|
||||||
|
- **Expected Result:**
|
||||||
|
- Value is 5.0 at t=2.5 (first interval)
|
||||||
|
- Value holds at 5.0 in gap (t=7.5)
|
||||||
|
- Value is 10.0 at t=12.5 (second interval)
|
||||||
|
- Value holds at 10.0 after all intervals (t=20)
|
||||||
|
|
||||||
|
### 5. ValueBeforeIntervals
|
||||||
|
|
||||||
|
**Description:** Tests behavior when querying time before any interval starts.
|
||||||
|
|
||||||
|
- **Test Configuration:**
|
||||||
|
- Single interval: t=[10, 20]
|
||||||
|
- **Expected Result:**
|
||||||
|
- Value is 0.0 at t=5 (before interval starts)
|
||||||
|
|
||||||
|
### 6. NonExistentChannel
|
||||||
|
|
||||||
|
**Description:** Tests graceful handling of queries for unconfigured channels.
|
||||||
|
|
||||||
|
- **Test Action:** Query `get_object_value("nonexistent_channel", 5.0)`
|
||||||
|
- **Expected Result:** Returns 0.0
|
||||||
|
|
||||||
|
### 7. OverlappingIntervalsThrowsException
|
||||||
|
|
||||||
|
**Description:** Verifies that overlapping intervals are detected and rejected.
|
||||||
|
|
||||||
|
- **Test Configuration:**
|
||||||
|
- Interval 0: t=[0, 10]
|
||||||
|
- Interval 1: t=[5, 15] (overlaps)
|
||||||
|
- **Expected Result:** Constructor throws `std::runtime_error`
|
||||||
|
|
||||||
|
### 8. MaxIntervalsLimit
|
||||||
|
|
||||||
|
**Description:** Tests that `max_intervals` parameter limits loaded intervals.
|
||||||
|
|
||||||
|
- **Test Configuration:**
|
||||||
|
- max_intervals=2
|
||||||
|
- num_intervals=3 (defines 3 intervals)
|
||||||
|
- **Expected Result:**
|
||||||
|
- Only first 2 intervals are loaded
|
||||||
|
- Third interval values default to holding second interval's end value
|
||||||
|
|
||||||
|
## Test Infrastructure
|
||||||
|
|
||||||
|
The test class `SimulatorTest` provides:
|
||||||
|
- Static `SetUpTestSuite()` and `TearDownTestSuite()` for ROS2 init/shutdown
|
||||||
|
- Helper method `createNodeWithParams()` for creating nodes with parameter overrides
|
||||||
73
doc/tests/WheelDataSimulator.md
Normal file
73
doc/tests/WheelDataSimulator.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Wheel Data Simulator Unit Tests
|
||||||
|
|
||||||
|
Unit tests for the `DataSimulator` (Wheel) node are implemented in `src/g2_2025_odometry_pkg/test/test_wheel_simulator.cpp` using Google Test and ROS2 test utilities. The tests validate node initialization, message publishing, array structure, and data correctness.
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
### 1. NodeInitialization
|
||||||
|
|
||||||
|
**Description:** Verifies that the Wheel DataSimulator node can be created without errors.
|
||||||
|
|
||||||
|
- **Test Action:** Create DataSimulator instance
|
||||||
|
- **Expected Result:** No exceptions thrown during construction
|
||||||
|
|
||||||
|
### 2. WheelDataPublishing
|
||||||
|
|
||||||
|
**Description:** Tests that wheel data messages are published on the correct topic.
|
||||||
|
|
||||||
|
- **Test Action:**
|
||||||
|
- Create subscription to `simulated_wheel_data` topic
|
||||||
|
- Create DataSimulator node
|
||||||
|
- Spin both nodes for a short period
|
||||||
|
- **Expected Result:** At least one `std_msgs/msg/Float64MultiArray` message is received
|
||||||
|
|
||||||
|
### 3. WheelDataArraySize
|
||||||
|
|
||||||
|
**Description:** Verifies that the published array contains the correct number of wheel values.
|
||||||
|
|
||||||
|
- **Test Action:**
|
||||||
|
- Subscribe to `simulated_wheel_data`
|
||||||
|
- Receive a message
|
||||||
|
- Check array size
|
||||||
|
- **Expected Result:** `data.size()` equals 4 (FL, FR, RL, RR)
|
||||||
|
|
||||||
|
### 4. ValidVelocityValues
|
||||||
|
|
||||||
|
**Description:** Tests that all wheel velocity values are valid (finite numbers).
|
||||||
|
|
||||||
|
- **Test Action:**
|
||||||
|
- Subscribe and receive wheel data message
|
||||||
|
- Check each value in the array
|
||||||
|
- **Expected Result:** All 4 values are finite numbers
|
||||||
|
|
||||||
|
### 5. MultipleMessagesReceived
|
||||||
|
|
||||||
|
**Description:** Tests that multiple messages are published over time.
|
||||||
|
|
||||||
|
- **Test Action:**
|
||||||
|
- Subscribe and count received messages
|
||||||
|
- Spin for a short duration
|
||||||
|
- **Expected Result:** Message count is greater than 0
|
||||||
|
|
||||||
|
## Test Infrastructure
|
||||||
|
|
||||||
|
The test class `WheelSimulatorTest` provides:
|
||||||
|
- Static `SetUpTestSuite()` and `TearDownTestSuite()` for ROS2 init/shutdown
|
||||||
|
- Helper method `setupBasicWheelParams()` for setting up default test parameters
|
||||||
|
|
||||||
|
## ROS2 Topics Used
|
||||||
|
|
||||||
|
| Topic | Message Type | Direction |
|
||||||
|
|-------|--------------|-----------|
|
||||||
|
| `simulated_wheel_data` | std_msgs/msg/Float64MultiArray | Published by node under test |
|
||||||
|
|
||||||
|
## Data Array Format
|
||||||
|
|
||||||
|
The published Float64MultiArray contains wheel values in the following order:
|
||||||
|
|
||||||
|
| Index | Wheel |
|
||||||
|
|-------|-------|
|
||||||
|
| 0 | Front Left (FL) |
|
||||||
|
| 1 | Front Right (FR) |
|
||||||
|
| 2 | Rear Left (RL) |
|
||||||
|
| 3 | Rear Right (RR) |
|
||||||
134
src/g2_2025_odometry_pkg/CMakeLists.txt
Normal file
134
src/g2_2025_odometry_pkg/CMakeLists.txt
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.8)
|
||||||
|
project(g2_2025_odometry_pkg)
|
||||||
|
|
||||||
|
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(sensor_msgs REQUIRED)
|
||||||
|
find_package(geometry_msgs REQUIRED)
|
||||||
|
|
||||||
|
# Add executable for position_speed_approximator_node
|
||||||
|
add_executable(position_speed_approximator_node
|
||||||
|
src/g2_2025_imu_position_approximator_node/nodes/IMUPositionApproximator.cpp
|
||||||
|
src/g2_2025_imu_position_approximator_node/Main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(position_speed_approximator_node PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/g2_2025_imu_position_approximator_node
|
||||||
|
)
|
||||||
|
|
||||||
|
ament_target_dependencies(position_speed_approximator_node
|
||||||
|
rclcpp
|
||||||
|
sensor_msgs
|
||||||
|
geometry_msgs
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(database_handler_node
|
||||||
|
src/g2_2025_database_node/Main.cpp
|
||||||
|
src/g2_2025_database_node/nodes/DatabaseHandlerNode.cpp
|
||||||
|
src/database/DatabaseManager.cpp
|
||||||
|
src/config/ConfigManager.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(database_handler_node PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/g2_2025_database_node
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(database_handler_node tomlplusplus::tomlplusplus pqxx pq)
|
||||||
|
|
||||||
|
ament_target_dependencies(database_handler_node
|
||||||
|
rclcpp
|
||||||
|
sensor_msgs
|
||||||
|
geometry_msgs
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS
|
||||||
|
position_speed_approximator_node
|
||||||
|
database_handler_node
|
||||||
|
DESTINATION lib/${PROJECT_NAME}
|
||||||
|
)
|
||||||
|
find_package(rclcpp REQUIRED)
|
||||||
|
find_package(sensor_msgs REQUIRED)
|
||||||
|
find_package(geometry_msgs REQUIRED)
|
||||||
|
find_package(std_msgs REQUIRED)
|
||||||
|
|
||||||
|
add_executable(imu_data_simulator_node
|
||||||
|
src/g2_2025_imu_data_simulator_node/nodes/ImuDataSimulator.cpp
|
||||||
|
src/g2_2025_imu_data_simulator_node/main.cpp
|
||||||
|
src/simulator/Simulator.cpp)
|
||||||
|
|
||||||
|
target_include_directories(imu_data_simulator_node PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/g2_2025_imu_data_simulator_node
|
||||||
|
)
|
||||||
|
|
||||||
|
ament_target_dependencies(imu_data_simulator_node rclcpp sensor_msgs geometry_msgs)
|
||||||
|
|
||||||
|
add_executable(wheel_data_simulator_node
|
||||||
|
src/g2_2025_wheel_data_simulator_node/nodes/WheelDataSimulator.cpp
|
||||||
|
src/g2_2025_wheel_data_simulator_node/main.cpp
|
||||||
|
src/simulator/Simulator.cpp)
|
||||||
|
|
||||||
|
target_include_directories(wheel_data_simulator_node PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/g2_2025_wheel_data_simulator_node
|
||||||
|
)
|
||||||
|
|
||||||
|
ament_target_dependencies(wheel_data_simulator_node rclcpp std_msgs)
|
||||||
|
|
||||||
|
install(TARGETS
|
||||||
|
imu_data_simulator_node
|
||||||
|
wheel_data_simulator_node
|
||||||
|
DESTINATION lib/${PROJECT_NAME}
|
||||||
|
)
|
||||||
|
|
||||||
|
install(DIRECTORY launch config
|
||||||
|
DESTINATION share/${PROJECT_NAME}/
|
||||||
|
)
|
||||||
|
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
find_package(ament_cmake_gtest REQUIRED)
|
||||||
|
|
||||||
|
# Simulator unit tests
|
||||||
|
ament_add_gtest(test_simulator test/TestSimulator.cpp src/simulator/Simulator.cpp)
|
||||||
|
target_include_directories(test_simulator PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||||
|
ament_target_dependencies(test_simulator rclcpp)
|
||||||
|
|
||||||
|
# IMU simulator integration tests
|
||||||
|
ament_add_gtest(test_imu_simulator test/TestImuSimulator.cpp
|
||||||
|
src/g2_2025_imu_data_simulator_node/nodes/ImuDataSimulator.cpp
|
||||||
|
src/simulator/Simulator.cpp)
|
||||||
|
target_include_directories(test_imu_simulator PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/g2_2025_imu_data_simulator_node)
|
||||||
|
ament_target_dependencies(test_imu_simulator rclcpp sensor_msgs geometry_msgs)
|
||||||
|
|
||||||
|
# Wheel simulator integration tests
|
||||||
|
ament_add_gtest(test_wheel_simulator test/TestWheelSimulator.cpp
|
||||||
|
src/g2_2025_wheel_data_simulator_node/nodes/WheelDataSimulator.cpp
|
||||||
|
src/simulator/Simulator.cpp)
|
||||||
|
target_include_directories(test_wheel_simulator PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/g2_2025_wheel_data_simulator_node)
|
||||||
|
ament_target_dependencies(test_wheel_simulator rclcpp std_msgs)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
ament_package()
|
||||||
74
src/g2_2025_odometry_pkg/config/opt.yaml
Normal file
74
src/g2_2025_odometry_pkg/config/opt.yaml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
imu_data_simulator:
|
||||||
|
ros__parameters:
|
||||||
|
max_intervals: 4
|
||||||
|
publish_rate: 10.0
|
||||||
|
linear_x:
|
||||||
|
num_intervals: 4
|
||||||
|
interval_0:
|
||||||
|
type: "linear"
|
||||||
|
t_start: 0.0
|
||||||
|
t_end: 5.0
|
||||||
|
y_start: 0.0
|
||||||
|
y_end: 9.81
|
||||||
|
interval_1:
|
||||||
|
type: "linear"
|
||||||
|
t_start: 7.5
|
||||||
|
t_end: 12.5
|
||||||
|
y_start: 9.81
|
||||||
|
y_end: 0.0
|
||||||
|
interval_2:
|
||||||
|
type: "linear"
|
||||||
|
t_start: 15.0
|
||||||
|
t_end: 20.0
|
||||||
|
y_start: 0.0
|
||||||
|
y_end: -9.81
|
||||||
|
interval_3:
|
||||||
|
type: "linear"
|
||||||
|
t_start: 22.5
|
||||||
|
t_end: 25.0
|
||||||
|
y_start: -9.81
|
||||||
|
y_end: 0.0
|
||||||
|
angular_z:
|
||||||
|
num_intervals: 1
|
||||||
|
interval_0:
|
||||||
|
type: "constant"
|
||||||
|
t_start: 2.0
|
||||||
|
y_start: 1.57
|
||||||
|
angular_x:
|
||||||
|
num_intervals: 2
|
||||||
|
interval_0:
|
||||||
|
type: "quadratic"
|
||||||
|
t_start: 3.0
|
||||||
|
y_start: 0.0
|
||||||
|
t_mid: 4.5
|
||||||
|
y_mid: 1.57
|
||||||
|
t_end: 6.0
|
||||||
|
y_end: 0.0
|
||||||
|
interval_1:
|
||||||
|
type: "quadratic"
|
||||||
|
t_start: 7.0
|
||||||
|
y_start: 0.0
|
||||||
|
t_mid: 8.5
|
||||||
|
y_mid: -1.57
|
||||||
|
t_end: 10.0
|
||||||
|
y_end: 0.0
|
||||||
|
|
||||||
|
wheel_data_simulator:
|
||||||
|
ros__parameters:
|
||||||
|
publish_rate: 10.0
|
||||||
|
wheel_fl:
|
||||||
|
num_intervals: 1
|
||||||
|
interval_0:
|
||||||
|
type: "linear"
|
||||||
|
t_start: 0.0
|
||||||
|
t_end: 10.0
|
||||||
|
y_start: 0.0
|
||||||
|
y_end: 5.0
|
||||||
|
wheel_fr:
|
||||||
|
num_intervals: 1
|
||||||
|
interval_0:
|
||||||
|
type: "linear"
|
||||||
|
t_start: 0.0
|
||||||
|
t_end: 10.0
|
||||||
|
y_start: 0.0
|
||||||
|
y_end: 10.0
|
||||||
8
src/g2_2025_odometry_pkg/launch/imu_sim.launch.xml
Normal file
8
src/g2_2025_odometry_pkg/launch/imu_sim.launch.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<launch>
|
||||||
|
<arg name="params_file" default="$(find-pkg-share g2_2025_odometry_pkg)/config/opt.yaml"/>
|
||||||
|
|
||||||
|
<node pkg="g2_2025_odometry_pkg" exec="imu_data_simulator_node" name="imu_data_simulator" output="screen">
|
||||||
|
<param from="$(var params_file)"/>
|
||||||
|
</node>
|
||||||
|
</launch>
|
||||||
8
src/g2_2025_odometry_pkg/launch/wheel_sim.launch.xml
Normal file
8
src/g2_2025_odometry_pkg/launch/wheel_sim.launch.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<launch>
|
||||||
|
<arg name="params_file" default="$(find-pkg-share g2_2025_odometry_pkg)/config/opt.yaml"/>
|
||||||
|
|
||||||
|
<node pkg="g2_2025_odometry_pkg" exec="wheel_data_simulator_node" name="wheel_data_simulator" output="screen">
|
||||||
|
<param from="$(var params_file)"/>
|
||||||
|
</node>
|
||||||
|
</launch>
|
||||||
23
src/g2_2025_odometry_pkg/package.xml
Normal file
23
src/g2_2025_odometry_pkg/package.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||||
|
<package format="3">
|
||||||
|
<name>g2_2025_odometry_pkg</name>
|
||||||
|
<version>0.0.0</version>
|
||||||
|
<description>TODO: Package description</description>
|
||||||
|
<maintainer email="contact@wessel.gg">wessel</maintainer>
|
||||||
|
<license>TODO: License declaration</license>
|
||||||
|
|
||||||
|
<buildtool_depend>ament_cmake</buildtool_depend>
|
||||||
|
|
||||||
|
<depend>rclcpp</depend>
|
||||||
|
<depend>sensor_msgs</depend>
|
||||||
|
<depend>geometry_msgs</depend>
|
||||||
|
|
||||||
|
<test_depend>ament_lint_auto</test_depend>
|
||||||
|
<test_depend>ament_lint_common</test_depend>
|
||||||
|
<test_depend>ament_cmake_gtest</test_depend>
|
||||||
|
|
||||||
|
<export>
|
||||||
|
<build_type>ament_cmake</build_type>
|
||||||
|
</export>
|
||||||
|
</package>
|
||||||
112
src/g2_2025_odometry_pkg/src/config/ConfigManager.cpp
Normal file
112
src/g2_2025_odometry_pkg/src/config/ConfigManager.cpp
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#include "ConfigManager.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
namespace assignments::three {
|
||||||
|
|
||||||
|
ConfigManager::ConfigManager(rclcpp::Logger logger)
|
||||||
|
: logger_(logger), loaded_(false)
|
||||||
|
{
|
||||||
|
// Try to auto-load config from default location
|
||||||
|
std::string config_path = find_config_file();
|
||||||
|
if (!config_path.empty()) {
|
||||||
|
load_config(config_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConfigManager::load_config(const std::string& config_file_path) {
|
||||||
|
try {
|
||||||
|
RCLCPP_INFO(logger_, "[CFG] '%s': loading configuration", config_file_path.c_str());
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(config_file_path)) {
|
||||||
|
RCLCPP_ERROR(logger_, "[CFG] '%s': file does not exist", config_file_path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
config_ = toml::parse_file(config_file_path);
|
||||||
|
loaded_ = true;
|
||||||
|
|
||||||
|
RCLCPP_INFO(logger_, "[CFG] '%s': configuration loaded", config_file_path.c_str());
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const toml::parse_error& e) {
|
||||||
|
RCLCPP_ERROR(logger_, "[CFG] '%s': failed to parse: %s", config_file_path.c_str(), e.what());
|
||||||
|
loaded_ = false;
|
||||||
|
return false;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
RCLCPP_ERROR(logger_, "[CFG] '%s': failed to load: %s", config_file_path.c_str(), e.what());
|
||||||
|
loaded_ = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<DatabaseConfig> ConfigManager::get_database_config() const {
|
||||||
|
if (!loaded_ || !config_.has_value()) {
|
||||||
|
RCLCPP_ERROR(logger_, "[CFG] database configuration not loaded");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return parse_database_config(config_.value());
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
RCLCPP_ERROR(logger_, "[CFG] database configuration failed to parse: %s", e.what());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConfigManager::is_loaded() const {
|
||||||
|
return loaded_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ConfigManager::find_config_file() const {
|
||||||
|
// Look for config file in several locations
|
||||||
|
for (const auto& path : default_config_paths_) {
|
||||||
|
if (std::filesystem::exists(path)) {
|
||||||
|
RCLCPP_INFO(logger_, "[CFG] '%s': found configuration file", path.c_str());
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RCLCPP_WARN(logger_, "[CFG] no configuration file found at default locations");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseConfig ConfigManager::parse_database_config(const toml::table& config) const {
|
||||||
|
DatabaseConfig db_config;
|
||||||
|
|
||||||
|
// Parse database section
|
||||||
|
auto database_section = config["database"];
|
||||||
|
if (!database_section) {
|
||||||
|
throw std::runtime_error("missing [database] section in configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
db_config.host = database_section["host"].value_or<std::string>("localhost");
|
||||||
|
db_config.port = database_section["port"].value_or<int>(5432);
|
||||||
|
db_config.dbname = database_section["dbname"].value_or<std::string>("imu_data");
|
||||||
|
db_config.user = database_section["user"].value_or<std::string>("postgres");
|
||||||
|
db_config.password = database_section["password"].value_or<std::string>("postgres");
|
||||||
|
db_config.timeout = database_section["timeout"].value_or<int>(30);
|
||||||
|
db_config.ssl = database_section["ssl"].value_or<bool>(false);
|
||||||
|
|
||||||
|
// Parse pool section if present
|
||||||
|
auto pool_section = database_section["pool"];
|
||||||
|
if (pool_section) {
|
||||||
|
db_config.min_connections = pool_section["min_connections"].value_or<int>(1);
|
||||||
|
db_config.max_connections = pool_section["max_connections"].value_or<int>(10);
|
||||||
|
} else {
|
||||||
|
db_config.min_connections = 1;
|
||||||
|
db_config.max_connections = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
RCLCPP_INFO(logger_, "[CFG] database config parsed - %s:%s@%s:%d",
|
||||||
|
db_config.user.c_str(),
|
||||||
|
db_config.dbname.c_str(),
|
||||||
|
db_config.host.c_str(),
|
||||||
|
db_config.port
|
||||||
|
);
|
||||||
|
|
||||||
|
return db_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace assignments::three
|
||||||
52
src/g2_2025_odometry_pkg/src/config/ConfigManager.hpp
Normal file
52
src/g2_2025_odometry_pkg/src/config/ConfigManager.hpp
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/* ConfigManager.hpp
|
||||||
|
* Configuration management for the exam result generator
|
||||||
|
* Uses toml++ library to parse TOML configuration files
|
||||||
|
*
|
||||||
|
* Reviewed by: <x>
|
||||||
|
* Changelog:
|
||||||
|
* [23-09-2025] Wessel T: Created configuration manager class for TOML config loading
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <toml++/toml.h>
|
||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
|
||||||
|
#include "DatabaseConfig.hpp"
|
||||||
|
|
||||||
|
namespace assignments::three {
|
||||||
|
|
||||||
|
class ConfigManager {
|
||||||
|
public:
|
||||||
|
explicit ConfigManager(rclcpp::Logger logger);
|
||||||
|
~ConfigManager() = default;
|
||||||
|
|
||||||
|
bool load_config(const std::string& config_file_path);
|
||||||
|
|
||||||
|
std::optional<DatabaseConfig> get_database_config() const;
|
||||||
|
|
||||||
|
bool is_loaded() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
rclcpp::Logger logger_;
|
||||||
|
|
||||||
|
std::optional<toml::table> config_;
|
||||||
|
|
||||||
|
bool loaded_ { false };
|
||||||
|
std::vector<std::string> default_config_paths_ = {
|
||||||
|
"config.toml",
|
||||||
|
"./src/config.toml",
|
||||||
|
"../config.toml",
|
||||||
|
"../../config.toml",
|
||||||
|
"../../../config.toml",
|
||||||
|
"../../../../config.toml",
|
||||||
|
"/etc/ros2_grade_calculator/config.toml"
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string find_config_file() const;
|
||||||
|
DatabaseConfig parse_database_config(const toml::table& config) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace assignments::three
|
||||||
46
src/g2_2025_odometry_pkg/src/config/DatabaseConfig.hpp
Normal file
46
src/g2_2025_odometry_pkg/src/config/DatabaseConfig.hpp
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/* DatabaseConfig.hpp
|
||||||
|
* Database configuration structure
|
||||||
|
*
|
||||||
|
* Reviewed by: <x>
|
||||||
|
* Changelog:
|
||||||
|
* [23-09-2025] Wessel T: Create initial configuration structure
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace assignments::three {
|
||||||
|
|
||||||
|
struct DatabaseConfig {
|
||||||
|
std::string host;
|
||||||
|
int port;
|
||||||
|
std::string dbname;
|
||||||
|
std::string user;
|
||||||
|
std::string password;
|
||||||
|
int timeout;
|
||||||
|
bool ssl;
|
||||||
|
|
||||||
|
// Pool settings
|
||||||
|
int min_connections;
|
||||||
|
int max_connections;
|
||||||
|
|
||||||
|
std::string to_connection_string() const {
|
||||||
|
std::string conn_str =
|
||||||
|
"host=" + host +
|
||||||
|
" port=" + std::to_string(port) +
|
||||||
|
" dbname=" + dbname +
|
||||||
|
" user=" + user +
|
||||||
|
" password=" + password +
|
||||||
|
" connect_timeout=" + std::to_string(timeout);
|
||||||
|
|
||||||
|
if (ssl) {
|
||||||
|
conn_str += " sslmode=require";
|
||||||
|
} else {
|
||||||
|
conn_str += " sslmode=disable";
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn_str;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace assignments::three
|
||||||
164
src/g2_2025_odometry_pkg/src/database/DatabaseManager.cpp
Normal file
164
src/g2_2025_odometry_pkg/src/database/DatabaseManager.cpp
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
#include "DatabaseManager.hpp"
|
||||||
|
|
||||||
|
#include <pqxx/pqxx>
|
||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
|
||||||
|
#include "SQLQueries.hpp"
|
||||||
|
#include "config/ConfigManager.hpp"
|
||||||
|
|
||||||
|
namespace assignments::three {
|
||||||
|
|
||||||
|
DatabaseManager::DatabaseManager(rclcpp::Logger logger) : logger_(logger) {
|
||||||
|
config_manager_ = std::make_unique<ConfigManager>(logger_);
|
||||||
|
init_database();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseManager::is_connected() const {
|
||||||
|
return conn_ && conn_->is_open();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseManager::connect(const std::string& connection_string) {
|
||||||
|
try {
|
||||||
|
RCLCPP_INFO(logger_, "[DBS] connecting to PostgreSQL database...");
|
||||||
|
|
||||||
|
conn_ = std::make_unique<pqxx::connection>(connection_string);
|
||||||
|
|
||||||
|
if (conn_->is_open()) {
|
||||||
|
RCLCPP_INFO(logger_, "[DBS] '%s': successfully connected to database", 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatabaseManager::create_tables() {
|
||||||
|
if (!conn_ || !conn_->is_open()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
pqxx::work txn(*conn_);
|
||||||
|
|
||||||
|
txn.exec(SQL_CREATE_IMU_DATA_TABLE);
|
||||||
|
txn.exec(SQL_CREATE_POSITION_DATA_TABLE);
|
||||||
|
txn.exec(SQL_CREATE_VELOCITY_DATA_TABLE);
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseManager::store_imu_data(
|
||||||
|
double linear_accel_x, double linear_accel_y, double linear_accel_z,
|
||||||
|
double angular_vel_x, double angular_vel_y, double angular_vel_z
|
||||||
|
) {
|
||||||
|
if (!is_connected()) {
|
||||||
|
RCLCPP_WARN(logger_, "[DBS] not connected to database");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pqxx::work txn(*conn_);
|
||||||
|
|
||||||
|
txn.exec_params(SQL_INSERT_IMU_DATA,
|
||||||
|
linear_accel_x, linear_accel_y, linear_accel_z,
|
||||||
|
angular_vel_x, angular_vel_y, angular_vel_z
|
||||||
|
);
|
||||||
|
|
||||||
|
txn.commit();
|
||||||
|
|
||||||
|
RCLCPP_DEBUG(logger_, "[DBS] stored IMU data: accel=[%.3f,%.3f,%.3f], angular=[%.3f,%.3f,%.3f]",
|
||||||
|
linear_accel_x, linear_accel_y, linear_accel_z,
|
||||||
|
angular_vel_x, angular_vel_y, angular_vel_z
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const pqxx::sql_error &e) {
|
||||||
|
RCLCPP_ERROR(logger_, "[DBS] failed to store IMU data: %s", e.what());
|
||||||
|
return false;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseManager::store_position_data(double x, double y, double theta) {
|
||||||
|
if (!is_connected()) {
|
||||||
|
RCLCPP_WARN(logger_, "[DBS] not connected to database");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pqxx::work txn(*conn_);
|
||||||
|
txn.exec_params(SQL_INSERT_POSITION_DATA, x, y, theta);
|
||||||
|
txn.commit();
|
||||||
|
|
||||||
|
RCLCPP_DEBUG(logger_, "[DBS] stored position data=[%.3f, %.3f, %.3f]", x, y, theta);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (const pqxx::sql_error &e) {
|
||||||
|
RCLCPP_ERROR(logger_, "[DBS] failed to store position data: %s", e.what());
|
||||||
|
return false;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseManager::store_velocity_data(double vx, double vy, double vz, double omega_z) {
|
||||||
|
if (!is_connected()) {
|
||||||
|
RCLCPP_WARN(logger_, "[DBS] not connected to database");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pqxx::work txn(*conn_);
|
||||||
|
txn.exec_params(SQL_INSERT_VELOCITY_DATA, vx, vy, vz, omega_z);
|
||||||
|
txn.commit();
|
||||||
|
|
||||||
|
RCLCPP_DEBUG(logger_, "[DBS] stored velocity data=[%.3f, %.3f, %.3f, %.3f]", vx, vy, vz, omega_z);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (const pqxx::sql_error &e) {
|
||||||
|
RCLCPP_ERROR(logger_, "[DBS] failed to store velocity data: %s", e.what());
|
||||||
|
return false;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
RCLCPP_ERROR(logger_, "[DBS] database error: %s", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace assignments::three
|
||||||
48
src/g2_2025_odometry_pkg/src/database/DatabaseManager.hpp
Normal file
48
src/g2_2025_odometry_pkg/src/database/DatabaseManager.hpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/* DatabaseManager.hpp
|
||||||
|
* Database manager for the IMU data collection system
|
||||||
|
*
|
||||||
|
* Reviewed by: <x>
|
||||||
|
* Changelog:
|
||||||
|
* [23-09-2025] Wessel T: Created database manager class for all DB operations
|
||||||
|
* [14-10-2025] Wessel T: Updated for IMU data storage functionality
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <pqxx/pqxx>
|
||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
|
||||||
|
#include "config/ConfigManager.hpp"
|
||||||
|
|
||||||
|
namespace assignments::three {
|
||||||
|
|
||||||
|
class DatabaseManager {
|
||||||
|
public:
|
||||||
|
explicit DatabaseManager(rclcpp::Logger logger);
|
||||||
|
~DatabaseManager() = default;
|
||||||
|
|
||||||
|
bool connect(const std::string& connection_string);
|
||||||
|
virtual bool is_connected() const;
|
||||||
|
|
||||||
|
// Table operations
|
||||||
|
virtual void init_database();
|
||||||
|
void create_tables();
|
||||||
|
|
||||||
|
// IMU Data operations
|
||||||
|
virtual bool store_position_data(double x, double y, double theta);
|
||||||
|
virtual bool store_velocity_data(double vx, double vy, double vz, double omega_z);
|
||||||
|
virtual bool store_imu_data(
|
||||||
|
double linear_accel_x, double linear_accel_y, double linear_accel_z,
|
||||||
|
double angular_vel_x, double angular_vel_y, double angular_vel_z
|
||||||
|
);
|
||||||
|
|
||||||
|
private:
|
||||||
|
rclcpp::Logger logger_;
|
||||||
|
|
||||||
|
std::unique_ptr<pqxx::connection> conn_;
|
||||||
|
std::unique_ptr<ConfigManager> config_manager_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace assignments::three
|
||||||
61
src/g2_2025_odometry_pkg/src/database/SQLQueries.hpp
Normal file
61
src/g2_2025_odometry_pkg/src/database/SQLQueries.hpp
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/* SQLQueries.hpp
|
||||||
|
* SQL query definitions for the IMU data collection system
|
||||||
|
*
|
||||||
|
* Reviewed by: <x>
|
||||||
|
* Changelog:
|
||||||
|
* [23-09-2025] Wessel T: Created initial database state
|
||||||
|
* [14-10-2025] Wessel T: Updated for IMU data storage
|
||||||
|
* [26-11-2025] Wessel T: Added position and velocity measurement tables
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
static const std::string SQL_CREATE_IMU_DATA_TABLE = R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS imu_data (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
linear_accel_x REAL NOT NULL,
|
||||||
|
linear_accel_y REAL NOT NULL,
|
||||||
|
linear_accel_z REAL NOT NULL,
|
||||||
|
angular_vel_x REAL NOT NULL,
|
||||||
|
angular_vel_y REAL NOT NULL,
|
||||||
|
angular_vel_z REAL NOT NULL
|
||||||
|
);
|
||||||
|
)";
|
||||||
|
|
||||||
|
static const std::string SQL_CREATE_POSITION_DATA_TABLE = R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS position_measurements (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
x REAL NOT NULL,
|
||||||
|
y REAL NOT NULL,
|
||||||
|
theta REAL NOT NULL
|
||||||
|
);
|
||||||
|
)";
|
||||||
|
|
||||||
|
static const std::string SQL_CREATE_VELOCITY_DATA_TABLE = R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS velocity_measurements (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
vx REAL NOT NULL,
|
||||||
|
vy REAL NOT NULL,
|
||||||
|
vz REAL NOT NULL,
|
||||||
|
omega_z REAL NOT NULL
|
||||||
|
);
|
||||||
|
)";
|
||||||
|
|
||||||
|
static const std::string SQL_INSERT_IMU_DATA = R"(
|
||||||
|
INSERT INTO imu_data (
|
||||||
|
linear_accel_x, linear_accel_y, linear_accel_z,
|
||||||
|
angular_vel_x, angular_vel_y, angular_vel_z
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6);
|
||||||
|
)";
|
||||||
|
|
||||||
|
static const std::string SQL_INSERT_POSITION_DATA = R"(
|
||||||
|
INSERT INTO position_measurements (x, y, theta)
|
||||||
|
VALUES ($1, $2, $3);
|
||||||
|
)";
|
||||||
|
|
||||||
|
static const std::string SQL_INSERT_VELOCITY_DATA = R"(
|
||||||
|
INSERT INTO velocity_measurements (vx, vy, vz, omega_z)
|
||||||
|
VALUES ($1, $2, $3, $4);
|
||||||
|
)";
|
||||||
14
src/g2_2025_odometry_pkg/src/g2_2025_database_node/Main.cpp
Normal file
14
src/g2_2025_odometry_pkg/src/g2_2025_database_node/Main.cpp
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
|
||||||
|
#include "nodes/DatabaseHandlerNode.hpp"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
rclcpp::init(argc, argv);
|
||||||
|
|
||||||
|
auto node = std::make_shared<assignments::three::g2_2025_database_node::DatabaseHandlerNode>();
|
||||||
|
|
||||||
|
rclcpp::spin(node->get_node_base_interface());
|
||||||
|
rclcpp::shutdown();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#include "DatabaseHandlerNode.hpp"
|
||||||
|
|
||||||
|
namespace assignments::three::g2_2025_database_node {
|
||||||
|
|
||||||
|
DatabaseHandlerNode::DatabaseHandlerNode(): Node("database_handler_node") {
|
||||||
|
RCLCPP_INFO(this->get_logger(), "database handler Node starting");
|
||||||
|
|
||||||
|
save_position_data_ = this->declare_parameter<bool>("save_position_data", true);
|
||||||
|
save_velocity_data_ = this->declare_parameter<bool>("save_velocity_data", true);
|
||||||
|
|
||||||
|
db_manager_ = std::make_unique<assignments::three::DatabaseManager>(this->get_logger());
|
||||||
|
|
||||||
|
if (!db_manager_->is_connected()) {
|
||||||
|
RCLCPP_ERROR(this->get_logger(), "failed to connect to database");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (save_position_data_) {
|
||||||
|
position_subscriber_ = this->create_subscription<geometry_msgs::msg::Pose2D>(
|
||||||
|
"estimated_position", 10,
|
||||||
|
std::bind(
|
||||||
|
&DatabaseHandlerNode::position_callback,
|
||||||
|
this,
|
||||||
|
std::placeholders::_1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
RCLCPP_INFO(this->get_logger(), "subscribed to 'estimated_position'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (save_velocity_data_) {
|
||||||
|
velocity_subscriber_ = this->create_subscription<geometry_msgs::msg::Twist>(
|
||||||
|
"estimated_velocity", 10,
|
||||||
|
std::bind(
|
||||||
|
&DatabaseHandlerNode::velocity_callback,
|
||||||
|
this,
|
||||||
|
std::placeholders::_1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
RCLCPP_INFO(this->get_logger(), "subscribed to 'estimated_velocity'");
|
||||||
|
}
|
||||||
|
|
||||||
|
RCLCPP_INFO(this->get_logger(), "database node ready to receive data on topics");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatabaseHandlerNode::position_callback(const geometry_msgs::msg::Pose2D::SharedPtr msg) {
|
||||||
|
if (!db_manager_->is_connected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = db_manager_->store_position_data(msg->x, msg->y, msg->theta);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
RCLCPP_WARN_THROTTLE(this->get_logger(),
|
||||||
|
*this->get_clock(),
|
||||||
|
5000,
|
||||||
|
"failed to store position data in database"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
RCLCPP_DEBUG(this->get_logger(), "Stored position=[%.3f, %.3f, %.3f]", msg->x, msg->y, msg->theta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatabaseHandlerNode::velocity_callback(const geometry_msgs::msg::Twist::SharedPtr msg) {
|
||||||
|
if (!db_manager_->is_connected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = db_manager_->store_velocity_data(msg->linear.x, msg->linear.y, msg->linear.z, msg->angular.z);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
RCLCPP_WARN_THROTTLE(this->get_logger(),
|
||||||
|
*this->get_clock(),
|
||||||
|
5000,
|
||||||
|
"failed to store velocity data in database"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
RCLCPP_DEBUG(this->get_logger(), "Stored velocity=[%.3f, %.3f, %.3f, %.3f]",
|
||||||
|
msg->linear.x, msg->linear.y, msg->linear.z, msg->angular.z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace assignments::three::g2_2025_database_node
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/* DatabaseHandlerNode.hpp
|
||||||
|
* Database handler node for subscribing to topics and storing data
|
||||||
|
*
|
||||||
|
* Changelog:
|
||||||
|
* [26-11-2025] Created database handler node for position and sensor data storage
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "database/DatabaseManager.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <rclcpp/rclcpp.hpp>
|
||||||
|
#include <sensor_msgs/msg/imu.hpp>
|
||||||
|
#include <geometry_msgs/msg/pose2_d.hpp>
|
||||||
|
#include <geometry_msgs/msg/twist.hpp>
|
||||||
|
|
||||||
|
namespace assignments::three::g2_2025_database_node {
|
||||||
|
|
||||||
|
class DatabaseHandlerNode : public rclcpp::Node {
|
||||||
|
public:
|
||||||
|
DatabaseHandlerNode();
|
||||||
|
~DatabaseHandlerNode() = default;
|
||||||
|
|
||||||
|
private:
|
||||||
|
rclcpp::Subscription<sensor_msgs::msg::Imu>::SharedPtr imu_subscriber_;
|
||||||
|
rclcpp::Subscription<geometry_msgs::msg::Pose2D>::SharedPtr position_subscriber_;
|
||||||
|
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr velocity_subscriber_;
|
||||||
|
|
||||||
|
std::unique_ptr<assignments::three::DatabaseManager> db_manager_;
|
||||||
|
|
||||||
|
bool save_position_data_ { true };
|
||||||
|
bool save_velocity_data_ { true };
|
||||||
|
|
||||||
|
void position_callback(const geometry_msgs::msg::Pose2D::SharedPtr msg);
|
||||||
|
void velocity_callback(const geometry_msgs::msg::Twist::SharedPtr msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace assignments::three::g2_2025_database_node
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
#include "nodes/ImuDataSimulator.hpp"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
rclcpp::init(argc, argv);
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto node = std::make_shared<assignments::three::data_simulator_node::DataSimulator>();
|
||||||
|
rclcpp::spin(node);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
RCLCPP_ERROR(rclcpp::get_logger("imu_data_simulator"), "Failed to initialize node: %s", e.what());
|
||||||
|
rclcpp::shutdown();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
rclcpp::shutdown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#include "ImuDataSimulator.hpp"
|
||||||
|
|
||||||
|
namespace assignments::three::data_simulator_node {
|
||||||
|
|
||||||
|
DataSimulator::DataSimulator() : Node("imu_data_simulator") {
|
||||||
|
RCLCPP_INFO(this->get_logger(), "ImuDataSimulator node created");
|
||||||
|
|
||||||
|
start_time_ = this->now();
|
||||||
|
|
||||||
|
this->declare_parameter("publish_rate", 10.0);
|
||||||
|
double rate = this->get_parameter("publish_rate").as_double();
|
||||||
|
|
||||||
|
axes_ = { "linear_x", "linear_y", "linear_z", "angular_x", "angular_y", "angular_z" };
|
||||||
|
|
||||||
|
// Simulator loads parameters itself
|
||||||
|
simulator_ = std::make_unique<Simulator>(this, axes_);
|
||||||
|
|
||||||
|
imu_publisher_ = this->create_publisher<sensor_msgs::msg::Imu>("simulated_imu_data", 10);
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Created IMU Publisher on topic 'simulated_imu_data'");
|
||||||
|
|
||||||
|
// Use publish_rate parameter
|
||||||
|
int timer_ms = static_cast<int>(1000.0 / rate);
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Publishing at %.1f Hz (every %d ms)", rate, timer_ms);
|
||||||
|
|
||||||
|
timer_ = this->create_wall_timer(
|
||||||
|
std::chrono::milliseconds(timer_ms),
|
||||||
|
std::bind(&DataSimulator::publish_imu_data, this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DataSimulator::~DataSimulator() {
|
||||||
|
RCLCPP_INFO(this->get_logger(), "DataSimulator node destroyed");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataSimulator::publish_imu_data() {
|
||||||
|
auto imu_msg = std::make_shared<sensor_msgs::msg::Imu>();
|
||||||
|
|
||||||
|
imu_msg->header.stamp = this->now();
|
||||||
|
imu_msg->header.frame_id = "imu_link";
|
||||||
|
|
||||||
|
// Calculate elapsed time since node start
|
||||||
|
double elapsed_time = (this->now() - start_time_).seconds();
|
||||||
|
|
||||||
|
// Get values for each axis
|
||||||
|
imu_msg->linear_acceleration.x = simulator_->get_object_value("linear_x", elapsed_time);
|
||||||
|
imu_msg->linear_acceleration.y = simulator_->get_object_value("linear_y", elapsed_time);
|
||||||
|
imu_msg->linear_acceleration.z = simulator_->get_object_value("linear_z", elapsed_time);
|
||||||
|
imu_msg->angular_velocity.x = simulator_->get_object_value("angular_x", elapsed_time);
|
||||||
|
imu_msg->angular_velocity.y = simulator_->get_object_value("angular_y", elapsed_time);
|
||||||
|
imu_msg->angular_velocity.z = simulator_->get_object_value("angular_z", elapsed_time);
|
||||||
|
|
||||||
|
RCLCPP_INFO(this->get_logger(),
|
||||||
|
"t=%.2fs - Accel: [%.3f, %.3f, %.3f], Gyro: [%.3f, %.3f, %.3f]",
|
||||||
|
elapsed_time,
|
||||||
|
imu_msg->linear_acceleration.x, imu_msg->linear_acceleration.y, imu_msg->linear_acceleration.z,
|
||||||
|
imu_msg->angular_velocity.x, imu_msg->angular_velocity.y, imu_msg->angular_velocity.z
|
||||||
|
);
|
||||||
|
|
||||||
|
imu_publisher_->publish(*imu_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace assignments::three::data_simulator_node
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
#include "sensor_msgs/msg/imu.hpp"
|
||||||
|
#include "simulator/Simulator.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
namespace assignments::three::data_simulator_node {
|
||||||
|
|
||||||
|
using assignments::three::Simulator;
|
||||||
|
|
||||||
|
class DataSimulator : public rclcpp::Node {
|
||||||
|
public:
|
||||||
|
DataSimulator();
|
||||||
|
~DataSimulator();
|
||||||
|
private:
|
||||||
|
rclcpp::Publisher<sensor_msgs::msg::Imu>::SharedPtr imu_publisher_;
|
||||||
|
rclcpp::TimerBase::SharedPtr timer_;
|
||||||
|
rclcpp::Time start_time_;
|
||||||
|
|
||||||
|
std::vector<std::string> axes_;
|
||||||
|
std::unique_ptr<Simulator> simulator_;
|
||||||
|
|
||||||
|
void publish_imu_data();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace assignments::three::data_simulator_node
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
|
||||||
|
#include "nodes/IMUPositionApproximator.hpp"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
rclcpp::init(argc, argv);
|
||||||
|
|
||||||
|
auto node = std::make_shared<assignments::three::g2_2025_imu_position_approximator_node::IMUPositionApproximator>();
|
||||||
|
|
||||||
|
rclcpp::spin(node->get_node_base_interface());
|
||||||
|
rclcpp::shutdown();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#include "IMUPositionApproximator.hpp"
|
||||||
|
|
||||||
|
namespace assignments::three::g2_2025_imu_position_approximator_node {
|
||||||
|
|
||||||
|
IMUPositionApproximator::IMUPositionApproximator()
|
||||||
|
: Node("imu_position_approximator")
|
||||||
|
, x_(0.0), y_(0.0), z_(0.0)
|
||||||
|
, theta_(0.0)
|
||||||
|
, vx_(0.0), vy_(0.0), vz_(0.0)
|
||||||
|
, last_time_(this->now())
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "IMU Position Approximator node started");
|
||||||
|
|
||||||
|
// Get initial position from parameters
|
||||||
|
x_ = this->declare_parameter<double>("initial_x", 0.0);
|
||||||
|
y_ = this->declare_parameter<double>("initial_y", 0.0);
|
||||||
|
z_ = this->declare_parameter<double>("initial_z", 0.0);
|
||||||
|
theta_ = this->declare_parameter<double>("initial_theta", 0.0);
|
||||||
|
imu_topic_ = this->declare_parameter<std::string>("imu_topic", "imu_data");
|
||||||
|
|
||||||
|
RCLCPP_INFO(this->get_logger(),
|
||||||
|
"Initial position set to: x=%.3f, y=%.3f, z=%.3f, theta=%.3f rad",
|
||||||
|
x_, y_, z_, theta_);
|
||||||
|
|
||||||
|
|
||||||
|
imu_subscriber_ = this->create_subscription<sensor_msgs::msg::Imu>(
|
||||||
|
imu_topic_, 10,
|
||||||
|
std::bind(&IMUPositionApproximator::imu_callback, this, std::placeholders::_1)
|
||||||
|
);
|
||||||
|
|
||||||
|
position_reset_subscriber_ = this->create_subscription<geometry_msgs::msg::Pose2D>(
|
||||||
|
"position_reset", 10,
|
||||||
|
std::bind(&IMUPositionApproximator::position_reset_callback, this, std::placeholders::_1)
|
||||||
|
);
|
||||||
|
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Subscribed to '%s' and 'position_reset'", imu_topic_.c_str());
|
||||||
|
|
||||||
|
position_publisher_ = this->create_publisher<geometry_msgs::msg::Pose2D>("estimated_position", 10);
|
||||||
|
velocity_publisher_ = this->create_publisher<geometry_msgs::msg::Twist>("estimated_velocity", 10);
|
||||||
|
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Publishing to 'estimated_position' and 'estimated_velocity'");
|
||||||
|
}
|
||||||
|
|
||||||
|
void IMUPositionApproximator::imu_callback(const sensor_msgs::msg::Imu::SharedPtr msg) {
|
||||||
|
auto current_time = this->now();
|
||||||
|
double dt = (current_time - last_time_).seconds();
|
||||||
|
last_time_ = current_time;
|
||||||
|
|
||||||
|
if (dt <= 0.0 || dt > 1.0) {
|
||||||
|
RCLCPP_WARN(this->get_logger(), "skipping iteration due to invalid delta time (dt=%.3f)", dt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Position pos_robot;
|
||||||
|
pos_robot.x = msg->linear_acceleration.x;
|
||||||
|
pos_robot.y = msg->linear_acceleration.y;
|
||||||
|
// remove gravity, standard gravity is ~9.81 m/s^2
|
||||||
|
pos_robot.z = msg->linear_acceleration.z - 9.81;
|
||||||
|
|
||||||
|
// calculate and normalize theta_ to be within [-pi, pi]
|
||||||
|
double omega_z = msg->angular_velocity.z;
|
||||||
|
theta_ += omega_z * dt;
|
||||||
|
|
||||||
|
while (theta_ > M_PI) theta_ -= 2.0 * M_PI;
|
||||||
|
while (theta_ < -M_PI) theta_ += 2.0 * M_PI;
|
||||||
|
|
||||||
|
// transform from robot frame to map frame
|
||||||
|
Position pos_map;
|
||||||
|
double cos_theta = std::cos(theta_);
|
||||||
|
double sin_theta = std::sin(theta_);
|
||||||
|
pos_map.x = pos_robot.x * cos_theta - pos_robot.y * sin_theta;
|
||||||
|
pos_map.y = pos_robot.x * sin_theta + pos_robot.y * cos_theta;
|
||||||
|
pos_map.z = pos_robot.z;
|
||||||
|
|
||||||
|
// account for centripetal acceleration due to rotation when rotating,
|
||||||
|
// linear velocity creates centripetal acceleration
|
||||||
|
pos_map.x -= -omega_z * vy_;
|
||||||
|
pos_map.y -= omega_z * vx_;
|
||||||
|
|
||||||
|
// integrate acceleration to get velocity
|
||||||
|
vx_ += pos_map.x * dt;
|
||||||
|
vy_ += pos_map.y * dt;
|
||||||
|
vz_ += pos_map.z * dt;
|
||||||
|
|
||||||
|
// integrate velocity to get position
|
||||||
|
x_ += vx_ * dt;
|
||||||
|
y_ += vy_ * dt;
|
||||||
|
z_ += vz_ * dt;
|
||||||
|
|
||||||
|
auto position_msg = geometry_msgs::msg::Pose2D();
|
||||||
|
position_msg.x = x_;
|
||||||
|
position_msg.y = y_;
|
||||||
|
position_msg.theta = theta_;
|
||||||
|
position_publisher_->publish(position_msg);
|
||||||
|
|
||||||
|
auto velocity_msg = geometry_msgs::msg::Twist();
|
||||||
|
velocity_msg.linear.x = vx_;
|
||||||
|
velocity_msg.linear.y = vy_;
|
||||||
|
velocity_msg.linear.z = vz_;
|
||||||
|
velocity_msg.angular.z = omega_z;
|
||||||
|
velocity_publisher_->publish(velocity_msg);
|
||||||
|
|
||||||
|
RCLCPP_INFO(this->get_logger(),
|
||||||
|
"Position: (%.3f, %.3f, %.3f) m, θ=%.3f rad | Velocity: (%.3f, %.3f, %.3f) m/s",
|
||||||
|
x_, y_, z_, theta_, vx_, vy_, vz_
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IMUPositionApproximator::position_reset_callback(const geometry_msgs::msg::Pose2D::SharedPtr msg) {
|
||||||
|
RCLCPP_INFO(this->get_logger(),
|
||||||
|
"resetting position, was: (%.3f, %.3f, %.3f), now: (%.3f, %.3f, %.3f)",
|
||||||
|
x_, y_, theta_, msg->x, msg->y, msg->theta
|
||||||
|
);
|
||||||
|
|
||||||
|
x_ = msg->x;
|
||||||
|
y_ = msg->y;
|
||||||
|
theta_ = msg->theta;
|
||||||
|
|
||||||
|
vx_ = 0.0;
|
||||||
|
vy_ = 0.0;
|
||||||
|
vz_ = 0.0;
|
||||||
|
|
||||||
|
last_time_ = this->now();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace assignments::three::g2_2025_imu_position_approximator_node
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
#include "sensor_msgs/msg/imu.hpp"
|
||||||
|
#include "geometry_msgs/msg/pose2_d.hpp"
|
||||||
|
#include "geometry_msgs/msg/twist.hpp"
|
||||||
|
|
||||||
|
namespace assignments::three::g2_2025_imu_position_approximator_node {
|
||||||
|
|
||||||
|
struct Position {
|
||||||
|
double x;
|
||||||
|
double y;
|
||||||
|
double z;
|
||||||
|
};
|
||||||
|
|
||||||
|
class IMUPositionApproximator : public rclcpp::Node {
|
||||||
|
public:
|
||||||
|
IMUPositionApproximator();
|
||||||
|
|
||||||
|
private:
|
||||||
|
rclcpp::Subscription<sensor_msgs::msg::Imu>::SharedPtr imu_subscriber_;
|
||||||
|
rclcpp::Subscription<geometry_msgs::msg::Pose2D>::SharedPtr position_reset_subscriber_;
|
||||||
|
rclcpp::Publisher<geometry_msgs::msg::Pose2D>::SharedPtr position_publisher_;
|
||||||
|
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr velocity_publisher_;
|
||||||
|
|
||||||
|
std::string imu_topic_ { "imu_data" };
|
||||||
|
double x_, y_, z_; // Position
|
||||||
|
double theta_; // Orientation (yaw angle)
|
||||||
|
double vx_, vy_, vz_; // Velocity
|
||||||
|
rclcpp::Time last_time_;
|
||||||
|
|
||||||
|
void imu_callback(const sensor_msgs::msg::Imu::SharedPtr msg);
|
||||||
|
void position_reset_callback(const geometry_msgs::msg::Pose2D::SharedPtr msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace assignments::three::g2_2025_imu_position_approximator_node
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
#include "nodes/WheelDataSimulator.hpp"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
rclcpp::init(argc, argv);
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto node = std::make_shared<assignments::three::wheel_data_simulator_node::DataSimulator>();
|
||||||
|
rclcpp::spin(node);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
RCLCPP_ERROR(rclcpp::get_logger("wheel_data_simulator"), "Failed to initialize node: %s", e.what());
|
||||||
|
rclcpp::shutdown();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
rclcpp::shutdown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#include "WheelDataSimulator.hpp"
|
||||||
|
|
||||||
|
namespace assignments::three::wheel_data_simulator_node {
|
||||||
|
|
||||||
|
DataSimulator::DataSimulator() : Node("wheel_data_simulator") {
|
||||||
|
RCLCPP_INFO(this->get_logger(), "WheelDataSimulator node created");
|
||||||
|
|
||||||
|
start_time_ = this->now();
|
||||||
|
|
||||||
|
this->declare_parameter("publish_rate", 10.0);
|
||||||
|
double rate = this->get_parameter("publish_rate").as_double();
|
||||||
|
|
||||||
|
wheels_ = { "wheel_fl", "wheel_fr", "wheel_rl", "wheel_rr" };
|
||||||
|
|
||||||
|
// Simulator loads parameters itself
|
||||||
|
simulator_ = std::make_unique<Simulator>(this, wheels_);
|
||||||
|
|
||||||
|
wheel_publisher_ = this->create_publisher<std_msgs::msg::Float64MultiArray>("simulated_wheel_data", 10);
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Created wheel Publisher on topic 'simulated_wheel_data'");
|
||||||
|
|
||||||
|
// Use publish_rate parameter
|
||||||
|
int timer_ms = static_cast<int>(1000.0 / rate);
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Publishing at %.1f Hz (every %d ms)", rate, timer_ms);
|
||||||
|
|
||||||
|
timer_ = this->create_wall_timer(
|
||||||
|
std::chrono::milliseconds(timer_ms),
|
||||||
|
std::bind(&DataSimulator::publish_wheel_data, this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DataSimulator::~DataSimulator() {
|
||||||
|
RCLCPP_INFO(this->get_logger(), "WheelDataSimulator node destroyed");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataSimulator::publish_wheel_data() {
|
||||||
|
auto wheel_msg = std::make_shared<std_msgs::msg::Float64MultiArray>();
|
||||||
|
|
||||||
|
// Calculate elapsed time since node start
|
||||||
|
double elapsed_time = (this->now() - start_time_).seconds();
|
||||||
|
|
||||||
|
// For now, just log wheel values (adjust based on your actual message type)
|
||||||
|
wheel_msg->data = {
|
||||||
|
simulator_->get_object_value("wheel_fl", elapsed_time),
|
||||||
|
simulator_->get_object_value("wheel_fr", elapsed_time),
|
||||||
|
simulator_->get_object_value("wheel_rl", elapsed_time),
|
||||||
|
simulator_->get_object_value("wheel_rr", elapsed_time)
|
||||||
|
};
|
||||||
|
|
||||||
|
RCLCPP_INFO(this->get_logger(),
|
||||||
|
"t=%.2fs - Wheel Data [%.3f, %.3f, %.3f, %.3f]",
|
||||||
|
elapsed_time,
|
||||||
|
wheel_msg->data[0], wheel_msg->data[1], wheel_msg->data[2], wheel_msg->data[3]
|
||||||
|
);
|
||||||
|
|
||||||
|
wheel_publisher_->publish(*wheel_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace assignments::three::wheel_data_simulator_node
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
#include "std_msgs/msg/float64_multi_array.hpp"
|
||||||
|
#include "simulator/Simulator.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace assignments::three::wheel_data_simulator_node {
|
||||||
|
|
||||||
|
using assignments::three::Simulator;
|
||||||
|
using assignments::three::IntervalConfig;
|
||||||
|
using assignments::three::SimType;
|
||||||
|
|
||||||
|
class DataSimulator : public rclcpp::Node {
|
||||||
|
public:
|
||||||
|
DataSimulator();
|
||||||
|
~DataSimulator();
|
||||||
|
private:
|
||||||
|
rclcpp::Publisher<std_msgs::msg::Float64MultiArray>::SharedPtr wheel_publisher_;
|
||||||
|
rclcpp::TimerBase::SharedPtr timer_;
|
||||||
|
rclcpp::Time start_time_;
|
||||||
|
|
||||||
|
std::vector<std::string> wheels_;
|
||||||
|
std::unique_ptr<Simulator> simulator_;
|
||||||
|
|
||||||
|
void publish_wheel_data();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace assignments::three::wheel_data_simulator_node
|
||||||
152
src/g2_2025_odometry_pkg/src/simulator/Simulator.cpp
Normal file
152
src/g2_2025_odometry_pkg/src/simulator/Simulator.cpp
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#include "Simulator.hpp"
|
||||||
|
|
||||||
|
namespace assignments::three {
|
||||||
|
|
||||||
|
Simulator::Simulator(rclcpp::Node* node, const std::vector<std::string>& objects) {
|
||||||
|
load_intervals(node, objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
Simulator::~Simulator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Simulator::load_intervals(rclcpp::Node* node, const std::vector<std::string>& objects) {
|
||||||
|
node->declare_parameter("max_intervals", 4);
|
||||||
|
max_intervals_ = node->get_parameter("max_intervals").as_int();
|
||||||
|
|
||||||
|
for (const auto& object : objects) {
|
||||||
|
node->declare_parameter(object + ".num_intervals", 0);
|
||||||
|
int num_intervals = node->get_parameter(object + ".num_intervals").as_int();
|
||||||
|
|
||||||
|
RCLCPP_INFO(node->get_logger(), "Loading %d intervals for object '%s'", num_intervals, object.c_str());
|
||||||
|
std::vector<IntervalConfig> intervals;
|
||||||
|
|
||||||
|
for (int i = 0; i < std::min(num_intervals, max_intervals_); i++) {
|
||||||
|
std::string prefix = object + ".interval_" + std::to_string(i);
|
||||||
|
|
||||||
|
node->declare_parameter(prefix + ".type", "constant");
|
||||||
|
node->declare_parameter(prefix + ".t_start", 0.0);
|
||||||
|
node->declare_parameter(prefix + ".t_end", 0.0);
|
||||||
|
node->declare_parameter(prefix + ".y_start", 0.0);
|
||||||
|
node->declare_parameter(prefix + ".y_end", 0.0);
|
||||||
|
node->declare_parameter(prefix + ".t_mid", 0.0);
|
||||||
|
node->declare_parameter(prefix + ".y_mid", 0.0);
|
||||||
|
|
||||||
|
IntervalConfig config;
|
||||||
|
std::string type_str = node->get_parameter(prefix + ".type").as_string();
|
||||||
|
if (type_str == "constant") {
|
||||||
|
config.type = SimType::CONSTANT;
|
||||||
|
} else if (type_str == "linear") {
|
||||||
|
config.type = SimType::LINEAR;
|
||||||
|
} else if (type_str == "quadratic") {
|
||||||
|
config.type = SimType::QUADRATIC;
|
||||||
|
} else {
|
||||||
|
RCLCPP_WARN(node->get_logger(), "Unknown interval type '%s', defaulting to 'constant'", type_str.c_str());
|
||||||
|
config.type = SimType::CONSTANT;
|
||||||
|
}
|
||||||
|
config.t_start = node->get_parameter(prefix + ".t_start").as_double();
|
||||||
|
config.t_end = node->get_parameter(prefix + ".t_end").as_double();
|
||||||
|
config.y_start = node->get_parameter(prefix + ".y_start").as_double();
|
||||||
|
config.y_end = node->get_parameter(prefix + ".y_end").as_double();
|
||||||
|
config.t_mid = node->get_parameter(prefix + ".t_mid").as_double();
|
||||||
|
config.y_mid = node->get_parameter(prefix + ".y_mid").as_double();
|
||||||
|
|
||||||
|
intervals.push_back(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t j = 0; j < intervals.size(); ++j) {
|
||||||
|
const auto& cfg = intervals[j];
|
||||||
|
const char* type_str = (cfg.type == SimType::CONSTANT) ? "constant" :
|
||||||
|
(cfg.type == SimType::LINEAR) ? "linear" : "quadratic";
|
||||||
|
RCLCPP_DEBUG(node->get_logger(),
|
||||||
|
"Object '%s' Interval %zu: type='%s', t_start=%.6f, t_end=%.6f, y_start=%.6f, y_end=%.6f, t_mid=%.6f, y_mid=%.6f",
|
||||||
|
object.c_str(), j, type_str,
|
||||||
|
cfg.t_start, cfg.t_end, cfg.y_start, cfg.y_end, cfg.t_mid, cfg.y_mid
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check for overlaps with other intervals
|
||||||
|
for (size_t k = j + 1; k < intervals.size(); ++k) {
|
||||||
|
const auto& other = intervals[k];
|
||||||
|
// Two intervals overlap if one starts before the other ends
|
||||||
|
bool overlaps = (cfg.t_start < other.t_end && cfg.t_end > other.t_start);
|
||||||
|
if (overlaps) {
|
||||||
|
RCLCPP_ERROR(node->get_logger(),
|
||||||
|
"Object '%s': Interval %zu [%.2f, %.2f] overlaps with Interval %zu [%.2f, %.2f]",
|
||||||
|
object.c_str(), j, cfg.t_start, cfg.t_end, k, other.t_start, other.t_end
|
||||||
|
);
|
||||||
|
throw std::runtime_error("Overlapping intervals detected for object '" + object + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object_intervals_[object] = intervals;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double Simulator::compute_value(double t, const IntervalConfig& interval) {
|
||||||
|
// Check if time is within interval
|
||||||
|
if (t < interval.t_start || t > interval.t_end) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (interval.type) {
|
||||||
|
case SimType::CONSTANT:
|
||||||
|
// Constant: y = c
|
||||||
|
return interval.y_start;
|
||||||
|
|
||||||
|
case SimType::LINEAR: {
|
||||||
|
// Linear interpolation: y = y0 + (y1 - y0) * (t - t0) / (t1 - t0)
|
||||||
|
double t0 = interval.t_start;
|
||||||
|
double t1 = interval.t_end;
|
||||||
|
double y0 = interval.y_start;
|
||||||
|
double y1 = interval.y_end;
|
||||||
|
|
||||||
|
double L0 = (t - t1) / (t0 - t1);
|
||||||
|
double L1 = (t - t0) / (t1 - t0);
|
||||||
|
|
||||||
|
return y0 * L0 + y1 * L1;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SimType::QUADRATIC: {
|
||||||
|
// Quadratic using Lagrange interpolation through 3 points
|
||||||
|
double t0 = interval.t_start;
|
||||||
|
double t1 = interval.t_mid;
|
||||||
|
double t2 = interval.t_end;
|
||||||
|
double y0 = interval.y_start;
|
||||||
|
double y1 = interval.y_mid;
|
||||||
|
double y2 = interval.y_end;
|
||||||
|
|
||||||
|
double L0 = ((t - t1) * (t - t2)) / ((t0 - t1) * (t0 - t2));
|
||||||
|
double L1 = ((t - t0) * (t - t2)) / ((t1 - t0) * (t1 - t2));
|
||||||
|
double L2 = ((t - t0) * (t - t1)) / ((t2 - t0) * (t2 - t1));
|
||||||
|
|
||||||
|
return y0 * L0 + y1 * L1 + y2 * L2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double Simulator::get_object_value(const std::string& object, double t) {
|
||||||
|
// Check if object exists in configuration
|
||||||
|
auto it = object_intervals_.find(object);
|
||||||
|
if (it == object_intervals_.end()) {
|
||||||
|
return 0.0; // Default value if object not configured
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each interval for this object
|
||||||
|
double last_value = 0.0;
|
||||||
|
for (const auto& interval : it->second) {
|
||||||
|
if (t >= interval.t_start && t <= interval.t_end) {
|
||||||
|
// Currently in this interval
|
||||||
|
return compute_value(t, interval);
|
||||||
|
} else if (t > interval.t_end) {
|
||||||
|
// Past this interval - remember its end value for holding
|
||||||
|
last_value = compute_value(interval.t_end, interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not in any interval - return last known value (or 0.0 if before all intervals)
|
||||||
|
return last_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace assignments::three
|
||||||
41
src/g2_2025_odometry_pkg/src/simulator/Simulator.hpp
Normal file
41
src/g2_2025_odometry_pkg/src/simulator/Simulator.hpp
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <rclcpp/rclcpp.hpp>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace assignments::three {
|
||||||
|
|
||||||
|
enum class SimType {
|
||||||
|
CONSTANT,
|
||||||
|
LINEAR,
|
||||||
|
QUADRATIC
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IntervalConfig {
|
||||||
|
SimType type;
|
||||||
|
double t_start;
|
||||||
|
double t_end;
|
||||||
|
double y_start;
|
||||||
|
double y_end;
|
||||||
|
double t_mid; // For quadratic
|
||||||
|
double y_mid; // For quadratic
|
||||||
|
};
|
||||||
|
|
||||||
|
class Simulator {
|
||||||
|
public:
|
||||||
|
Simulator(rclcpp::Node* node, const std::vector<std::string>& objects);
|
||||||
|
~Simulator();
|
||||||
|
|
||||||
|
double get_object_value(const std::string& object, double t);
|
||||||
|
|
||||||
|
private:
|
||||||
|
int max_intervals_;
|
||||||
|
std::map<std::string, std::vector<IntervalConfig>> object_intervals_;
|
||||||
|
|
||||||
|
void load_intervals(rclcpp::Node* node, const std::vector<std::string>& objects);
|
||||||
|
double compute_value(double t, const IntervalConfig& interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace assignments::three
|
||||||
171
src/g2_2025_odometry_pkg/test/TestImuSimulator.cpp
Normal file
171
src/g2_2025_odometry_pkg/test/TestImuSimulator.cpp
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <rclcpp/rclcpp.hpp>
|
||||||
|
#include <sensor_msgs/msg/imu.hpp>
|
||||||
|
#include "g2_2025_imu_data_simulator_node/nodes/ImuDataSimulator.hpp"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
using namespace assignments::three::data_simulator_node;
|
||||||
|
|
||||||
|
class IMUSimulatorTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
static void SetUpTestSuite() {
|
||||||
|
rclcpp::init(0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void TearDownTestSuite() {
|
||||||
|
rclcpp::shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupBasicIMUParams(rclcpp::Node* node) {
|
||||||
|
node->declare_parameter("max_intervals", 4);
|
||||||
|
node->declare_parameter("publish_rate", 10.0);
|
||||||
|
|
||||||
|
// Setup linear_x
|
||||||
|
node->declare_parameter("linear_x.num_intervals", 1);
|
||||||
|
node->declare_parameter("linear_x.interval_0.type", "constant");
|
||||||
|
node->declare_parameter("linear_x.interval_0.t_start", 0.0);
|
||||||
|
node->declare_parameter("linear_x.interval_0.t_end", 5.0);
|
||||||
|
node->declare_parameter("linear_x.interval_0.y_start", 1.0);
|
||||||
|
node->declare_parameter("linear_x.interval_0.y_end", 0.0);
|
||||||
|
node->declare_parameter("linear_x.interval_0.t_mid", 0.0);
|
||||||
|
node->declare_parameter("linear_x.interval_0.y_mid", 0.0);
|
||||||
|
|
||||||
|
// Setup other axes with no intervals
|
||||||
|
for (const auto& axis : { "linear_y", "linear_z", "angular_x", "angular_y", "angular_z" }) {
|
||||||
|
node->declare_parameter(std::string(axis) + ".num_intervals", 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test node initialization
|
||||||
|
TEST_F(IMUSimulatorTest, NodeInitialization) {
|
||||||
|
auto node = std::make_shared<rclcpp::Node>("test_imu_init_node");
|
||||||
|
setupBasicIMUParams(node.get());
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
EXPECT_NO_THROW({
|
||||||
|
DataSimulator sim;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test IMU message publishing
|
||||||
|
TEST_F(IMUSimulatorTest, MessagePublishing) {
|
||||||
|
auto test_node = std::make_shared<rclcpp::Node>("test_imu_pub_node");
|
||||||
|
|
||||||
|
sensor_msgs::msg::Imu::SharedPtr received_msg;
|
||||||
|
auto subscription = test_node->create_subscription<sensor_msgs::msg::Imu>(
|
||||||
|
"simulated_imu_data",
|
||||||
|
10,
|
||||||
|
[&received_msg](sensor_msgs::msg::Imu::SharedPtr msg) {
|
||||||
|
received_msg = msg;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
auto sim_node = std::make_shared<DataSimulator>();
|
||||||
|
|
||||||
|
// Spin a few times to allow publishing
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
rclcpp::spin_some(sim_node);
|
||||||
|
rclcpp::spin_some(test_node);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we received a message
|
||||||
|
ASSERT_NE(received_msg, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test IMU message frame_id
|
||||||
|
TEST_F(IMUSimulatorTest, MessageFrameId) {
|
||||||
|
auto test_node = std::make_shared<rclcpp::Node>("test_imu_frame_node");
|
||||||
|
|
||||||
|
sensor_msgs::msg::Imu::SharedPtr received_msg;
|
||||||
|
auto subscription = test_node->create_subscription<sensor_msgs::msg::Imu>(
|
||||||
|
"simulated_imu_data",
|
||||||
|
10,
|
||||||
|
[&received_msg](sensor_msgs::msg::Imu::SharedPtr msg) {
|
||||||
|
received_msg = msg;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
auto sim_node = std::make_shared<DataSimulator>();
|
||||||
|
|
||||||
|
// Spin a few times to allow publishing
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
rclcpp::spin_some(sim_node);
|
||||||
|
rclcpp::spin_some(test_node);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_NE(received_msg, nullptr);
|
||||||
|
EXPECT_EQ(received_msg->header.frame_id, "imu_link");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test linear acceleration values
|
||||||
|
TEST_F(IMUSimulatorTest, LinearAccelerationValues) {
|
||||||
|
auto test_node = std::make_shared<rclcpp::Node>("test_imu_linear_node");
|
||||||
|
|
||||||
|
sensor_msgs::msg::Imu::SharedPtr received_msg;
|
||||||
|
auto subscription = test_node->create_subscription<sensor_msgs::msg::Imu>(
|
||||||
|
"simulated_imu_data",
|
||||||
|
10,
|
||||||
|
[&received_msg](sensor_msgs::msg::Imu::SharedPtr msg) {
|
||||||
|
if (!received_msg) { // Only capture first message
|
||||||
|
received_msg = msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
auto sim_node = std::make_shared<DataSimulator>();
|
||||||
|
|
||||||
|
// Spin until we receive a message
|
||||||
|
for (int i = 0; i < 10 && !received_msg; i++) {
|
||||||
|
rclcpp::spin_some(sim_node);
|
||||||
|
rclcpp::spin_some(test_node);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_NE(received_msg, nullptr);
|
||||||
|
|
||||||
|
// Values should be set according to configuration
|
||||||
|
// Since we don't control exact timing, just verify structure
|
||||||
|
EXPECT_TRUE(std::isfinite(received_msg->linear_acceleration.x));
|
||||||
|
EXPECT_TRUE(std::isfinite(received_msg->linear_acceleration.y));
|
||||||
|
EXPECT_TRUE(std::isfinite(received_msg->linear_acceleration.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test angular velocity values
|
||||||
|
TEST_F(IMUSimulatorTest, AngularVelocityValues) {
|
||||||
|
auto test_node = std::make_shared<rclcpp::Node>("test_imu_angular_node");
|
||||||
|
|
||||||
|
sensor_msgs::msg::Imu::SharedPtr received_msg;
|
||||||
|
auto subscription = test_node->create_subscription<sensor_msgs::msg::Imu>(
|
||||||
|
"simulated_imu_data",
|
||||||
|
10,
|
||||||
|
[&received_msg](sensor_msgs::msg::Imu::SharedPtr msg) {
|
||||||
|
if (!received_msg) {
|
||||||
|
received_msg = msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
auto sim_node = std::make_shared<DataSimulator>();
|
||||||
|
|
||||||
|
// Spin until we receive a message
|
||||||
|
for (int i = 0; i < 10 && !received_msg; i++) {
|
||||||
|
rclcpp::spin_some(sim_node);
|
||||||
|
rclcpp::spin_some(test_node);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_NE(received_msg, nullptr);
|
||||||
|
|
||||||
|
// Values should be set according to configuration
|
||||||
|
EXPECT_TRUE(std::isfinite(received_msg->angular_velocity.x));
|
||||||
|
EXPECT_TRUE(std::isfinite(received_msg->angular_velocity.y));
|
||||||
|
EXPECT_TRUE(std::isfinite(received_msg->angular_velocity.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
testing::InitGoogleTest(&argc, argv);
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
257
src/g2_2025_odometry_pkg/test/TestSimulator.cpp
Normal file
257
src/g2_2025_odometry_pkg/test/TestSimulator.cpp
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <rclcpp/rclcpp.hpp>
|
||||||
|
#include "simulator/Simulator.hpp"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
using namespace assignments::three;
|
||||||
|
|
||||||
|
class SimulatorTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
static void SetUpTestSuite() {
|
||||||
|
rclcpp::init(0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void TearDownTestSuite() {
|
||||||
|
rclcpp::shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create a node with parameter overrides
|
||||||
|
std::shared_ptr<rclcpp::Node> createNodeWithParams(
|
||||||
|
const std::string& name,
|
||||||
|
const std::vector<rclcpp::Parameter>& params)
|
||||||
|
{
|
||||||
|
rclcpp::NodeOptions options;
|
||||||
|
options.parameter_overrides(params);
|
||||||
|
return std::make_shared<rclcpp::Node>(name, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test constant interval
|
||||||
|
TEST_F(SimulatorTest, ConstantInterval) {
|
||||||
|
auto node = createNodeWithParams("test_constant_node", {
|
||||||
|
rclcpp::Parameter("max_intervals", 4),
|
||||||
|
rclcpp::Parameter("test_channel.num_intervals", 1),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.type", "constant"),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_start", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_end", 10.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_start", 5.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_end", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_mid", 0.0)
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<std::string> channels = { "test_channel" };
|
||||||
|
Simulator sim(node.get(), channels); // Test values within interval
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 0.0), 5.0);
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 5.0), 5.0);
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 10.0), 5.0);
|
||||||
|
|
||||||
|
// Test value after interval holds
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 15.0), 5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test linear interval
|
||||||
|
TEST_F(SimulatorTest, LinearInterval) {
|
||||||
|
auto node = createNodeWithParams("test_linear_node", {
|
||||||
|
rclcpp::Parameter("max_intervals", 4),
|
||||||
|
rclcpp::Parameter("test_channel.num_intervals", 1),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.type", "linear"),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_start", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_end", 10.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_start", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_end", 10.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_mid", 0.0)
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<std::string> channels = { "test_channel" };
|
||||||
|
Simulator sim(node.get(), channels);
|
||||||
|
|
||||||
|
// Test linear interpolation
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 0.0), 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 5.0), 5.0);
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 10.0), 10.0);
|
||||||
|
|
||||||
|
// Test value after interval holds
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 15.0), 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test quadratic interval
|
||||||
|
TEST_F(SimulatorTest, QuadraticInterval) {
|
||||||
|
auto node = createNodeWithParams("test_quadratic_node", {
|
||||||
|
rclcpp::Parameter("max_intervals", 4),
|
||||||
|
rclcpp::Parameter("test_channel.num_intervals", 1),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.type", "quadratic"),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_start", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_end", 10.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_start", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_end", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_mid", 5.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_mid", 10.0)
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<std::string> channels = { "test_channel" };
|
||||||
|
Simulator sim(node.get(), channels);
|
||||||
|
|
||||||
|
// Test quadratic interpolation
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 0.0), 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 5.0), 10.0);
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 10.0), 0.0);
|
||||||
|
|
||||||
|
// Test value after interval holds
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 15.0), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test multiple intervals
|
||||||
|
TEST_F(SimulatorTest, MultipleIntervals) {
|
||||||
|
auto node = createNodeWithParams("test_multiple_node", {
|
||||||
|
rclcpp::Parameter("max_intervals", 4),
|
||||||
|
rclcpp::Parameter("test_channel.num_intervals", 2),
|
||||||
|
// First interval: constant 5.0 from t=0 to t=5
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.type", "constant"),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_start", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_end", 5.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_start", 5.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_end", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_mid", 0.0),
|
||||||
|
// Second interval: constant 10.0 from t=10 to t=15
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.type", "constant"),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.t_start", 10.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.t_end", 15.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.y_start", 10.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.y_end", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.t_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.y_mid", 0.0)
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<std::string> channels = { "test_channel" };
|
||||||
|
Simulator sim(node.get(), channels);
|
||||||
|
|
||||||
|
// Test first interval
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 2.5), 5.0);
|
||||||
|
|
||||||
|
// Test gap between intervals - should hold value from first interval
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 7.5), 5.0);
|
||||||
|
|
||||||
|
// Test second interval
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 12.5), 10.0);
|
||||||
|
|
||||||
|
// Test after all intervals - should hold value from last interval
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 20.0), 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test value before any interval
|
||||||
|
TEST_F(SimulatorTest, ValueBeforeIntervals) {
|
||||||
|
auto node = createNodeWithParams("test_before_node", {
|
||||||
|
rclcpp::Parameter("max_intervals", 4),
|
||||||
|
rclcpp::Parameter("test_channel.num_intervals", 1),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.type", "constant"),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_start", 10.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_end", 20.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_start", 5.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_end", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_mid", 0.0)
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<std::string> channels = { "test_channel" };
|
||||||
|
Simulator sim(node.get(), channels);
|
||||||
|
|
||||||
|
// Before any interval starts, should return 0.0
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 5.0), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test non-existent channel
|
||||||
|
TEST_F(SimulatorTest, NonExistentChannel) {
|
||||||
|
auto node = createNodeWithParams("test_nonexistent_node", {
|
||||||
|
rclcpp::Parameter("max_intervals", 4),
|
||||||
|
rclcpp::Parameter("test_channel.num_intervals", 1),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.type", "constant"),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_start", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_end", 10.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_start", 5.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_end", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_mid", 0.0)
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<std::string> channels = { "test_channel" };
|
||||||
|
Simulator sim(node.get(), channels);
|
||||||
|
|
||||||
|
// Query non-existent channel should return 0.0
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("nonexistent_channel", 5.0), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test overlapping intervals detection
|
||||||
|
TEST_F(SimulatorTest, OverlappingIntervalsThrowsException) {
|
||||||
|
auto node = createNodeWithParams("test_overlap_node", {
|
||||||
|
rclcpp::Parameter("max_intervals", 4),
|
||||||
|
rclcpp::Parameter("test_channel.num_intervals", 2),
|
||||||
|
// First interval: t=0 to t=10
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.type", "constant"),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_start", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_end", 10.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_start", 5.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_end", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_mid", 0.0),
|
||||||
|
// Second interval: t=5 to t=15 (overlaps with first)
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.type", "constant"),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.t_start", 5.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.t_end", 15.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.y_start", 10.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.y_end", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.t_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.y_mid", 0.0)
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<std::string> channels = { "test_channel" };
|
||||||
|
|
||||||
|
// Should throw exception due to overlapping intervals
|
||||||
|
EXPECT_THROW({
|
||||||
|
Simulator sim(node.get(), channels);
|
||||||
|
}, std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test max_intervals limit
|
||||||
|
TEST_F(SimulatorTest, MaxIntervalsLimit) {
|
||||||
|
auto node = createNodeWithParams("test_max_intervals_node", {
|
||||||
|
rclcpp::Parameter("max_intervals", 2),
|
||||||
|
rclcpp::Parameter("test_channel.num_intervals", 3),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.type", "constant"),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_start", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_end", 10.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_start", 1.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_end", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.t_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_0.y_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.type", "constant"),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.t_start", 10.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.t_end", 20.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.y_start", 2.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.y_end", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.t_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_1.y_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_2.type", "constant"),
|
||||||
|
rclcpp::Parameter("test_channel.interval_2.t_start", 20.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_2.t_end", 30.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_2.y_start", 3.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_2.y_end", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_2.t_mid", 0.0),
|
||||||
|
rclcpp::Parameter("test_channel.interval_2.y_mid", 0.0)
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<std::string> channels = { "test_channel" };
|
||||||
|
Simulator sim(node.get(), channels);
|
||||||
|
|
||||||
|
// Only first 2 intervals should be loaded
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 5.0), 1.0); // First interval
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 15.0), 2.0); // Second interval
|
||||||
|
EXPECT_DOUBLE_EQ(sim.get_object_value("test_channel", 25.0), 2.0); // Third interval should not exist
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
testing::InitGoogleTest(&argc, argv);
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
168
src/g2_2025_odometry_pkg/test/TestWheelSimulator.cpp
Normal file
168
src/g2_2025_odometry_pkg/test/TestWheelSimulator.cpp
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <rclcpp/rclcpp.hpp>
|
||||||
|
#include <std_msgs/msg/float64_multi_array.hpp>
|
||||||
|
#include "g2_2025_wheel_data_simulator_node/nodes/WheelDataSimulator.hpp"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
using namespace assignments::three::wheel_data_simulator_node;
|
||||||
|
|
||||||
|
class WheelSimulatorTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
static void SetUpTestSuite() {
|
||||||
|
rclcpp::init(0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void TearDownTestSuite() {
|
||||||
|
rclcpp::shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupBasicWheelParams(rclcpp::Node* node) {
|
||||||
|
node->declare_parameter("max_intervals", 4);
|
||||||
|
node->declare_parameter("publish_rate", 10.0);
|
||||||
|
|
||||||
|
// Setup wheel_fl
|
||||||
|
node->declare_parameter("wheel_fl.num_intervals", 1);
|
||||||
|
node->declare_parameter("wheel_fl.interval_0.type", "linear");
|
||||||
|
node->declare_parameter("wheel_fl.interval_0.t_start", 0.0);
|
||||||
|
node->declare_parameter("wheel_fl.interval_0.t_end", 10.0);
|
||||||
|
node->declare_parameter("wheel_fl.interval_0.y_start", 0.0);
|
||||||
|
node->declare_parameter("wheel_fl.interval_0.y_end", 5.0);
|
||||||
|
node->declare_parameter("wheel_fl.interval_0.t_mid", 0.0);
|
||||||
|
node->declare_parameter("wheel_fl.interval_0.y_mid", 0.0);
|
||||||
|
|
||||||
|
// Setup other wheels with no intervals
|
||||||
|
for (const auto& wheel : {"wheel_fr", "wheel_bl", "wheel_br"}) {
|
||||||
|
node->declare_parameter(std::string(wheel) + ".num_intervals", 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test node initialization
|
||||||
|
TEST_F(WheelSimulatorTest, NodeInitialization) {
|
||||||
|
auto node = std::make_shared<rclcpp::Node>("test_wheel_init_node");
|
||||||
|
setupBasicWheelParams(node.get());
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
EXPECT_NO_THROW({
|
||||||
|
DataSimulator sim;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test wheel message publishing
|
||||||
|
TEST_F(WheelSimulatorTest, WheelDataPublishing) {
|
||||||
|
auto test_node = std::make_shared<rclcpp::Node>("test_wheel_pub_node");
|
||||||
|
|
||||||
|
std_msgs::msg::Float64MultiArray::SharedPtr received_msg;
|
||||||
|
auto subscription = test_node->create_subscription<std_msgs::msg::Float64MultiArray>(
|
||||||
|
"simulated_wheel_data",
|
||||||
|
10,
|
||||||
|
[&received_msg](std_msgs::msg::Float64MultiArray::SharedPtr msg) {
|
||||||
|
received_msg = msg;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
auto sim_node = std::make_shared<DataSimulator>();
|
||||||
|
|
||||||
|
// Spin a few times to allow publishing
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
rclcpp::spin_some(sim_node);
|
||||||
|
rclcpp::spin_some(test_node);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we received a message
|
||||||
|
ASSERT_NE(received_msg, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test wheel data array size
|
||||||
|
TEST_F(WheelSimulatorTest, WheelDataArraySize) {
|
||||||
|
auto test_node = std::make_shared<rclcpp::Node>("test_wheel_size_node");
|
||||||
|
|
||||||
|
std_msgs::msg::Float64MultiArray::SharedPtr received_msg;
|
||||||
|
auto subscription = test_node->create_subscription<std_msgs::msg::Float64MultiArray>(
|
||||||
|
"simulated_wheel_data",
|
||||||
|
10,
|
||||||
|
[&received_msg](std_msgs::msg::Float64MultiArray::SharedPtr msg) {
|
||||||
|
if (!received_msg) {
|
||||||
|
received_msg = msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
auto sim_node = std::make_shared<DataSimulator>();
|
||||||
|
|
||||||
|
// Spin until we receive a message
|
||||||
|
for (int i = 0; i < 10 && !received_msg; i++) {
|
||||||
|
rclcpp::spin_some(sim_node);
|
||||||
|
rclcpp::spin_some(test_node);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_NE(received_msg, nullptr);
|
||||||
|
// Should have 4 wheel values: FL, FR, RL, RR
|
||||||
|
EXPECT_EQ(received_msg->data.size(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test wheel velocity values are finite
|
||||||
|
TEST_F(WheelSimulatorTest, ValidVelocityValues) {
|
||||||
|
auto test_node = std::make_shared<rclcpp::Node>("test_wheel_values_node");
|
||||||
|
|
||||||
|
std_msgs::msg::Float64MultiArray::SharedPtr received_msg;
|
||||||
|
auto subscription = test_node->create_subscription<std_msgs::msg::Float64MultiArray>(
|
||||||
|
"simulated_wheel_data",
|
||||||
|
10,
|
||||||
|
[&received_msg](std_msgs::msg::Float64MultiArray::SharedPtr msg) {
|
||||||
|
if (!received_msg) {
|
||||||
|
received_msg = msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
auto sim_node = std::make_shared<DataSimulator>();
|
||||||
|
|
||||||
|
// Spin until we receive a message
|
||||||
|
for (int i = 0; i < 10 && !received_msg; i++) {
|
||||||
|
rclcpp::spin_some(sim_node);
|
||||||
|
rclcpp::spin_some(test_node);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_NE(received_msg, nullptr);
|
||||||
|
ASSERT_EQ(received_msg->data.size(), 4);
|
||||||
|
|
||||||
|
// All values should be finite
|
||||||
|
for (const auto& val : received_msg->data) {
|
||||||
|
EXPECT_TRUE(std::isfinite(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test multiple wheel messages received
|
||||||
|
TEST_F(WheelSimulatorTest, MultipleMessagesReceived) {
|
||||||
|
auto test_node = std::make_shared<rclcpp::Node>("test_multi_wheel_node");
|
||||||
|
|
||||||
|
int msg_count = 0;
|
||||||
|
auto subscription = test_node->create_subscription<std_msgs::msg::Float64MultiArray>(
|
||||||
|
"simulated_wheel_data",
|
||||||
|
10,
|
||||||
|
[&msg_count](std_msgs::msg::Float64MultiArray::SharedPtr) {
|
||||||
|
msg_count++;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
auto sim_node = std::make_shared<DataSimulator>();
|
||||||
|
|
||||||
|
// Spin for a bit to receive messages
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
rclcpp::spin_some(sim_node);
|
||||||
|
rclcpp::spin_some(test_node);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have received at least one message
|
||||||
|
EXPECT_GT(msg_count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
testing::InitGoogleTest(&argc, argv);
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user