In this tutorial we will understand IMU sensors and how to visualize the data using a web application.
After completing this tutorial, you will be able to:
Understand the principle behind IMU sensors
Understand the data structure of IMU messages
Understand how to visualize IMU data using a web application
16.1 Getting Started
We will be using the same repository as the previous tutorial. Once the previous tutorial deadline is over, your instructor will update the upstream repository with the necessary files for this tutorial and raise a pull request on your team repository. You will need to pull in the changes from the upstream repository to your team repository to start this tutorial. The instructions to pull in the changes are given in Appendix B.
After you complete the steps shown in Appendix B to synchronize your repository with the upstream repository, navigate to the root directory of your repository. Pull the latest changes made to your team repository onto your local machine.
cd <your-repository-name>
git pull origin main
The relevant folder structure of the repository for this tutorial is shown below.
All your group members can work independently on their own branches. However, you will have to merge your branches back to the main branch before the end of the tutorial. We learnt how to do this in Chapter 13 using pull requests.
First start docker desktop and then follow the instructions below to run the course docker container. Make sure to navigate in the terminal to the root folder of your repository.
For Windows:
./ros2_run_devdocker.bat
For Ubuntu (or MacOS):
./ros2_run_devdocker.sh
Source the ROS 2 environment by running the following command:
sros2
Notice that this is an alias for sourcing your ROS 2 workspace as well as the ROS 2 installation in the docker container. Remember that every time you perform a colcon build on your workspace, you need to source the ROS 2 environment again with the command above.
16.2 IMU Sensor
IMU sensors are a package of three sensors:
Accelerometer
Gyroscope
Magnetometer
The accelerometer measures the linear acceleration of the sensor. This includes the gravity force and the linear acceleration of the sensor. Notice that the IMU would report zero acceleration when it is in free fall. The inclusion of the acceleration due to gravity is an important aspect of the accelerometer. As the gravity vector always points downwards, the measurement form the accelerometer can be used to determine the roll and pitch orientations of the sensor. Notice however that the yaw orientation cannot be determined from the accelerometer alone. Consider a case where roll and pitch are both zero. The accelerometer would report the same acceleration vector for a sensor oriented in any yaw direction!
The gyroscope measures the angular velocity of the sensor. As we learnt in our lectures, the angular velocity can be used to obtain Euler rates and integrating these rates over time will give us the Euler angles. However, the gyroscope is susceptible to drift over time. This is because the gyroscope measures the rate of change of orientation and any error in the initial orientation will be accumulated over time.
The magnetometer measures the magnetic field of the sensor, which is used to determine the yaw orientation of the sensor. The magnetometer is susceptible to magnetic interference from the environment. For example, if the sensor is placed close to a magnet, the magnetometer will report a strong magnetic field in the direction of the magnet. This will cause the yaw orientation to be incorrect. This means that when we place the IMU on our marine vehicle, we need to ensure that the magnetometer is not close to any ferromagnetic materials as well as any other sources of magnetic interference such as electric motors.
16.3 Bag File
We will be using a bag file to simulate the IMU sensor. The bag file contains the /imu/data topic which was obtained from a real IMU sensor on a marine vehicle. The bag file is located in the data folder inside the tut_05 folder.
The bag folder contains the directory 09_free which contains the bag file 09_free_0.db3. For our ease of understanding, the data from the bag file is also extracted into csv files hosted inside the directory /workspaces/mavlab/ros2_ws/src/student/tut_05/bag/09_free/. The following CSV files are present:
actuator_cmd.csv
imu_data.csv
odom.csv
uwb_data.csv
uwb_velocities.csv
The folder also contains a plots folder which contains the plots of the data. You are encouraged to look at the plots to understand the data.
The metadata of the bag file is present in the file metadata.yaml. Feel free to open the file in any text editor to view the metadata.
To play the bag file, run the following command inside the docker container:
ros2 bag play -l /workspaces/mavlab/ros2_ws/src/student/tut_05/bag/09_free/09_free_0.db3
16.4 IMU Message
The IMU sensor on our marine vehicle publishes data to the /imu/data topic. The message type is sensor_msgs/Imu.
Open a new terminal and connect to the container by running the following command:
docker exec -it oe3036 bash
We can use the ros2 topic echo command to view the data on the new terminal connected to the container.
sros2ros2 topic echo /imu/data
The sensor_msgs/Imu message type has the following fields:
header: The header of the message.
orientation: The orientation of the sensor.
orientation_covariance: The covariance of the orientation.
angular_velocity: The angular velocity of the sensor.
angular_velocity_covariance: The covariance of the angular velocity.
linear_acceleration: The linear acceleration of the sensor.
linear_acceleration_covariance: The covariance of the linear acceleration.
16.5 Create a new ROS2 package
Create a new ROS2 package inside /workspaces/mavlab/ros2_ws/src/student/tut_05
cd /workspaces/mavlab/ros2_ws/src/student/tut_05ros2 pkg create --build-type ament_python mav_imu_visualizer --dependencies rclpy std_msgs nav_msgs sensor_msgs geometry_msgs --license MIT
16.6 Create a new launch file
Create a new launch file inside /workspaces/mavlab/ros2_ws/src/student/tut_05/mav_imu_visualizer/launch
cd /workspaces/mavlab/ros2_ws/src/student/tut_05/mav_imu_visualizermkdir launchcd launchtouch mav_imu_visualizer_launch.py
Copy the following code into the mav_imu_visualizer_launch.py file:
Code
from launch import LaunchDescriptionfrom launch.actions import ExecuteProcess, IncludeLaunchDescriptionfrom launch.launch_description_sources import AnyLaunchDescriptionSourcefrom launch_ros.substitutions import FindPackageSharefrom launch.substitutions import PathJoinSubstitutiondef generate_launch_description():# Path to the rosbridge launch file rosbridge_launch = PathJoinSubstitution([ FindPackageShare('rosbridge_server'),'launch','rosbridge_websocket_launch.xml' ])# Path to the built web directory web_build_dir = PathJoinSubstitution([ FindPackageShare('mav_imu_visualizer'),'web' ])return LaunchDescription([# Start the HTTP server to serve web files ExecuteProcess( cmd=['python3', '-m', 'http.server', '8000', '--directory', web_build_dir], name='http_server', output='screen' ),# Start the rosbridge server IncludeLaunchDescription( AnyLaunchDescriptionSource(rosbridge_launch) ), ])
This launch file will start a HTTP server to serve the web files and a rosbridge server to communicate with the ROS 2 nodes. As we will be using javascript to visualize the data in our web application, we need to start the rosbridge server to communicate with the ROS 2 nodes.
16.7 Create a new web directory
Create a new web directory inside /workspaces/mavlab/ros2_ws/src/student/tut_05/mav_imu_visualizer/ to house the web application.
cd /workspaces/mavlab/ros2_ws/src/student/tut_05/mav_imu_visualizermkdir web
Create a new index.html file inside /workspaces/mavlab/ros2_ws/src/student/tut_05/mav_imu_visualizer/web
cd /workspaces/mavlab/ros2_ws/src/student/tut_05/mav_imu_visualizer/webtouch index.html
Copy the following code into the index.html file:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/><title>Live IMU Visualization</title><!-- Include Chart.js (from CDN or your local node_modules build) --><!-- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> --><script src="node_modules/chart.js/dist/chart.umd.js"></script><!-- Include ROSLIBJS (from your node_modules or a CDN) --><script src="node_modules/roslib/build/roslib.min.js"></script><style> body {font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;max-width:1200px;margin:0auto;padding:20px;background-color:#f5f5f5; } h1, h2 {color:#2c3e50;text-align:center;margin-top:30px; } h1 {font-size:2.5em;margin-bottom:40px;border-bottom:2pxsolid#3498db;padding-bottom:10px; }.chart-container {background:white;border-radius:10px;box-shadow:04px6pxrgba(0,0,0,0.1);padding:20px;margin:20pxauto;max-width:800px; } canvas {max-width:100%;height:auto;margin:10pxauto;display:block; }</style></head><body><h1>IMU Data Visualization</h1><div class="chart-container"><h2>Acceleration (m/s²)</h2><canvas id="accChart"></canvas></div><div class="chart-container"><h2>Angular Velocity (rad/s)</h2><canvas id="gyroChart"></canvas></div><div class="chart-container"><h2>Euler Angles (degrees)</h2><canvas id="rollChart"></canvas><canvas id="pitchChart"></canvas><canvas id="yawChart"></canvas></div><!-- Main JavaScript file --><script src="main.js"></script></body></html>
This file is a simple HTML file that will be used to visualize the IMU data. It contains a header with the title of the web application and a main section with three charts to visualize the acceleration, angular velocity and Euler angles.
Create a new main.js file inside /workspaces/mavlab/ros2_ws/src/student/tut_05/mav_imu_visualizer/web
cd /workspaces/mavlab/ros2_ws/src/student/tut_05/mav_imu_visualizer/webtouch main.js
Copy the following code into the main.js file:
// Connect to rosbridge websocket (adjust the URL as needed)var ros =new ROSLIB.Ros({url:'ws://localhost:9090' }); ros.on('connection',function () {console.log('Connected to rosbridge.'); }); ros.on('error',function (error) {console.error('Error connecting to rosbridge: ', error); });// Create a subscriber to the /imu/data topicvar imuTopic =new ROSLIB.Topic({ros: ros,name:'/imu/data',messageType:'sensor_msgs/Imu' });// Helper: convert quaternion to Euler anglesfunctionquaternionToEuler(q) {var x = q.x, y = q.y, z = q.z, w = q.w;var sinr_cosp =2* (w * x + y * z);var cosr_cosp =1-2* (x * x + y * y);var roll =Math.atan2(sinr_cosp, cosr_cosp) *180/Math.PI;var sinp =2* (w * y - z * x);var pitch =Math.abs(sinp) >=1?Math.sign(sinp) *Math.PI/2:Math.asin(sinp) *180/Math.PI;var siny_cosp =2* (w * z + x * y);var cosy_cosp =1-2* (y * y + z * z);var yaw =Math.atan2(siny_cosp, cosy_cosp) *180/Math.PI;return { roll: roll,pitch: pitch,yaw: yaw }; }// Set up Chart.js chartsfunctioncreateChart(ctx, label, borderColor) {returnnewChart(ctx, {type:'line',data: {labels: [],// time or sample indexdatasets: [{label: label,data: [],borderColor: borderColor,fill:false,tension:0.1, }] },options: {animation:false,responsive:true,scales: { x: { display:false } } } }); }// Get canvas contextsvar accCtx =document.getElementById('accChart').getContext('2d');var gyroCtx =document.getElementById('gyroChart').getContext('2d');var orientationCtx =document.getElementById('rollChart').getContext('2d');// Create charts for each data type.// For acceleration and angular velocity, we use one chart per topic (each with three datasets for x, y, z).var accChart =newChart(accCtx, {type:'line',data: {labels: [],datasets: [ { label:'Acc X',data: [],borderColor:'red',fill:false }, { label:'Acc Y',data: [],borderColor:'green',fill:false }, { label:'Acc Z',data: [],borderColor:'blue',fill:false }, ] },options: { animation:false,responsive:true,scales: { x: { display:false } } } });var gyroChart =newChart(gyroCtx, {type:'line',data: {labels: [],datasets: [ { label:'Gyro X',data: [],borderColor:'red',fill:false }, { label:'Gyro Y',data: [],borderColor:'green',fill:false }, { label:'Gyro Z',data: [],borderColor:'blue',fill:false }, ] },options: { animation:false,responsive:true,scales: { x: { display:false } } } });// Create one chart for all orientation anglesvar orientationChart =newChart(orientationCtx, {type:'line',data: {labels: [],datasets: [ { label:'Roll',data: [],borderColor:'purple',fill:false }, { label:'Pitch',data: [],borderColor:'orange',fill:false }, { label:'Yaw',data: [],borderColor:'brown',fill:false }, ] },options: { animation:false,responsive:true,scales: { x: { display:false } } } });var sampleIndex =0;functionupdateAccChart(ax, ay, az) { accChart.data.labels.push(sampleIndex); accChart.data.datasets[0].data.push(ax); accChart.data.datasets[1].data.push(ay); accChart.data.datasets[2].data.push(az);if (accChart.data.labels.length>100) { accChart.data.labels.shift(); accChart.data.datasets.forEach(ds => ds.data.shift()); } accChart.update(); }functionupdateGyroChart(gx, gy, gz) { gyroChart.data.labels.push(sampleIndex); gyroChart.data.datasets[0].data.push(gx); gyroChart.data.datasets[1].data.push(gy); gyroChart.data.datasets[2].data.push(gz);if (gyroChart.data.labels.length>100) { gyroChart.data.labels.shift(); gyroChart.data.datasets.forEach(ds => ds.data.shift()); } gyroChart.update(); }functionupdateOrientationChart(roll, pitch, yaw) { orientationChart.data.labels.push(sampleIndex); orientationChart.data.datasets[0].data.push(roll); orientationChart.data.datasets[1].data.push(pitch); orientationChart.data.datasets[2].data.push(yaw);if (orientationChart.data.labels.length>100) { orientationChart.data.labels.shift(); orientationChart.data.datasets.forEach(ds => ds.data.shift()); } orientationChart.update(); }// Subscribe to the /imu/data topic imuTopic.subscribe(function (message) {// Increment our sample index for the x-axis sampleIndex++;// Extract accelerations and angular velocitiesvar acc = message.linear_acceleration;// {x, y, z}var gyro = message.angular_velocity;// {x, y, z}// Convert quaternion to Euler anglesvar euler =quaternionToEuler(message.orientation);// returns {roll, pitch, yaw}// Update the chartsupdateAccChart(acc.x, acc.y, acc.z);updateGyroChart(gyro.x, gyro.y, gyro.z);updateOrientationChart(euler.roll, euler.pitch, euler.yaw); });
This file is a javascript file that will be used to visualize the IMU data. It contains a subscriber to the /imu/data topic and a function to update the charts with the latest data.
16.8 Install npm packages
Install the npm packages by running the following command inside the web directory.
cd /workspaces/mavlab/ros2_ws/src/student/tut_05/mav_imu_visualizer/webnpm init -ynpm install chart.js roslib
This will install the chart.js and roslib packages that will be used by our javascript file main.js to visualize the IMU data.
16.9 Modify the setup.py file
Modify the setup.py file to include the chart.js and roslib packages.
cd /workspaces/mavlab/ros2_ws/src/student/tut_05/mav_imu_visualizer
Overwrite the existing setup.py file with the following code:
Navigate to the web page http://localhost:8000 in your browser
You should see the IMU data visualizer. If you are running the bag file from the previous section, you will see the IMU data getting plotted in the web application.
During the evaluation, the instructor will launch the IMU sensor. You will then open the web application in your browser and see the IMU data in real-time.
16.13 Instructions for the day of the tutorial
Connect to the mavlab network (ask the instructor for the password)
Launch the mav_imu_visualizer ROS2 package as instructed above
Open the web application in your browser
The instructor will launch the IMU sensor and you will see the IMU data in real-time in the web application
16.14 Evaluation
The evaluation of the tutorial will be performed in the classroom. Notice that this is a group activity. You will be working in a group of 4 students. The evaluation will be performed on the group level. In order to pass the tutorial, the group must pass all the tests in the GitHub Actions workflow and also be able to answer all the questions during the evaluation in the classroom. Those who are not present in the classroom on the day of evaluation will not be able to participate in the evaluation and will automatically fail the tutorial.
While one of the objectives of these tutorials is to give you hands on experience with coding and practical implementation, the more important goal is to help you relate what you are doing in your tutorials to the underlying concepts discussed in the class. Therefore, while you are working on the implementation tasks, make sure you are also thinking about the underlying concepts and how they are being applied in the code.
Some questions to think about while you are working on the implementation tasks:
Is your code able to pass all the tests in the GitHub Actions workflow?
Are you able to show plotting of the live IMU data during the evaluation?
When echoing the /imu/data topic, what is the message type?
What is meant by the term quaternion?
How do you convert a quaternion to Euler angles?
What is the difference between angular_velocity and angular_velocity_covariance?
What is the difference between linear_acceleration and linear_acceleration_covariance?
What is a covariance matrix? What does it tell us about the measurement?
Do you think the covariance has any practical significance in real life applications of the IMU?
What is the size of the covariance matrix corresponding to the orientation? Is this consistent with the measurement being reported?
If you were asked to choose between two IMUs, how would you compare them?
From the plots of the data provided in the plots folder what can you infer about the angular velocity and linear acceleration of the IMU?
How does the sensor measure angular velocity and linear acceleration?
How does the sensor measure orientation?
16.15 Instructor Feedback through Pull Requests
Commit your changes to the remote repository often. This will help you to track your progress and also help you to revert to a working version if you make a mistake. In addition, the instructor and TA will be able to provide you with feedback on your code through the Feeback pull request available in your repository. Please do not close this pull request.