Motivation
RPI Zero W is a low cost computer, succeded by RPI Zero 2 W, still popular in many applications, especially right now when shortage hit the electronic market all around the world. It’s also much slower than his younger brother therefore makes it more challenging to use. Today you are going to learn how to survive installing all packages for IMU.
Prerequisites
You should have RPI Zero W prepared from the previous tutorial, where you install ROS Melodic on RPI Zero W with Rasbian Buster.
Installation
Workspace
Create separate workspace in your home directory
mkdir -p ~/catkin_ws/src/
cd ~/catkin_ws/src/
Git clone required packages
git clone -b indigo-devel https://github.com/ros/actionlib.git
git clone https://github.com/ros/angles
git clone -b kinetic-devel https://github.com/ros/bond_core.git
git clone -b indigo-devel https://github.com/ros/class_loader
git clone -b noetic-devel https://github.com/ros/common_msgs
git clone -b master https://github.com/ros/dynamic_reconfigure.git
git clone -b indigo-devel https://github.com/ros/geometry.git
git clone -b noetic-devel https://github.com/ros/geometry2
git clone -b kinetic-devel https://github.com/ros/pluginlib.git
git clone -b main https://github.com/GAVLab/ros_icm20948
git clone -b kinetic https://github.com/ccny-ros-pkg/imu_tools.git
Enter imu_tools
and remove rviz_imu_plugin
. We are not needing this one since we have neither rviz nor desktop installed. Also, it causes errors if you do not remove it beforehand.
git clone -b indigo-devel https://github.com/ros/nodelet_core
git clone -b master https://github.com/orocos/orocos_kinematics_dynamics.git
Enter orocos_kinematics_dynamics
and follow instructions described here. However, if you are lazy and willing to take a risk of outdated commands feel free to execute lines below.
Install required packages.
sudo apt-get install python-psutil python-future
sudo apt-get install libeigen3-dev libcppunit-dev
Install the submodule.
git submodule update --init #installing submodule for PyBind11
Installing missing packages for ros_icm20948
Install packages. based on original source
sudo apt-get install i2c-tools wiringpi python3-scipy python-pip
Install some python3.6 packages
python3.6 -m pip install Cython numpy ahrs sparkfun-qwiic-icm20948 pyyaml PyYAML
python3.6 -m pip install adafruit-blinka==6.10.0 adafruit-circuitpython-icm20x==2.0.7 Adafruit-PlatformDetect=3.6.0
python3.6 -m pip install adafruit-circuitpython-busdevice==5.0.6 adafruit-python-shell
Install some python2 packages (yes, you have to downgrade pyparsing 🤷)
pip install pyyaml PyYAML pyparsing==2.4.7
These are the versions of the packages that are proven to work for python3.6 on RPI with melodic. However In case you missed one tutorial with installing python3.6 on RPI please go here.
Add I2C permission
Add i2c-devl
to boot with
sudo nano /etc/modules-load.d/modules.conf
You may find i2c-dev
already in the modules.conf
if you enabled I2C communication in previous tutorial
Before you build anything be sure to have bullet
library installed.
sudo apt-get install libbullet-dev
Building time!
Build the workspace with
catkin_make_isolated
It will take a while so here is something to keep you happy.
After the workspace is built, go to
sudo nano /etc/dhcpcd.conf
and paste at the end configuration coresponding to you LAN setup. In my case it looks like this. It sets static IP address, gateway and DNS for the RPI on the wifi interface.
interface wlan0
static ip_address=192.168.1.101/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1
Make sure that in /etc/wpa_supplicant/wpa_supplicant.conf
you have your network SSID with password key added.
now you can reboot, log in and continue in the next section.
ROS nodes
Enter ~/catkin_ws/src/
and create new package with catkin_tools
catkin_create_pkg imu_rpi std_msgs rospy roscpp
Test script
Go to src
folder of the newly created package and create test script nano imu_test.py
.
from __future__ import print_function
import qwiic_icm20948
import time
import sys
def runExample():
print("\nSparkFun 9DoF ICM-20948 Sensor Example 1\n")
IMU = qwiic_icm20948.QwiicIcm20948()
if IMU.connected == False:
print("The Qwiic ICM20948 device isn't connected to the system. Please check your connection", \
file=sys.stderr)
return
IMU.begin()
while True:
if IMU.dataReady():
IMU.getAgmt() # read all axis and temp from sensor, note this also updates all instance variables
print("Acceleration: X:%.2f, Y: %.2f, Z: %.2f m/s^2" % (IMU.axRaw,IMU.ayRaw,IMU.azRaw))
print("Gyro X:%.2f, Y: %.2f, Z: %.2f rads/s" % (IMU.gxRaw,IMU.gyRaw,IMU.gzRaw))
print("Magnetometer X:%.2f, Y: %.2f, Z: %.2f uT" % (IMU.mxRaw,IMU.myRaw,IMU.mzRaw))
print("")
time.sleep(0.03)
else:
print("Waiting for data")
time.sleep(0.5)
if __name__ == '__main__':
try:
runExample()
except (KeyboardInterrupt, SystemExit) as exErr:
print("\nEnding Example 1")
sys.exit(0)
If above code works you are good to go and create ROS node.
IMU publisher
Create in the src
the file nano imu_talker.py
. Then paste the contents of the file below.
#!/usr/bin/env python3
import rospy
import sys
from sensor_msgs.msg import MagneticField,Imu
from std_msgs.msg import Float64
import time
import board
import busio
import os
import numpy as np
from adafruit_icm20x import ICM20948,AccelRange,GyroRange
def imu_node():
print("dupa")
raw_pub = rospy.Publisher('imu/data_raw', Imu, queue_size=10)
mag_pub = rospy.Publisher('imu/mag', MagneticField, queue_size=10)
rospy.init_node('icm20948')
rate = rospy.Rate(100)
rospy.loginfo(rospy.get_caller_id() + " icm20948 node launched.")
i2c = busio.I2C(board.SCL, board.SDA)
icm = ICM20948(i2c)
icm.accelerometer_range = AccelRange.RANGE_4G # Options: RANGE_2G, RANGE_4G, RANGE_8G, RANGE_16G
rospy.loginfo(rospy.get_caller_id() + " Initializing IMU module...")
time.sleep(5)
rospy.loginfo(rospy.get_caller_id() + " IMU module set")
if icm.gyro_range == 0:
gyro_range = 250
elif icm.gyro_range == 1:
gyro_range = 500
elif icm.gyro_range == 2:
gyro_range = 1000
elif icm.gyro_range == 3:
gyro_range = 2000
else:
gyro_range = i
time.sleep(1)
frequency = 100 # frequency in Hertz
rospy.loginfo(rospy.get_caller_id() + " starting to publish")
while not rospy.is_shutdown():
acc_data = icm.acceleration # linear acceleration (m/s^2) x,y,z
gyr_data = icm.gyro # angular velocity (rad/s) x,y,z
mag_data = tuple(i for i in icm.magnetic) # magnetic field (uT) x,y,z
rospy.loginfo(rospy.get_caller_id() + " publishing")
raw_msg = Imu()
raw_msg.header.frame_id = "imu"
raw_msg.header.stamp = rospy.Time.now()
raw_msg.orientation.w = 0
raw_msg.orientation.x = 0
raw_msg.orientation.y = 0
raw_msg.orientation.z = 0
raw_msg.linear_acceleration.x = acc_data[0]
raw_msg.linear_acceleration.y = acc_data[1]
raw_msg.linear_acceleration.z = acc_data[2]
raw_msg.angular_velocity.x = gyr_data[0]
raw_msg.angular_velocity.y = gyr_data[1]
raw_msg.angular_velocity.z = gyr_data[2]
raw_msg.orientation_covariance[0] = -1
raw_msg.linear_acceleration_covariance[0] = -1
raw_msg.angular_velocity_covariance[0] = -1
raw_pub.publish(raw_msg)
mag_msg = MagneticField()
mag_msg.header.stamp = rospy.Time.now()
mag_msg.magnetic_field.x = mag_data[0]
mag_msg.magnetic_field.y = mag_data[1]
mag_msg.magnetic_field.z = mag_data[2]
mag_msg.magnetic_field_covariance[0] = -1
mag_pub.publish(mag_msg)
rate.sleep()
rospy.loginfo(rospy.get_caller_id() + " icm20948 node finished")
if __name__ == '__main__':
try:
imu_node()
except rospy.ROSInterruptException:
rospy.loginfo(rospy.get_caller_id() + " icm20948 node exited with exception.")
It lets you convert raw readings of the IMU module into a ROS topic with Imu
message type.
TF broadcaster (extra)
If you want to have some other frame assinged to the IMU frame because of numerous reasons (e.g. different orientation on robot than expected).
Create nano custom_tf_broadcaster.py
and paste:
#!/usr/bin/env python
### IMU pose broadcaster
# Broadcaster of TF for imu link with respect to the base link
# Prints the tf orientation of imu link wrt base
# USED AT IMU CUBE ONLY
import roslib
import rospy
from sensor_msgs.msg import Imu
import math
import tf
import tf2_ros
from tf.transformations import euler_from_quaternion, quaternion_from_euler, quaternion_multiply, quaternion_about_axis
import geometry_msgs.msg
import time
class Imu_pose_broadcaster:
def __init__(self):
print("Initializing.....")
time.sleep(5)
print("Go!")
rospy.init_node('erc_imu_broadcaster',anonymous=True)
self.check_service = rospy.Subscriber("/imu/data",Imu,self.callback)
self.listener = tf.TransformListener()
rospy.spin()
def callback(self,data):
q_rot = quaternion_about_axis(math.pi/2.0,(0,1,0))
quat= q_rot
static_transformStamped = geometry_msgs.msg.TransformStamped()
static_transformStamped.header.stamp = rospy.Time.now()
static_transformStamped.header.frame_id = "imu"
static_transformStamped.child_frame_id = "imu_panel"
static_transformStamped.transform.translation.x = 0
static_transformStamped.transform.translation.y = 0
static_transformStamped.transform.translation.z = 0
static_transformStamped.transform.rotation.x = quat[0]
static_transformStamped.transform.rotation.y = quat[1]
static_transformStamped.transform.rotation.z = quat[2]
static_transformStamped.transform.rotation.w = quat[3]
br = tf2_ros.StaticTransformBroadcaster()
br.sendTransform(static_transformStamped)
try:
(trans, rot) = self.listener.lookupTransform('/imu_panel', '/base', rospy.Time(0))
except (tf.LookupException, tf.ConnectivityException, tf.ExtrapolationException):
print("no transform")
roll, pitch, yaw = euler_from_quaternion(rot)
roll = math.degrees(roll)
pitch = math.degrees(pitch)
yaw = math.degrees(yaw)
print("TF Roll:", "{:.2f}".format(roll), "Pitch:", "{:.2f}".format(pitch), "Yaw:",
"{:.2f}".format(yaw))
if __name__ == '__main__':
ipb = Imu_pose_broadcaster()
Launch files
Go back to root folder of the package and create launch folder mkdir launch
. Get inside and nano imu.launch
, then paste.
<launch>
<node name="imu_rpi_node" pkg="imu_rpi" type="imu_talker.py" respawn="true" respawn_delay="2" >
</node>
</launch>
Then create a final launch file for all required packages.
Create nano start.launch
<launch>
#### IMU Driver
<include file="$(find imu_rpi_node)/launches/imu.launch" />
#### Complementary filter
<node pkg="imu_complementary_filter" type="complementary_filter_node"
name="complementary_filter_gain_node" output="screen" >
<param name="do_bias_estimation" value="true"/>
<param name="do_adaptive_gain" value="true"/>
<param name="use_mag" value="false"/>
<param name="gain_acc" value="0.01"/>
<param name="gain_mag" value="0.01"/>
<param name="publish_tf" value="true"/>
</node>
#### Custom tf broadcaster (optional)
<node name="custom_tf_broadcaster_node" pkg="imu_rpi" type="custom_tf_broadcaster.py"
name="custom_tf_broadcaster" output="screen"/>
</launch>
Rebuild workspace
After creating all files, rebuild the workspace. It will take less time now, because most of packages are already built. Remember to use catkin_make_isolated
.
Start script
At home directory create nano run.sh
and paste following contents.
##!/bin/bash
# RPI IMU
# export ROS_MASTER_URI=http://192.168.1.121:11311
export ROS_IP=192.168.1.101
source ~/catkin_ws/devel_isolated/setup.bash
roslaunch imu_rpi start.launch
Remember to customize ROS_MASTER_URI
based on your network configuration.
Test!
Reboot and run bash run.sh
. Observe the effect!