Tutorial: Creating expressions with LEDs

🏁 Goal of this tutorial

By the end of this tutorial, you will know how to request different type of LEDs effects using the LEDs devices available on your robot. Additionally, you will see an example of how LEDs can be used for expressiveness purposes.

Pre-requisites

  • Make sure you are familiar with the robot’s LEDs API

Creating LED effects from Python

The script leds.py, below, is a simple ROS 2 node that demonstrates several LEDs effects on a ARI robot, to showcase how LEDs can be used to enhance the robot expressiveness.

Note

Similar effects can be achieved on the other PAL robots; only the devices setting needs to be changed. Refer to LEDs API for the list of available LEDs devices on your robot.

Important

The script is meant to be run either from a Docker image configured on the same ROS 2 network as your robot, or directly on the robot (using a SSH connection).

leds.py
  1import rclpy
  2from rclpy.node import Node
  3from rclpy.action import ActionClient
  4from pal_device_msgs.action import DoTimedLedEffect
  5from std_msgs.msg import ColorRGBA
  6from builtin_interfaces.msg import Duration
  7import numpy as np
  8import math
  9import time
 10
 11# LED devices IDs
 12BACK = 0
 13LEFT_EAR = 1
 14RIGHT_EAR = 2
 15RESPEAKER = 4
 16
 17MAX_PRIORITY = 255  # The maximum value for leds effect priority
 18
 19
 20def to_duration_msg(d):
 21    """ Converts floating point duration into ROS duration message. """
 22    nano, sec = math.modf(d)
 23    return Duration(sec=int(sec), nanosec=int(nano * 1e9))
 24
 25
 26class LEDClient(Node):
 27    def __init__(self):
 28        super().__init__('led_client')
 29
 30        self._leds = ActionClient(
 31            self, DoTimedLedEffect, '/led_manager_node/do_effect')
 32        while not self._leds.wait_for_server(timeout_sec=1.0):
 33            self.get_logger().info("waiting for the LED manager...")
 34
 35    def fixed_color(self, devices, color, duration=5.):
 36        """
 37        Sets the LEDs to a fixed color.
 38        """
 39        goal = DoTimedLedEffect.Goal()
 40        goal.devices = devices
 41        goal.params.effect_type = goal.params.FIXED_COLOR
 42
 43        goal.params.fixed_color.color = color
 44
 45        goal.effect_duration = to_duration_msg(duration)
 46        goal.priority = MAX_PRIORITY
 47
 48        self.get_logger().info(
 49            f"Setting a fixed color: {color} for {duration}s")
 50        self._leds.send_goal_async(goal)
 51        time.sleep(duration)
 52
 53    def progress_bar(self, devices):
 54        """
 55        Uses the LEDs to display a progress bar.
 56        """
 57        goal = DoTimedLedEffect.Goal()
 58        goal.devices = devices  # The devices performing the effect
 59        goal.params.effect_type = goal.params.PROGRESS  # The type of effect
 60
 61        # Setting values for the two progress
 62        # color components
 63        # first color --> complete
 64        # second color --> uncomplete
 65        blue = ColorRGBA(r=0., g=0., b=1., a=1.)
 66        goal.params.progress.first_color = blue
 67
 68        red = ColorRGBA(r=1., g=0., b=0., a=1.)
 69        goal.params.progress.second_color = red
 70
 71        # led offset set to 0. Progression
 72        # will be displayed starting from
 73        # the back led, counterclockwise
 74        goal.params.progress.led_offset = 0.
 75
 76        goal.effect_duration = to_duration_msg(0.2)
 77        goal.priority = MAX_PRIORITY
 78
 79        self.get_logger().info(f"iDisplaying a progress bar")
 80
 81        # Increment by 1% every 0.2 seconds.
 82        for percentage in np.arange(0, 1.01, 0.01):
 83
 84            goal.params.progress.percentage = percentage
 85            self._leds.send_goal_async(goal)
 86
 87            self.get_logger().info(f"Task completion: {percentage * 100}%")
 88
 89            # Sleeping slightly less then the effect duration
 90            # to always have the next completion goal
 91            # queuing when the previous one execution finishes
 92            time.sleep(0.19)
 93
 94    def rainbow(self, devices, transition_duration=1, duration=6.):
 95        """
 96        Sets up and sends a rainbow effect
 97
 98        (that is, continous transition over the rainbow colours).
 99        """
