2
\$\begingroup\$

Although this code uses ros2 my concern is about C++ code quality, because I still struggle when it comes to making good design decisions with regards to single responsibility, dependency injection, and callbacks, etc. I tend to second-guess decisions due to lack of experience.

I am also always worried about which objects to use in callbacks in terms of race conditions (which in this case is not really applicable but it's something I worry about getting wrong).

The below code example is very simple. There's a wheel encoder connected to the GPIO pin on an NVIDIA Jetson Nano board, and a ROS2 Node that publishes the encoder.

I already tested this code on the Jetson Nano and it is fully functional.

The wheel_encoder_sensor.h file

#pragma once

#include <iostream>
#include <memory>
#include <functional>
#include <JetsonGPIO.h>

namespace xiaoduckie {
namespace sensors {

/*!
 * @brief This class manages the WheelEncoder sensor connected to the GPIO
 * pins in the NVIDIA Jetson Nano Board
 * @ingroup Sensors
*/
class WheelEncoderSensor {

  /*!
    * @brief ENUM that represents the direction of motion for the robot.
    * This value is updated when the controller makes a decision about
    * direction.
    * The value is used to update the global tick count on the encoder.
    * @ingroup Sensors
    */
  enum Direction {
    FORWARD = 1,
    BACKWARD = -1
  };

  /*!
    * @brief This class is used by the Jetson GPIO c++ library as a callback
    * for the event detect on a GPIO pin.
    * We cannot use a regular lambda bcause this Jetson GPIO library requires
    * the callbacks must be equaility-comparable.
    * More details: https://github.com/pjueon/JetsonGPIO/issues/55#issuecomment-1059888835
    * @ingroup Sensors
    */
  class EncoderCallback
  {
  public:
    /*!
      * @brief Default constructor.
      * @param name Reference to the name assigned to this callback.
      * @param ticks Pointer referring to total ticks in the WheelEncoderSensor.
      * @param callback The external callback passed to WheelEncoderSensor.
      * @param direction Pointer to the DIRECTION enum in WheelEncoderSensor.
      */
    EncoderCallback(
      const std::string& name,
      std::shared_ptr<int> ticks,
      std::function<void(int)> callback,
      std::shared_ptr<WheelEncoderSensor::Direction> direction)
      : name(name),
      callback_(std::move(callback)),
      ticks_(ticks),
      direction_(direction)
    {};

    /*!
      * @brief Default copy constructor
      */
    EncoderCallback(const EncoderCallback&) = default; // Copy-constructible

    /*!
      * @brief Callable executed by the GPIO library upon event detection.
      * Used to update the global tick count and execute the callback provided
      * to the Encoder through its constructor.
      * @param channel Reference to the GPIO pin passed in by the library.
      */
    void operator()(const std::string& channel) // Callable with one string type argument
    {
      std::cout << "A callback named " << name;
      std::cout << " called from channel " << channel << std::endl;
      *ticks_ += *direction_;
      callback_(*ticks_);
    }

    /*!
      * @brief equality operator used by GPIO library to remove callbacks.
      * @param other Reference to the object being compared.
      */
    bool operator==(const EncoderCallback& other) const // Equality-comparable
    {
        return name == other.name;
    }

    /*!
      * @brief in-equality operator used by GPIO library to remove callbacks.
      * @param other Reference to the object being compared.
      */
    bool operator!=(const EncoderCallback& other) const 
    {
        return !(*this == other);
    }
      
  private:
    std::string name;
    std::function<void(int)> callback_;
    std::shared_ptr<int> ticks_ = std::make_shared<int>(0);
    std::shared_ptr<WheelEncoderSensor::Direction> direction_ = std::make_shared<WheelEncoderSensor::Direction>();
  };

  public:
    /*!
      * @brief Default constructor.
      */
    WheelEncoderSensor() = default;

    /*!
      * @brief The Init function sets the GPIO used by this encoder,
      * accepts an additional callback to use when the encoder is
      * updated.
      * It initializes the internal callback mechanism and passes it
      * the additional/external one.
      * @param gpio The GPIO pin to which the encoder is connected.
      * @param callback The external callback.
      */
    bool Init(int gpio, std::function<void(int)> callback) {
      gpio_pin_ = gpio;
      *direction_ = Direction::FORWARD;

      EncoderCallback callback_(
        "EncoderCallback",
        ticks_,
        std::move(callback),
        direction_
      );

      GPIO::setmode(GPIO::BCM);
      GPIO::setup(gpio_pin_, GPIO::IN);
      GPIO::add_event_detect(gpio_pin_, GPIO::RISING, callback_);

      initialized_ = true;
      return true;
    };
  private:
    int gpio_pin_;
    bool initialized_ = false;
    std::shared_ptr<int> ticks_ = std::make_shared<int>(0);
    std::shared_ptr<WheelEncoderSensor::Direction> direction_ = std::make_shared<WheelEncoderSensor::Direction>();
};


} // namespace sensors
} // namespace xiaoduckie

The wheel_encoder_publisher.h file

#pragma once

#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include <iostream>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int32.hpp"

#include "wheel_encoder/wheel_encoder_sensor.h"

namespace xiaoduckie {
namespace sensors {

/*!
 * @brief This is the ROS2 Node that publishes the encoder messages.
 * It is constructed with an std::vector of WheelEncodersNode::WheelEncoderConfig
 * objects and creates an instance of a WheelEncoderSensor for each, then assigns
 * to each its own callback using a lambda function.  The lambda function publishes
 * the encoder value.
 * @ingroup Sensors
 */
class WheelEncodersNode : public rclcpp::Node {

  typedef rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr PublisherPtr;
  
  public:
    /*!
      * @brief This Struct holds the config for each WheelEncoderSensor
      * @ingroup Sensors
      */
    struct WheelEncoderConfig {
        int gpio_pin_;
        int resolution_;
        int publish_frequency_;
        int queue_limit;
        std::string orientation_;
        std::string topic_name_;
    };

    /*!
      * @brief Default constructor.
      * @param encoder_configs const Reference to the std::vector of WheelEncoderConfig.
      */
    WheelEncodersNode(const std::vector<WheelEncoderConfig>& encoder_configs)
    : Node("WheelEncodersNode"), encoder_configs_(encoder_configs)
    {
      int index = 0;
      for (auto const &config : encoder_configs_) {
        encoder_total_ticks_.push_back(0);

        PublisherPtr p = this->create_publisher<std_msgs::msg::Int32>(
          config.topic_name_,
          config.publish_frequency_);

        std::shared_ptr<WheelEncoderSensor> encoder = std::make_shared<WheelEncoderSensor>();
        
        encoder.get()->Init(
          config.gpio_pin_,
          [p, index, this] (int encoder_ticks) {

            auto message = std_msgs::msg::Int32();
            message.data = encoder_ticks;

            this->encoder_total_ticks_[index] = encoder_ticks;
            p->publish(message);
          }
        );

        encoders_.push_back(encoder);
        ++index;
      }
    };

  private:
    std::vector<int> encoder_total_ticks_;
    std::vector<WheelEncoderConfig> encoder_configs_;
    std::vector<std::shared_ptr<WheelEncoderSensor>> encoders_;
    std::vector<PublisherPtr> publishers_;
};

} // namespace sensors
} // namespace xiaoduckie
\$\endgroup\$
0

1 Answer 1

4
\$\begingroup\$

It isn't clear why there are 2 namespaces, one nested inside the other. The namespace sensors is understandable, what is xiaoduckie?

Indentation: A namespace inside a namespace needs to be indented.

Defining a class within a class such as EncoderCallback is problematic. Among other things it means that the class can't be easily reused; any inheritance becomes overly complicated. Why is it being defined within a class rather than as its own class file?

The class EncoderCallback violates the rule of 3 which states

If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three.

Why define a copy constructor using default without defining a destructor or an assignment operator?

Rather than have a default constructor and an Init() function for the class WheelEncoderSensor there can be 2 constructors, one with the function body of Init().

Why is direction_ a pointer?

gpio_pin_ should always be initialized, if the parameter isn't passed in, initialize it to a suitable value such as zero.

Where are the C++ (.cpp) files that should be with these header files? Why is all the code in the header files rather than putting most of the executable code in .cpp files? Putting all of the code into the header files means that all the files need to be recompiled every time there is a code change.

\$\endgroup\$
5
  • \$\begingroup\$ Thank you! This is very helpful! The namespace xiaoduckie is the name of the project/robot. I defined EncoderCallback internally because of the limitation of the GPIO library not accepting lambdas. I thought that it will never be used outside of the Encoder Sensor class. If lambdas were accepted I would not need it. The reason there's an Init is because I am sending a lambda as the callback, but I also thought it might be useful to store the WheelEncoderSensor objects in the Node class. So I would not be able to send a lambda this way, but have to use an std::bind. \$\endgroup\$ Commented Dec 23, 2022 at 16:10
  • \$\begingroup\$ The code is in the header files just while I figure out the correct way to do things, it's easier to navigate and share on posts like this. I will change that right away. \$\endgroup\$ Commented Dec 23, 2022 at 16:11
  • \$\begingroup\$ once I make changes to the code, should I open a new review or update this one? \$\endgroup\$ Commented Dec 23, 2022 at 16:14
  • 2
    \$\begingroup\$ @SamHammamy You can ask a follow up question, do not update this question. I suggest you re-read the help section for Code Review before asking a follow up question. This question was barely on-topic I mostly answered it because you had indicated you were getting bounced back and forth between SO and CR. \$\endgroup\$
    – pacmaninbw
    Commented Dec 23, 2022 at 16:23
  • \$\begingroup\$ @SamHammamy How to get the best value out of Code Review - Asking Questions, the FAQ. \$\endgroup\$
    – Mast
    Commented Dec 23, 2022 at 20:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.