Seamless Communication between Jetson Nano and ESP32 with MicroROS

Kabilankb
8 min readMay 1, 2024

--

Introduction

Robotics enthusiasts and professionals alike are constantly seeking ways to enhance robot teleoperation, enabling more seamless control and interaction. MicroROS, a lightweight implementation of the Robot Operating System (ROS) 2, offers a promising avenue for achieving this goal. In this blog post, we’ll delve into how MicroROS interface can be utilized on the Jetson Nano platform for efficient robot teleoperation, opening up exciting possibilities for robotics applications using ESP32.

What is MicroROS?

MicroROS is designed to bring ROS 2 capabilities to microcontrollers and microcomputers, enabling real-time, resource-constrained robotic systems to leverage ROS 2 features. It provides a lightweight middleware solution suitable for embedded systems, making it ideal for robotics applications where resource efficiency is paramount.

Integration of MicroROS on Jetson Nano

By integrating MicroROS with the Jetson Nano platform, developers can harness the combined power of ROS 2 and NVIDIA’s hardware acceleration for advanced robot teleoperation. The process involves setting up MicroROS on the Jetson Nano and establishing communication with ROS 2 nodes running on other devices or robots within the network.

Follow the Tutorial For Microros Installing

https://youtu.be/DxGftRaD9k4

Getting Started with MicroROS on Jetson Nano

  1. Install MicroROS on Jetson Nano using the provided installation instructions.
  2. Configure MicroROS communication parameters to establish connectivity with ESP32.
  3. Develop teleoperation interfaces using ROS 2 packages such as ros2_teleop or custom-built solutions tailored to specific application requirements.
  4. Test and validate the teleoperation setup in real-world environments, iterating as necessary to optimize performance and usability.

Connect an ESP32 module and write code for Micro-ROS