100
101        goal = DoTimedLedEffect.Goal()
102        goal.devices = devices
103        goal.params.effect_type = goal.params.RAINBOW
104
105        # transition_duration --> the time the effect will take to complete an
106        # iteration over the rainbow colours
107        goal.params.rainbow.transition_duration = to_duration_msg(transition_duration)
108
109        goal.effect_duration = to_duration_msg(duration)
110        goal.priority = MAX_PRIORITY
111
112        self.get_logger().info(f"Starting a rainbow")
113        self._leds.send_goal_async(goal)
114        time.sleep(duration)
115
116    def blinking(self, devices, color1, duration1, color2, duration2, total_duration=5.):
117        """
118        Sets up and sends blinking color leds effect.
119
120        It alternates two colors with custom temporisation.
121        """
122
123        goal = DoTimedLedEffect.Goal()
124        goal.devices = devices
125        goal.params.effect_type = goal.params.BLINK
126
127        goal.params.blink.first_color = color1
128        goal.params.blink.first_color_duration = to_duration_msg(duration1)
129
130        goal.params.blink.second_color = color2
131        goal.params.blink.second_color_duration = to_duration_msg(duration2)
132
133        goal.effect_duration = to_duration_msg(total_duration)
134        goal.priority = MAX_PRIORITY
135
136        self.get_logger().info(f"Start blinking")
137        self._leds.send_goal_async(goal)
138        time.sleep(total_duration)
139
140
141def main():
142    rclpy.init()
143    led_node = LEDClient()
144
145    green = ColorRGBA(r=0., g=1., b=0., a=1.)
146    yellow = ColorRGBA(r=1., g=1., b=0., a=1.)
147    turquoise = ColorRGBA(r=0., g=1., b=1., a=1.)
148
149    led_node.blinking([LEFT_EAR, RIGHT_EAR, BACK],
150                      yellow, 1,
151                      turquoise, 2)
152
153    led_node.fixed_color([LEFT_EAR, RIGHT_EAR, BACK], green)
154
155    led_node.progress_bar([LEFT_EAR, RIGHT_EAR])
156
157    led_node.rainbow([LEFT_EAR, RIGHT_EAR, BACK])
158
159
160if __name__ == '__main__':
161    main()

The code explained

First things first, we need to import the required packages.

leds.py
1import rclpy
2from rclpy.node import Node
3from rclpy.action import ActionClient
4from pal_device_msgs.action import DoTimedLedEffect
5from std_msgs.msg import ColorRGBA
6from builtin_interfaces.msg import Duration
7import numpy as np
8import math
9import time

Then, we define some constant values used later in the code. This step increases code readability.

leds.py
11# LED devices IDs
12BACK = 0
13LEFT_EAR = 1
14RIGHT_EAR = 2
15RESPEAKER = 4
16
17MAX_PRIORITY = 255  # The maximum value for leds effect priority
18
19
20def to_duration_msg(d):
21    """ Converts floating point duration into ROS duration message. """
22    nano, sec = math.modf(d)
23    return Duration(sec=int(sec), nanosec=int(nano * 1e9))

to_duration_msg(d) is a utility function that converts a floating point duration into a ROS duration message. This is useful as several of the LEDs effects require a Duration message to specify the effect duration.

Next, we create the LEDClient class, which inherits from a ROS Node. This class will be used to manage the communication with the LED Manager action server, which is responsible for executing the LEDs effects.

