Skip to content

Commit

Permalink
Merge commit 'c0f5f621f519dd160d83a35840427f36f93254f7'
Browse files Browse the repository at this point in the history
  • Loading branch information
sjev committed May 2, 2024
2 parents 94ad6a7 + c0f5f62 commit d25ed61
Show file tree
Hide file tree
Showing 21 changed files with 635 additions and 14 deletions.
11 changes: 0 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,6 @@ It follows these key principles:
* **Less Code Equals Fewer Problems:** - keep codebase small and portable
* **Don't reinvent the wheel** - leverage robust existing technologies. Think of `asyncio`, `Docker` etc.

## Repository overview

├── docker - source for docker images
│ ├── ci - image for running ci locally
│ └── dev - development image for devcontainer
├── examples
│ └── line_follower - basic linie following example
├── src - library code
│ └── roxbot
└── tests - library tests


## Concepts

Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions docs/blog/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Blog
Binary file added docs/blog/posts/img/benchmark_emoticons.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/blog/posts/img/benchmark_rpi4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/blog/posts/img/blinker.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/blog/posts/img/motor_pos.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
26 changes: 26 additions & 0 deletions docs/blog/posts/live_plots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
draft: false
date: 2023-11-20
categories:
- development
- gui
- can
---

# Plotting live data with Plotjuggler

Containerization is crucial for a reliable CI/CD workflow, but it can be challenging when you need live data visualization, especially for things like tuning motion control.

The good news? There's a simple solution. Just send your data to Plotjuggler using a UDP socket. I've included a code example below to show you how it's done. It's straightforward yet still packs in all the features you need.

Need to analyze data quickly? Just use the UDP_Client from the example. With just a few lines of code, you're all set.

Happy coding!

![Plotjuggler](img/motor_pos.png)

<!-- more -->

```python
--8<-- "code/udp_plot.py"
```
214 changes: 214 additions & 0 deletions docs/blog/posts/messaging_benchmarks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
---
draft: false
date: 2023-12-13
categories:
- ROS2
- asyncio
---

# Why asyncio is Ideal for Robotics Development

As a professional in robotics software development, I've extensively used ROS. In fact, ROS2 is the current standard in this field. However, I've recently transitioned to using asyncio, a leaner solution that allows for the creation of more efficient and manageable codebases in a shorter time. In this post, I'll share my experiences with ROS and explain why I believe asyncio is a superior choice.


![](img/benchmark_emoticons.png)

<!-- more -->

## ROS In A Nutshell

Let's start with a brief introduction to ROS for those unfamiliar with it. Essentially, ROS (Robot Operating System) is a framework designed to help in creating and managing asynchronous processes, known as "nodes". These nodes are individual processes tasked with specific functions, like reading a sensor or controlling an actuator. They communicate through "topics" using a publish/subscribe pattern, and request/reply interactions are facilitated through services. In this article, when I refer to "ROS", I'm specifically talking about ROS2, as ROS1 is now outdated and not recommended for industrial use.

## Why ROS Might Not Be the Ideal Choice

While ROS is quite helpful in managing complex, asynchronous systems, I often found it to be somewhat cumbersome, adding unnecessary complexity and slowing down development, especially when compared to more streamlined frameworks available in the Python ecosystem.

Here are some key limitations I encountered with ROS, particularly from a Python developer's perspective:

1. **Primary Language**: ROS primarily uses C++, making Python integration feel like an afterthought. The Python library `rclpy` often lacks the full functionality of its C++ counterpart.
2. **Packaging and Execution Process**: The way ROS packages and executes code is quite different from standard Python practices. For example, why do we need to source a setup file (`source /setup.bash`) and then use a ROS-specific command (`ros2 run ...`) when we could just run an executable directly?
3. **Launch System**: ROS employs its own unique and complex and inefficient launch system (each `launch.py` file consuming around 20MB of memory). In contrast, tools like `systemd` and Docker provide more mature and well-designed launching solutions for modern applications.
4. **Interface Definitions**: The process of defining and compiling interfaces in ROS can add unnecessary overhead to development.
5. **Cross-Domain Communication**: Transferring data across different subdomains in ROS can be challenging. An alternative like MQTT, with its centralized protocol, simplifies this process by specifying clear host and port connections, making troubleshooting more straightforward.
6. **Performance Issues**: Inter-node communication, especially between nodes written in Python, can be inefficient, leading to sluggish performance. I'll delve more into this in the "Benchmarks" section.
7. **Native graphics requirement**: ROS relies on tools like RQT, which depend on native graphics. This setup becomes problematic when working remotely, as these tools are not easily accessible or functional across network domains. A more practical alternative would be web-based tools, which can be operated from any browser, offering greater flexibility and ease of use in remote working scenarios.