/Motor controller using micro_ros serial set_microros_transports/
#include <micro_ros_arduino.h>
#include <stdio.h>
#include <rcl/rcl.h>
#include <rcl/error_handling.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <geometry_msgs/msg/twist.h>
#include <std_msgs/msg/int32.h>
#include <nav_msgs/msg/odometry.h>
#include <geometry_msgs/msg/twist.h>
#include <geometry_msgs/msg/vector3.h>
//pin declaration
//Left wheel
int8_t L_FORW = 26;
int8_t L_BACK = 27;
int8_t L_enablePin = 25;
int8_t L_encoderPin1 = 18; //Encoder Output of pin1 must connected with intreput pin of Esp32.
int8_t L_encoderPin2 = 21;
//right wheel
int8_t R_FORW = 33;
int8_t R_BACK = 32;
int8_t R_enablePin = 5;
int8_t R_encoderPin1 = 23; //Encoder Output of pin1 must connected with intreput pin of Esp32.
int8_t R_encoderPin2 = 15;
//parameters of the robot
float wheels_y_distance_ = 0.1;
float wheel_radius = 0.02;
float wheel_circumference_ = 2 * 3.14 * wheel_radius;
//encoder value per revolution of left wheel and right wheel
int tickPerRevolution_LW = 1055;
int tickPerRevolution_RW = 1055;
int threshold = 0;
//pid constants of left wheel
float kp_l = 1.8;
float ki_l = 5;
float kd_l = 0.1;
//pid constants of right wheel
float kp_r = 2.25;
float ki_r = 5;
float kd_r = 0.1;
//pwm parameters setup
const int freq = 30000;
const int pwmChannelL = 0;
const int pwmChannelR = 1;
const int resolution = 8;
rcl_subscription_t subscriber;
geometry_msgs__msg__Twist msg;
rclc_executor_t executor;
rcl_allocator_t allocator;
rclc_support_t support;
rcl_node_t node;
rcl_publisher_t odom_publisher;
std_msgs__msg__Int32 encodervalue_l;
std_msgs__msg__Int32 encodervalue_r;
nav_msgs__msg__Odometry odom_msg;
rcl_timer_t timer;
rcl_timer_t ControlTimer;
unsigned long long time_offset = 0;
unsigned long prev_cmd_time = 0;
unsigned long prev_odom_update = 0;
//creating a class for motor control
class MotorController {
public:
int8_t Forward;
int8_t Backward;
int8_t Enable;
int8_t EncoderPinA;
int8_t EncoderPinB;
std_msgs__msg__Int32 EncoderCount;
volatile long CurrentPosition;
volatile long PreviousPosition;
volatile long CurrentTime;
volatile long PreviousTime;
volatile long CurrentTimeforError;
volatile long PreviousTimeForError;
float rpmFilt;
float eintegral;
float ederivative;
float rpmPrev;
float kp;
float ki;
float kd;
float error;
float previousError = 0;
int tick;
MotorController(int8_t ForwardPin, int8_t BackwardPin, int8_t EnablePin, int8_t EncoderA, int8_t EncoderB, int tickPerRevolution) {
this->Forward = ForwardPin;
this->Backward = BackwardPin;
this->Enable = EnablePin;
this->EncoderPinA = EncoderA;
this->EncoderPinB = EncoderB;
this->tick = tickPerRevolution;
pinMode(Forward, OUTPUT);
pinMode(Backward, OUTPUT);
pinMode(EnablePin, OUTPUT);
pinMode(EncoderPinA, INPUT);
pinMode(EncoderPinB, INPUT);
}
//initializing the parameters of PID controller
void initPID(float proportionalGain, float integralGain, float derivativeGain) {
kp = proportionalGain;
ki = integralGain;
kd = derivativeGain;
}
//function return rpm of the motor using the encoder tick values
float getRpm() {
CurrentPosition = EncoderCount.data;
CurrentTime = millis();
float delta1 = ((float)CurrentTime - PreviousTime) / 1.0e3;
float velocity = ((float)CurrentPosition - PreviousPosition) / delta1;
float rpm = (velocity / tick) * 60;
rpmFilt = 0.854 * rpmFilt + 0.0728 * rpm + 0.0728 * rpmPrev;
float rpmPrev = rpm;
PreviousPosition = CurrentPosition;
PreviousTime = CurrentTime;
// Serial.println(rpmFilt);
return rpmFilt;
}
//pid controller
float pid(float setpoint, float feedback) {
CurrentTimeforError = millis();
float delta2 = ((float)CurrentTimeforError - PreviousTimeForError) / 1.0e3;
error = setpoint - feedback;
eintegral = eintegral + (error * delta2);
ederivative = (error - previousError) / delta2;
float control_signal = (kp * error) + (ki * eintegral) + (kd * ederivative);
previousError = error;
PreviousTimeForError = CurrentTimeforError;
return control_signal;
}
//move the robot wheels based the control signal generated by the pid controller
void moveBase(float ActuatingSignal, int threshold, int pwmChannel) {
if (ActuatingSignal > 0) {
digitalWrite(Forward, HIGH);
digitalWrite(Backward, LOW);
} else {
digitalWrite(Forward, LOW);
digitalWrite(Backward, HIGH);
}
int pwm = threshold + (int)fabs(ActuatingSignal);
if (pwm > 255)
pwm = 255;
ledcWrite(pwmChannel, pwm);
}
void stop() {
digitalWrite(Forward, LOW);
digitalWrite(Backward, LOW);
}
// void plot(float Value1, float Value2){
// Serial.print("Value1:");
// Serial.print(Value1);
// Serial.print(",");
// Serial.print("value2:");
// Serial.println(Value2);
// }
};
//creating objects for right wheel and left wheel
MotorController leftWheel(L_FORW, L_BACK, L_enablePin, L_encoderPin1, L_encoderPin2, tickPerRevolution_LW);
MotorController rightWheel(R_FORW, R_BACK, R_enablePin, R_encoderPin1, R_encoderPin2, tickPerRevolution_RW);
#define LED_PIN 2
#define RCCHECK(fn) \
{ \
rcl_ret_t temp_rc = fn; \
if ((temp_rc != RCL_RET_OK)) { error_loop(); } \
}
#define RCSOFTCHECK(fn) \
{ \
rcl_ret_t temp_rc = fn; \
if ((temp_rc != RCL_RET_OK)) { error_loop(); } \
}
void error_loop() {
while (1) {
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
delay(100);
}
}
//subscription callback function
void setup() {
//initializing the pid constants
leftWheel.initPID(kp_l, ki_l, kd_l);
rightWheel.initPID(kp_r, ki_r, kd_r);
//initializing interrupt functions for counting the encoder tick values
attachInterrupt(digitalPinToInterrupt(leftWheel.EncoderPinB), updateEncoderL, RISING);
attachInterrupt(digitalPinToInterrupt(rightWheel.EncoderPinA), updateEncoderR, RISING);
//initializing pwm signal parameters
ledcSetup(pwmChannelL, freq, resolution);
ledcAttachPin(leftWheel.Enable, pwmChannelL);
ledcSetup(pwmChannelR, freq, resolution);
ledcAttachPin(rightWheel.Enable, pwmChannelR);
set_microros_transports();
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH);
delay(2000);
allocator = rcl_get_default_allocator();
//create init_options
RCCHECK(rclc_support_init(&support, 0, NULL, &allocator));
// create node
RCCHECK(rclc_node_init_default(&node, "micro_ros_esp32_node", "", &support));
// create subscriber for cmd_vel topic
RCCHECK(rclc_subscription_init_default(
&subscriber,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(geometry_msgs, msg, Twist),
"cmd_vel"));
//create a odometry publisher
RCCHECK(rclc_publisher_init_default(
&odom_publisher,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(nav_msgs, msg, Odometry),
"odom/unfiltered"));
//timer function for controlling the motor base. At every samplingT time
//MotorControll_callback function is called
//Here I had set SamplingT=10 Which means at every 10 milliseconds MotorControll_callback function is called
const unsigned int samplingT = 20;
RCCHECK(rclc_timer_init_default(
&ControlTimer,
&support,
RCL_MS_TO_NS(samplingT),
MotorControll_callback));
// create executor
RCCHECK(rclc_executor_init(&executor, &support.context, 2, &allocator));
RCCHECK(rclc_executor_add_subscription(&executor, &subscriber, &msg, &subscription_callback, ON_NEW_DATA));
// RCCHECK(rclc_executor_add_timer(&executor, &timer));
RCCHECK(rclc_executor_add_timer(&executor, &ControlTimer));
}
void loop() {
// put your main code here, to run repeatedly:
delay(100);
RCCHECK(rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100)));
}
void subscription_callback(const void* msgin) {
prev_cmd_time = millis();
}
//function which controlles the motor
void MotorControll_callback(rcl_timer_t* timer, int64_t last_call_time) {
float linearVelocity;
float angularVelocity;
//linear velocity and angular velocity send cmd_vel topic
linearVelocity = msg.linear.x;
angularVelocity = msg.angular.z;
//linear and angular velocities are converted to leftwheel and rightwheel velocities
float vL = (linearVelocity - (angularVelocity * 1 / 2)) * 20;
float vR = (linearVelocity + (angularVelocity * 1 / 2)) * 20;
//current wheel rpm is calculated
float currentRpmL = leftWheel.getRpm();
float currentRpmR = rightWheel.getRpm();
//pid controlled is used for generating the pwm signal
float actuating_signal_LW = leftWheel.pid(vL, currentRpmL);
float actuating_signal_RW = rightWheel.pid(vR, currentRpmR);
if (vL == 0 && vR == 0) {
leftWheel.stop();
rightWheel.stop();
actuating_signal_LW = 0;
actuating_signal_RW = 0;
} else {
rightWheel.moveBase(actuating_signal_RW, threshold, pwmChannelR);
leftWheel.moveBase(actuating_signal_LW, threshold, pwmChannelL);
}
//odometry
//float average_rps_x = ((float)(currentRpmL + currentRpmR) / 2) / 60.0; // RPM
// float linear_x = average_rps_x * wheel_circumference_; // m/s
// float average_rps_a = ((float)(-currentRpmL + currentRpmR) / 2) / 60.0;
//float angular_z = (average_rps_a * wheel_circumference_) / (wheels_y_distance_ / 2.0); // rad/s
//float linear_y = 0;
//unsigned long now = millis();
//float vel_dt = (now - prev_odom_update) / 1000.0;
//prev_odom_update = now;
//odometry.update(
//vel_dt,
//linear_x,
// linear_y,
//angular_z);
// publishData();
}
//interrupt function for left wheel encoder.
void updateEncoderL() {
if (digitalRead(leftWheel.EncoderPinB) > digitalRead(leftWheel.EncoderPinA))
leftWheel.EncoderCount.data++;
else
leftWheel.EncoderCount.data - ;
encodervalue_l = leftWheel.EncoderCount;
}
//interrupt function for right wheel encoder
void updateEncoderR() {
if (digitalRead(rightWheel.EncoderPinA) > digitalRead(rightWheel.EncoderPinB))
rightWheel.EncoderCount.data++;
else
rightWheel.EncoderCount.data - ;
encodervalue_r = rightWheel.EncoderCount;
}
//function which publishes wheel odometry.
void publishData() {
// odom_msg = odometry.getData();
;
struct timespec time_stamp = getTime();
odom_msg.header.stamp.sec = time_stamp.tv_sec;
odom_msg.header.stamp.nanosec = time_stamp.tv_nsec;
RCSOFTCHECK(rcl_publish(&odom_publisher, &odom_msg, NULL));
}
void syncTime() {
// get the current time from the agent
unsigned long now = millis();
RCCHECK(rmw_uros_sync_session(10));
unsigned long long ros_time_ms = rmw_uros_epoch_millis();
// now we can find the difference between ROS time and uC time
time_offset = ros_time_ms - now;
}
struct timespec getTime() {
struct timespec tp = { 0 };
// add time difference between uC time and ROS time to
// synchronize time with ROS
unsigned long long now = millis() + time_offset;
tp.tv_sec = now / 1000;
tp.tv_nsec = (now % 1000) * 1000000;
return tp;
}
Flow diagram