leds.py
26class LEDClient(Node):
27    def __init__(self):
28        super().__init__('led_client')
29
30        self._leds = ActionClient(
31            self, DoTimedLedEffect, '/led_manager_node/do_effect')
32        while not self._leds.wait_for_server(timeout_sec=1.0):
33            self.get_logger().info("waiting for the LED manager...")

Fixed-color effect

The most basic effect we can request to the LEDs action server is the fixed-color effect. This effect sets the LEDs to a fixed color for a certain amount of time. The method fixed_color does just that.

leds.py
35def fixed_color(self, devices, color, duration=5.):
36    """
37    Sets the LEDs to a fixed color.
38    """
39    goal = DoTimedLedEffect.Goal()
40    goal.devices = devices
41    goal.params.effect_type = goal.params.FIXED_COLOR
42
43    goal.params.fixed_color.color = color
44
45    goal.effect_duration = to_duration_msg(duration)
46    goal.priority = MAX_PRIORITY
47
48    self.get_logger().info(
49        f"Setting a fixed color: {color} for {duration}s")
50    self._leds.send_goal_async(goal)
51    time.sleep(duration)

We first create a DoTimedLedEffect.Goal object, which is the object we will send to the action server to request the fixed-color effect.

We set the target LED devices that will execute the effect. For instance, ears’ LEDs, or back LEDs. Check the LEDs API for the list of available LEDs devices on your robot.

The color parameter is a std_msgs.msg.ColorRGBA message, which contains the RGBA components of the color as floating point values between 0 and 1.

Then, we set the effect duration and priority, before sending the goal object to the action server. The call is asynchronous; in this example, we block the process for the effect duration, to allow the effect to be executed before the next effect is requested.

Progression effect

The progression or progress bar effect is a more complex effect that allows us to show a progression state through the LEDs. This is useful, for instance, to show the battery level while the robot is docked, or the progress of a task being executed by the robot. The method progress_bar implements this.

leds.py
53def progress_bar(self, devices):
54    """
55    Uses the LEDs to display a progress bar.
56    """
57    goal = DoTimedLedEffect.Goal()
58    goal.devices = devices  # The devices performing the effect
59    goal.params.effect_type = goal.params.PROGRESS  # The type of effect
60
61    # Setting values for the two progress
62    # color components
63    # first color --> complete
64    # second color --> uncomplete
65    blue = ColorRGBA(r=0., g=0., b=1., a=1.)
66    goal.params.progress.first_color = blue
67
68    red = ColorRGBA(r=1., g=0., b=0., a=1.)
69    goal.params.progress.second_color = red
70
71    # led offset set to 0. Progression
72    # will be displayed starting from
73    # the back led, counterclockwise
74    goal.params.progress.led_offset = 0.
75
76    goal.effect_duration = to_duration_msg(0.2)
77    goal.priority = MAX_PRIORITY
78
79    self.get_logger().info(f"iDisplaying a progress bar")
80
81    # Increment by 1% every 0.2 seconds.
82    for percentage in np.arange(0, 1.01, 0.01):
83
84        goal.params.progress.percentage = percentage
85        self._leds.send_goal_async(goal)
86
87        self.get_logger().info(f"Task completion: {percentage * 100}%")
88
89        # Sleeping slightly less then the effect duration
90        # to always have the next completion goal
91        # queuing when the previous one execution finishes
92        time.sleep(0.19)

For this effect, we set two colors (here hardcoded to blue and red) that represent the completed and uncompleted parts of the progression bar.

Using the parameters percentage and led_offset, we can control the progression bar behaviour:

  • percentage represents the fraction of lights to set with the completion colour. In a process advancement fashion, it represents the process completion status.

  • led_offset represents from which light in the LEDs array the progression should start from. For instance, if set to 0, the progression will start from the back-most LED and will progress counterclockwise.

