Skip to content

callbacks.py ‐ Using the CheckBoxCallback class

Ryan Roche edited this page Jul 13, 2023 · 9 revisions

Info

There is a lot of boilerplate involved for making a checkbox menu option for an interactive marker in RViz. In order to simplify the creation of such menu options, I created the template class CheckBoxCallback in callbacks.py, which simplifies the process by automatically handling the work of toggling the checkbox state and sending the updated menu item state to the corresponding menu handler and interactive marker server. It also surfaces four abstract functions for user-defined code that is to be executed whenever the button is clicked. The functions are as follows:

State-dependent Functions

Whether these functions are run when the button is clicked depends on whether the checkbox was enabled or disabled.

  • _on_enable: This function is run when the checkbox is toggled from off to on
  • _on_disable: This function is run when the checkbox is toggled from on to off

State-independent Functions

These functions are run every time the button is clicked, regardless of whether the checkbox was enabled or disabled. Use these functions if you have code that needs to be executed every time a button is pressed (for example, to call a toggle function).

  • _before_either: This function is run before _on_enable/_on_disable is run
  • _after_either: This function is run after _on_enable/_on_disable is run

⚠️IMPORTANT!⚠️

EVEN IF YOU DO NOT NEED ALL FOUR OF THESE FUNCTIONS TO DO SOMETHING, YOU STILL MUST IMPLEMENT THEM IN SOME CAPACITY, EVEN IF ALL THEY DO IS pass. THE CLASS WAS DESIGNED THIS WAY TO ENSURE THAT USERS ARE PRECISELY AWARE OF THE ORDER THEIR FUNCTIONS ARE CALLED.

The call order for these functions when the button is clicked is as follows:

flowchart TD
    classDef class1 fill:blue,  stroke:white, color:white
    classDef class2 fill:green, stroke:white, color:white
    classDef class3 fill:red,   stroke:white, color:white

    id1(["Button is Clicked"])
    id2["_before_either() is called"]
    class id1 class1
    class id2 class1

    id3{"Checkbox State is Checked"}
    class id3 class1

    id4a["_on_enable() is called"]
    id5a["Checkbox is automatically enabled"]
    class id4a,id5a class3

    id4b["_on_disable() is called"]
    id5b["Checkbox is automatically disabled"]
    class id4b,id5b class2

    id6(["_after_either() is called"])
    class id6 class1

    id1 ---> id2
    id2 ---> id3
    
    id3 -- Checkbox is disabled --> id4a
        id4a ---> id5a
        id5a ---> id6

    id3 -- Checkbox is enabled --> id4b
        id4b ---> id5b
        id5b ---> id6
Loading

The following section explains how to use this class to add a checkbox button to the context menu of an interactive marker.


Tutorial

This tutorial will make an example Checkbox button that does the following:

  • Print a user-given string whenever the button is clicked
  • Track the amount of times that the box has been checked, and print that count whenever the box is checked
  • Track the amount of times that the box has been unchecked, and print that count whenever the box is unchecked
  • Track the amount of times that the button has been clicked in total, and print that whenever the button is clicked

However, this tutorial can still be used as a guide for making a checkbox button with your own functionality. Simply replace the relevant functions with your own code.

1. Create a subclass of CheckBoxCallback

In callbacks.py, create the callback object for your button as a subclass of the CheckBoxCallback class as such:

class ExampleCheckboxCallback(CheckBoxCallback):

2. __init__ Requirements

Its __init__ method will need to at least have the parameters menu_handler and marker_server, as they are needed to update the checkbox's "checked" state. Any additional parameters your callback needs for its functionality will go here. For example, this callback will take in a string to be logged to the ROS console whenever the button is pressed, so the init function will take in an extra parameter for that string. We will also need to initialize the tallies for the button-click totals.

Regardless of what extra parameters you need, the init function will always need to call the superclass's init with the menu_handler and marker_server parameters.

With that all in mind, __init__ for our example class is as follows:

class ExampleCheckboxCallback(CheckBoxCallback):
    __init__(self, menu_handler, marker_server, user_string):
        super().__init__(menu_handler, marker_server)

        self.s = user_string
        self.clicks   = 0
        self.checks   = 0
        self.unchecks = 0

3. Implement the Required Functions

As stated above, regardless of whether you need them to actually do anything, all four of the functions _before_either, _after_either, _on_enable, and _on_disable need to be defined in some way.

For our example callback, we have two state-independent operations for when the button is clicked: printing that user-defined string passed to our __init__, and incrementing & printing the total of how many times the button has been clicked. It doesn't matter whether this is done before or after the state-dependent code is run, so we'll have it run after the state-dependent code.

Since we're putting all of our state-independent code into _after_either, our _before_either can simply be

    def _before_either(self):
        pass

However, for the sake of the demonstration, we'll have the _before_either method log a message to demonstrate how it's the first thing run when the button is clicked.

    def _before_either(self):
        rospy.loginfo("_before_either has been called")

Our _after_either will then be as follows:

    def _after_either(self):
        rospy.loginfo(f"The string is {self.s}")
        self.clicks += 1
        rospy.loginfo(f"Total button clicks: {self.clicks}")

We also want the button to track and print how many times it has been checked & unchecked. Those functions, respectively, are:

    def _on_enable(self):
        self.checks += 1
        rospy.loginfo(f"The box has been checked {self.checks} time(s)")

    def _on_disable(self):
        self.unchecks += 1
        rospy.loginfo(f"The box has been checked {self.unchecks} time(s)")

All together, our class is now

class ExampleCheckboxCallback(CheckBoxCallback):
    def __init__(self, menu_handler, marker_server, user_string):
        super().__init__(menu_handler, marker_server)

        self.s = user_string
        self.clicks   = 0
        self.checks   = 0
        self.unchecks = 0

    def _before_either(self):
        rospy.loginfo("_before_either has been called")

    def _after_either(self):
        rospy.loginfo(f"The string is {self.s}")
        self.clicks += 1
        rospy.loginfo(f"Total button clicks: {self.clicks}")

    def _on_enable(self):
        self.checks += 1
        rospy.loginfo(f"The box has been checked {self.checks} time(s)")

    def _on_disable(self):
        self.unchecks += 1
        rospy.loginfo(f"The box has been checked {self.unchecks} time(s)")

4. Create the Corresponding Menu Item

Now that the callback for the button has been created, all that's left is creating the actual button in RViz. In this example, I'll be adding the button to the chair_marker and therefor using its corresponding MenuHandler and `InteractiveMarkerServer.

menu_handler.insert("Example Checkbox", callback=ExampleCheckboxCallback(menu_handler=menu_handler, 
                                                                         marker_server=server, 
                                                                         user_string="foo"))

After the call to the menu handler's insert method, don't forget to set an initial state for the checkbox. The insert method returns a handler to the menu item, which you pass to the setCheckState method.

handle = menu_handler.insert("Example Checkbox", callback=ExampleCheckboxCallback(menu_handler=menu_handler, 
                                                                                  marker_server=server, 
                                                                                  user_string="foo"))
menu_handler.setCheckState(handle, menu_handler.UNCHECKED)

All together, in-action:

example