Connect the ESP32 with Jetson

Jetson and ESP32
Configuration

Configure MicroROS communication parameters to establish connectivity with Microros esp32

To connect the ESP32 to the Jetson Nano, you’ll first need to establish a serial connection between them. Once connected, you can determine the port name by checking the device manager or using terminal commands. For instance, you might use the `ls /dev/tty*` command in Linux to list available serial ports and identify the one associated with your ESP32.

ls -l /dev |grep ttyUSB

To launch the MicroROS agent from the source, you’ll typically navigate to the directory where the agent source code is located and execute the appropriate command. Assuming you have the necessary dependencies installed and the MicroROS agent source code cloned locally, here’s a general outline of the steps:

  1. Open a terminal window.
  2. Navigate to the directory where the MicroROS agent source code is located using the cd command.
  3. Once in the directory, you may need to source any setup files or environment variables required for the agent. This might involve running a command like source setup.bash or similar, depending on how the project is configured.
  4. After sourcing, you can launch the agent using the appropriate command. This could be something like ros2 run micro_ros_agent micro_ros_agent, but the exact command may vary depending on the specifics of your setup and how the agent is structured.

Than open new terminal find ros2 topic

To launch the teleoperation node for ROS teleoperation, you typically need to have the teleoperation package installed and configured properly in your ROS environment. Assuming you have the necessary package installed, here’s how you can launch the teleoperation node:

Open rqt and visulize

Conclusion:

The combination of MicroROS interface and Jetson Nano opens up exciting possibilities for enhancing robot teleoperation in various robotics applications. By leveraging the lightweight and real-time capabilities of MicroROS on the powerful Jetson Nano platform, developers can create efficient and scalable teleoperation solutions for diverse robotic systems. Whether it’s enabling remote control of robots, facilitating human-robot interaction, or integrating autonomy algorithms, MicroROS on Jetson Nano represents a significant step forward in advancing the field of intelligent robotics.

--

--