In this example, we set the progression bar to advance by 1% every 0.2 seconds, by iterating over an np.arange object. At each step, we set the goal.params.progress.percentage to the current percentage and send the goal object to the action server.

Rainbow effect

The next effect performed by the LEDs is the rainbow effect. This effect continuously changes the LEDs colour, ranging over the whole rainbow colours.

leds.py
 94def rainbow(self, devices, transition_duration=1, duration=6.):
 95    """
 96    Sets up and sends a rainbow effect
 97
 98    (that is, continous transition over the rainbow colours).
 99    """
100
101    goal = DoTimedLedEffect.Goal()
102    goal.devices = devices
103    goal.params.effect_type = goal.params.RAINBOW
104
105    # transition_duration --> the time the effect will take to complete an
106    # iteration over the rainbow colours
107    goal.params.rainbow.transition_duration = to_duration_msg(transition_duration)
108
109    goal.effect_duration = to_duration_msg(duration)
110    goal.priority = MAX_PRIORITY
111
112    self.get_logger().info(f"Starting a rainbow")
113    self._leds.send_goal_async(goal)
114    time.sleep(duration)

This effect is implemented in a similar way to the fixed-color effect, setting the goal.params.effect_type to goal.params.RAINBOW. The goal.params.rainbow.transition_duration parameter defines the speed of the color wheel.

Blinking effect

Finally, the last effect we will implement is the blinking effect. This effect alternates two colors with custom durations, creating a blinking effect.

leds.py
116def blinking(self, devices, color1, duration1, color2, duration2, total_duration=5.):
117    """
118    Sets up and sends blinking color leds effect.
119
120    It alternates two colors with custom temporisation.
121    """
122
123    goal = DoTimedLedEffect.Goal()
124    goal.devices = devices
125    goal.params.effect_type = goal.params.BLINK
126
127    goal.params.blink.first_color = color1
128    goal.params.blink.first_color_duration = to_duration_msg(duration1)
129
130    goal.params.blink.second_color = color2
131    goal.params.blink.second_color_duration = to_duration_msg(duration2)
132
133    goal.effect_duration = to_duration_msg(total_duration)
134    goal.priority = MAX_PRIORITY
135
136    self.get_logger().info(f"Start blinking")
137    self._leds.send_goal_async(goal)
138    time.sleep(total_duration)

For this effect, we set the two colors to blink, their respective durations, and the total duration of the blinking effect.

Script execution

Finally, we define the main function that will create an instance of the LEDClient class and call the different methods to perform the LEDs effects.

leds.py
141def main():
142    rclpy.init()
143    led_node = LEDClient()
144
145    green = ColorRGBA(r=0., g=1., b=0., a=1.)
146    yellow = ColorRGBA(r=1., g=1., b=0., a=1.)
147    turquoise = ColorRGBA(r=0., g=1., b=1., a=1.)
148
149    led_node.blinking([LEFT_EAR, RIGHT_EAR, BACK],
150                      yellow, 1,
151                      turquoise, 2)
152
153    led_node.fixed_color([LEFT_EAR, RIGHT_EAR, BACK], green)
154
155    led_node.progress_bar([LEFT_EAR, RIGHT_EAR])
156
157    led_node.rainbow([LEFT_EAR, RIGHT_EAR, BACK])

You can use the code snippets for the different effects in your own scripts, adapting them to your needs. For instance, you can change the LEDs devices to use, the colors, the durations, and so on.

Important

While you can control the LEDs effect via the ROS action interface, as we have done in this tutorial, you can also use special markup expressions to easily perform LED effects while the robot is speaking. Refer to Multi-modal expression markup language to learn more.

Next steps

  • The node you have developed only includes some of the effects that can be performed with LEDs. We invite you to explore more of these effects, playing with them for a deeper understanding of how LEDs can enhance the robot’s expressiveness: LEDs API.

  • If you want to know more about building expressive interaction, check 😄 Expressive interactions.