## Introducing Roxbot: A Pythonic ROS Alternative

Confronted daily with these challenges in ROS, I was motivated to find solutions to streamline my work. Over the past year, I've been developing a Python-centric robotics framework named "Roxbot". This framework is akin to ROS but is designed to overcome the limitations I've encountered. Currently, Roxbot is in its pre-release phase and can be explored on [GitLab](https://gitlab.com/roxautomation/roxbot) and in [online documentation](https://roxautomation.gitlab.io/roxbot/how_it_works/) Note that while Roxbot is not ready for release as a full-fledged ROS replacement, it contains many bits and pieces that
I've been using in various customer projects over the course of last year.


While I plan to delve deeper into Roxbot in a future post, this article will focus on its most critical component – the communication layer. Selecting the right communication protocol is perhaps the most significant and challenging architectural decision in the framework's development. To ensure I made an informed choice, I created an extensive benchmark suite. This suite evaluates various communication protocols, examining their performance both within and across Docker containers. I will share the comprehensive findings and insights from this benchmarking in the next section of the blog.


## Benchmarks: The Results Are In!

The complete benchmark suite is accessible on [GitLab](https://gitlab.com/roxautomation/playground/benchmarks) in the `messaging` folder.

The benchmark involves various scenarios with nodes named `Alice` and `Bob` communicating within the same Docker container or across two different ones. Here's a brief overview:

- **ROS Benchmark Python**: Two `rclpy` nodes in the same container.
- **C++ in Single Container**: Two C++ nodes in the same container.
- **C++ in Two Containers**: Separate containers, communicating over the host network.
- **C++ in Two Isolated Containers**: Separate containers, communicating over an isolated Docker network.
- **MQTT Benchmark**: Separate containers using `paho.mqtt` with a Mosquitto broker, over an isolated Docker network.
- **Async Benchmark**: Same container, nodes communicate via `asyncio.Queue`.
- **Websocket Benchmark**: Separate containers, nodes communicate using `websockets`.

### Benchmark Methodology:

1. Alice sends the number 0 to Bob.
2. Bob increments the number to 1 and sends it back to Alice.
3. Alice increments it to 2 and sends it back to Bob.
4. This ping-pong continues until a specific count is reached (typically 10,000).
5. The "message rate" is calculated based on the number of messages exchanged per second.

These benchmarks are designed to run in Docker containers, allowing for easy replication on your system. For detailed instructions, refer to the `README.md` file.

## And The Winner Is...

After running the benchmark suite on various systems, the insights gained were quite eye-opening. Here's what emerged:

1. Python nodes in ROS exhibit significantly slow communication speeds.
2. C++ nodes offer decent performance, but C++ may not be the preferred language for ease of coding.
3. Python `async` stands out remarkably, surpassing even C++ systems in single-container setups by a substantial margin.

An interesting observation was the performance drop in ROS C++ nodes when communicating across containers in an isolated Docker network. Therefore, if you're considering segmenting your system into separate Docker containers, using `net=host` might be a more efficient approach.


Top of this post contains results from a fairly modern laptop with an i5 processor.
Tests on a RaspberryPi 4 with 2GB of memory produce these results:

![](img/benchmark_rpi4.png)

While the difference in perofmance between C++ and asyncio on `aarch64` is less dramatic than on a `x86_64`system, asyncio is an undisputed winner here.

## Show Me The Code!

!!! info
The complete benchmark suite is accessible on [GitLab](https://gitlab.com/roxautomation/playground/benchmarks) in the [`messaging`](https://gitlab.com/roxautomation/playground/benchmarks/-/tree/main/messaging?ref_type=heads) folder.

Another great advantage of `asyncio` is code simplicity. An implementation of `EchoNode` looks like this:

```python

STOP_AFTER = 100_000

class EchoNode:
def __init__(self, name: str, sub_q: asyncio.Queue, pub_q: asyncio.Queue):
self.name = name
self.sub_q = sub_q
self.pub_q = pub_q

# Alice starts the ping-pong
if name.lower() == "alice":
self.pub(0)

def pub(self, nr):
self.pub_q.put_nowait(nr)

async def sub(self):
"""handle incoming messages"""

while True:
nr = await self.sub_q.get()

if nr > STOP_AFTER:
print(f"{self.name} had enough. Stopping.")
raise TestComplete

self.sub_q.task_done()
self.pub(nr + 1)

```

Let's compare it with a C++ snippets (split over `.hpp` and `.cpp` files) to achieve the same functionality...


!!! note
I haven't used C++ much since my masters thesis in 2004. So "Pardon my C++" ;-).
This is what I managed to build with help of ChatGPT.

```C++

// ----------------- .hpp -----------------------
#ifndef ROS_BENCHMARK_CPP__ECHO_NODE_HPP_
#define ROS_BENCHMARK_CPP__ECHO_NODE_HPP_

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int64.hpp"
#include <chrono>

class EchoNode : public rclcpp::Node {
public:
static constexpr int STOP_AFTER = 10'000;
EchoNode(const std::string & name, const std::string & sub_topic, const std::string & pub_topic);
void pub(int nr);

// Method to get the start time of the node
const std::chrono::steady_clock::time_point& get_start_time() const;

private:
void sub_callback(const std_msgs::msg::Int64::SharedPtr msg);

rclcpp::Publisher<std_msgs::msg::Int64>::SharedPtr publisher_;
rclcpp::Subscription<std_msgs::msg::Int64>::SharedPtr subscriber_;

// Start time for the benchmark
std::chrono::steady_clock::time_point start_time_;


};

#endif // ROS_BENCHMARK_CPP__ECHO_NODE_HPP_

// ------------------ .cpp -----------------------------


#include "ros_benchmark_cpp/echo_node.hpp"
#include <iostream>

EchoNode::EchoNode(const std::string & name, const std::string & sub_topic, const std::string & pub_topic)
: Node(name),
publisher_(this->create_publisher<std_msgs::msg::Int64>(pub_topic, 10)),
subscriber_(this->create_subscription<std_msgs::msg::Int64>(
sub_topic, 10, [this](const std_msgs::msg::Int64::SharedPtr msg) { this->sub_callback(msg); })),
start_time_(std::chrono::steady_clock::now())
{
}

void EchoNode::sub_callback(const std_msgs::msg::Int64::SharedPtr msg) {
int nr = msg->data;

if (nr > STOP_AFTER) {
std::cout << this->get_name() << " had enough. Stopping." << std::endl;
rclcpp::shutdown();
} else {
this->pub(nr + 1);
}
}

void EchoNode::pub(int nr) {
auto message = std_msgs::msg::Int64();
message.data = nr;
publisher_->publish(message);
}

const std::chrono::steady_clock::time_point& EchoNode::get_start_time() const {
return start_time_;
}

```
Regarding maintainability and readability, I don't have to explain much here - the code
speaks for itself.
## Looking Forward
!!! question "Interested?"
Are you passionate about robotics and Python, dreaming of a "Pythonic ROS"? You're not alone! I'm on a quest to develop Roxbot, and I'd love to collaborate with like-minded developers. If this resonates with you, let's connect and contribute together. Your insights, experience, and enthusiasm could be the perfect addition to this journey. Don't hesitate to reach out – let's make Roxbot not just a tool, but a community-driven success.
27 changes: 27 additions & 0 deletions docs/blog/posts/power_button.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
draft: false
date: 2023-12-22
categories:
- rpi
- hardware
---

# Effortlessly Add a Heartbeat and Power Button to Your Raspberry Pi

![](img/blinker.gif)


Want to make your Raspberry Pi experience even better with a visible heartbeat and easy shutdown? It's simpler than you think and doesn't even require any software!.

Just connect an LED to GPIO 4 and a momentary push button between GPIO 3 and a ground pin.

Next, tweak your `config.txt` by adding these two lines:

```plaintext
dtoverlay=gpio-led,gpio=4,label=heartbeat-led,trigger=heartbeat
dtoverlay=gpio-shutdown,gpio_pin=3,active_low=1,gpio_pull=up
```

This setup will give you a pulsating heartbeat indicator and a convenient power button.

Reboot your Pi, and voilà! Your Raspberry Pi now has a handy power button and a cool heartbeat indicator, making it more user-friendly, especially for headless operations.
File renamed without changes.
65 changes: 65 additions & 0 deletions docs/blog/posts/ros_shortcomings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
date: 2023-07-27
categories:
- ROS2
draft: true

---

# Where ROS2 falls short

If you are into robotics, you just can't ignore ROS. In fact, ROS has become defacto standard platform
for robotics.
While it is widely agreed that ROS1 is not suitable for industrial applications, it's redesigned brother ROS2 is
not without its own shortcomings. I've been developing autonomous robots for a couple of years now. When I started
with my first robot, choosing ROS was logical, everybody else was using it.
I started with ROS2 Foxy and my first impression was very positive. But as the time went by, I felt that ROS system
was getting in the way instead of providing a decent solution for my robotics needs. SStep by step I gradually moved
away from ROS to a pure Python system running in a Docker stack, resulting in a simpler system that is build upon
well-tested components.

<!-- more -->

In this article I'll share with you where I think that ROS gets in the way and provide a better working alternative.

But first, let's recap on what ROS actually is. It is a software framework providing:

1. inter-process communication
2. package management (deployment)
3. node management (launcing configuratoin etc.)

ROS provides more than that, but in essence these are the most important parts.

Inter-process communication is essential for creating a heterogenous asynchronous system.
Distributed system of ROS works well when operating within same subnet. Sending messages to a remote
system (or even communicating to host of a docker container) can be major pain. I've spent countless hours trying
to connect hosts on different subnets without success. And connecing nodes in separate (remote) subnets is just
not something that I have time for figuring it out. You'll need to start digging trhough documentation about communication layer,
do some math to calculate which ports are used for a domain id etc.

An alternative could be a centralized protocol, like MQTT. You know to what host and which port you need to connect. If something
does not work, troubleshooting is easy.


As for package management...


## Story in bullet points

* I develop preferably in Python, it enables me to build functionality faster with less code and provides access to a wealth of tools and packages.
* ROS has C++ as primary language, using Python is a second-class citizen.
* The way the code needs to be packaged is different from standard python packages. Why do we need `ros2 run ...` while an executable can be installed on the system?
* ROS uses it's own custom and complex launch system. Systemd and docker stacks are much more mature and well designed solutions.
* Defining and compiling interfaces causes development overhead.
* Getting data across subdomains can be a major pain. An alternative could be a centralized protocol, like MQTT. You know to what host and which port you need to connect. If something
does not work, troubleshooting is easy.
* Performance - based on my benchmark ping-pong between two nodes in ROS is approximately 100 times slower than same system implemented with asyncio in pure Python.

Conclusion: for a Python-first system ROS is not optimal.
After working with ROS for a couple of years I found an architecture that works much better for me:

* The system is split in a number of subsystems, each running in a separate docker container. This achieves modularity and there are great mature tools for container management.
* Within each subsystem asyncio is used for running concurrent tasks. Inter-node communication is easy and blazing fast with queues. There is no risk of race conditions, that's a trait of
asyncio not running multiple threads.
* Subsystems communicate with each other through MQTT (mosquitto broker is running in a separate conainer). This is still much faster than sending messages through rclpy. Not sure
why, but the benchmark proves it.
Loading

0 comments on commit d25ed61

Please sign in to comment.