this article will contain little discussion and stories about the mechanics of robots and the process of developing drives and housings. It will be an overview of exactly how I developed the software part, and how quickly training actually occurs on a real project. The materials were scraped from correspondence in VK and Telegram, because it did not occur to us to document our developments in detail.
The core of the team consisted of 5 people: Me (programmer), Dima (chief mechanic), Pavel (electrician), Alexander (mechanic), Mihaba (jack of all trades).
Eurobot is a former international robotics competition that has two categories: Junior and Open. In both categories, the basic rules are similar: 1-2 robots from a team perform various game tasks, trying to score as many points as possible in 100 seconds. They do this together with the robots of the opposing team, while actively interfering with the other team is allowed, but collisions and outright aggression are prohibited. This format, as well as the size of the field, have changed little from year to year, starting as far back as 1998, when the first Eurobot was held in France.
Once international - because starting from 2020/21, the organizers refused to accept the winners from the All-Russian stage to the international one. But this did not stop the Russian side of the organizers from holding regional and All-Russian stages. Not so Open.
In the Junior League, young people under 18 make robots with remote controls (wired/wireless). But it’s one thing to assemble a cart using Chinese examples, and another thing to assemble a fully autonomous robot. The contrast in complexity between even a sophisticated remote-controlled robot and a simple autonomous one is colossal.
In a cozy SPO for installation and repair of electronics, our ideological and wonderful teacher decided to participate first in the Junior version, and then in the Open. There will be no story about the Junior League here, although it was also an unforgettable experience.
ROS
Robot Operating System - a meta operating system for developing robots. In fact, it is a large bundle of packages that provide development tools, runtime, and many ready-made solutions that are needed when programming robots.
The main feature of ROS 1 (both years were on it, namely the noetic version) is the master, which controls the exchange of messages between robot processes (nodes). This whole system is very similar to MQTT, where there are topics that have subscriber nodes and publisher nodes. The structure of messages is described in advance and the code is generated at the build stage for all supported languages: C++, Python, JS.
There are also extensions to the PUB/SUB model: services and actions. Actions were not used by me in both projects. Services allow synchronous access to the “server” - the service executor.
Eurobot Open 2022
The rules of the competition are laid out in a rough form in September and slightly refined and clarified over the course of a couple of months, with the competition itself taking place around mid-May. This year’s theme was “excavations of an ancient robot civilization.”
It was the 4th course out of 5 in my SPO. Just in September I got a job at a service center at a local Internet provider. Some kind of job, where I earned crazy money and then did industrial training.
Having received the rules and suffered enough in preparation for the Junior League last year, we knew that we had to start right away! But there is still so much time ahead, we can not worry, right? Apparently, experience has taught us poorly
Eurobot 2022 Field Model
Time flew by. In the background, some absolutely minimal development on my part and the chief mechanic’s sketching out of ideas about mechanics were just beginning. Fortunately, I immediately guessed to conduct development on github . In the commits right before and during the competition, I no longer had time for messages in the commits.
In fact, the main work fell on the last 3 months. You can see the pain in the dates of the competitions themselves
I barely knew what programming was. My only experience was writing a Pong/Breakout clone for two on pygame. Why ROS then? The thing is, we also participated in the WorldSkills service robotics competition , which was super-super fresh at the time.
At this competition I first got acquainted with Linux. And of course with ROS. But using and minimally tweaking a ready-made educational robot in hothouse conditions != developing something of my own. So of course I decided to refuse to use ready-made packages like slam_navigation. In fact, I did not understand how to use them. In retrospect, refusing ready-made solutions was a good idea. But in the first year it was terribly difficult.
Ross was a good help in terms of visualizing what was going on in the robot: rviz made it relatively easy to draw a map and a route on it.
I didn’t completely abandon the ready-made. I used a node architecture similar to what is used in slam navigation
The final ultra poster was made almost on the knee
I dove into CMake like into cold water, having never worked with it before. I was familiar with C and C++ only through Arduino, and here is an extended build system on top of simake (catkin), which allows you to “painlessly” build packages from GitHub and use them in other projects in the system through squats. For me, simake was like a scary black box that does something.
Firmware for Arduino
The first thing that needed to be developed was firmware for the Arduino Mega 1560. We had Chinese motors, plastic omni-wheels and convenient shields with drivers for Moebius motors (just right for the mega).
The purpose of the Arduino in the project: it had to process the encoder ticks and control the motors. The firmware on the Arduino provided a topic for controlling the motors, and also wrote encoder ticks from the motors to another topic. One of the reasons for choosing ROS was the impression that it would simplify the writing of the interaction of the Arduino with the Raspberry.
But in fact, the development was very inconvenient: in order to assemble the firmware, it was necessary to have the Arduino IDE installed and dance with the generation of libraries for the Arduino, including code generation for standard and custom message types. Closer to the middle, I wrote a simple script for myself with automation of this process . I especially like the way it defines the folder with the firmware).
This was almost the longest stage: writing a PID controller, as well as kinematics for the omni wheels. The kinematics turned out to be quite simple - project the vector of the desired direction onto the vectors of the wheel directions. I heard that float is not very good to use in time-limited code, but I didn’t care. Half a month to give birth to such code. Although of course it takes longer to test and learn to select coefficients.
const float dtime = (loop_delay / 1000.0);
void PID(int mot)
{
float error = targ_spd[mot] - curr_spd[mot];
inter_term[mot] += dtime * error;
pwm[mot] = error * prop_coeff[mot]
+ inter_term[mot] * inter_coeff[mot]
- (error - last_error[mot]) / dtime * diff_coeff[mot];
inter_term[mot] = constrain(inter_term[mot], -30000, 30000);
last_error[mot] = error;
pwm[mot] = constrain(pwm[mot], -255, 255);
}
At first he crawled, but then he learned to ride. However, in both years of competitions I still haven’t found good coefficients
Costmap server
By this time I had already quit my job.
Obviously, I myself would not have thought that some kind of costpama is needed, but I saw how it is used in educational robots. But I quickly realized that it greatly simplifies the construction of the route: bypassing targets at a safe distance due to the fact that each point that cannot be driven gets a “cost” of 100. A point is the smallest unit on the navigation grid, we had 2x2 centimeters, my python code would not have been able to handle a more accurate descriptor
All other points receive decreasing cost depending on the proximity to full obstacles. Such a system allows to set the threshold of the maximum price at which the robot can go, and also, for example, to go slower where it is “dangerous”
For some reason, something went wrong in me in this project and almost all the nodes were executed not even as singletons, but as Python classes, in which everything is static. In general, it is not shameful to simply make global variables and functions in a node, because one program is launched in one or several copies and all modularity is built at the level of several processes. But I wanted OOP! As a result, this only added garbage and noise to the code, it is not clear why
class Costmap():
publish_on_obstacles_recieve = rospy.get_param('~publish_on_obstacles_recieve', 1)
write_map_enable = rospy.get_param('~write_map_enable', 1)
debug = rospy.get_param('~debug', 1)
interpolate_enable = rospy.get_param('~interpolate_enable',1)
...
@classmethod
def publish(cls):
msg = OccupancyGrid()
curr_time = rospy.Time.now()
msg.header.frame_id = "costmap" ###????????
msg.header.stamp = curr_time
msg.info.resolution = cls.resolution
msg.info.height = cls.height
msg.info.width = cls.width
for y, x in cls.grid_parser:
#print(x,y)
data = int(cls.grid[y][x] + cls.mask[y][x])
if data > 100:
data = 100
msg.data.append(data)
I had a hard time with this wonderful map publishing method because I was filling out the parameters in the OccupancyGrid message incorrectly.
Attempts to draw a costmap in rviz
You can also notice this line
data = int(cls.grid[y][x] + cls.mask[y][x])
My server was divided into a static map (which was loaded from a .png image) and dynamics, which came from the lidar. There were many times fewer points for which “inflation” in dynamics was needed; in my opinion, the obstacle was just one point (a group of points on the lidar). You can clearly see the incredible speed of my algorithm for inflation here.
I had a thing for a dark, eye-catching, high-contrast theme in VSCode back then.
I read this code and absolutely do not understand what was going on in my head at that time. Like these pearls:
for num, _tup in enumerate(cls.grid_parser):
y,x = _tup
#if y/50 >= 1.5 and x/50 >= 1.5:
#raise SyntaxError()
...
def obstaclesCallback(obst):
if _node_ready:
Objects.clear()
for obj in obst.data:
...
Objects.updateMask()
# AHAHAHAHAHAAHAHA
# AHAHAHAHHAHAHAHAAHHAHAHHAHAHAAHAHAHAHAHAHAHAHAHAHAHAHHAHAHA
The comment is original 3 years ago. I think I was looking for this bug for a long time, it was also floating. Objects.updateMask()
It was two tabs to the right and I did not notice it for a long time.
Global planner
The biggest sacrifice in this project. No, I didn’t read about what kind of people in general came up with planners in the world. I went my own way. I came up with a brilliant idea - something like ray marching.
- Throws a vector of fixed length to the target
- If none of the points through which the vector passed is too expensive, then a piece of the route is built, we throw the next one
- If not, then we start to sort through, turning the vector left or right, until we have tried all the options or we can’t set the vector without hitting obstacles (180 degrees was the maximum, in my opinion)
- ???
- Profit
Someone will say that this is a terrible route planner - and they will be absolutely right. This is nonsense! I just really liked the Mathologer video about e^pi = -1. And I thought - damn, you can rotate vectors in Python by multiplication, if vectors are complex numbers
This node suffered the most from endless tweaking. For some reason, a piece of logic made on the trendy Action Server for performing movement tasks was moved here. Total chaos .
But. It worked. It worked because everything was built on the assumption that obstacles were round. A ton of crutches were hung up so that it wouldn’t fail to build a route at every sneeze, or, God forbid, in the corner of a field.
Awesome route
Even better
Local scheduler
Local planner - a node that translates a route built in the global into specific commands for the Arduino to execute. That is, it translates a global command, for example, “go up X”, into a local one, depending on the current rotation of the robot (for which up X can be forward and backward and anything).
As with all my nodes in this project, over time this miracle acquired specific crutches and “optimizations”. There was some convoluted logic that you can go not to the next point of the route, but skip a certain number of them depending on the freeness of the route, but I don’t remember exactly. In any case, you can’t go straight to the next one, because it’s easy to skip a point and not mark it as passed.
There was no order here - total chaos, throwing brilliant ideas at the fan. And time was ticking. Was it already a month and a half before the competition? Or one? In order for at least something from this mess to work, it was necessary to tediously select parameters for a long time, add hacks. But my favorite.
In the video below, the robot is lying on a table at my house on cans and spinning its wheels in the air, which at that time were its only source of position (the lidar was not ready yet).
My happy reaction to my first successful route
Later video
LiDAR Processor
Each node does very little individually. This is of course the advantage of ROS. It imposes an architecture. Yes, for a simple robot it is all quite overcomplicated, but overall the architecture is correct. I judge by myself. Being an absolute zero, I wrote code, and as soon as one file grew beyond 500 lines, chaos began in it. Still, in retrospect, ROS was useful from this side. A clear and sharp cutoff of the boundary between processes forced me to write from the beginning.
The small, testable nodes were combined together into something that already looked like a robot. Now it was time to start up the wonderful lidar, which had only been gathering dust all this time.
I haven’t read any articles or guides on working with lidar, but I had some experience using it, again in the turtlebro robot. I’ve already gotten the hang of using ROS, and even learned how to display beacons in RVIZ.
The starting position of the robot is set in advance. The coordinates of the reference beacons are also set in advance. There are special sites for them on the field. There are 3 beacons in total. To obtain lidar scans, the ROS package supplied by the lidar manufacturer was used - rplidar-ros-noetic.
I came up with the following algorithm:
- A walk is performed based on the points coming from the lidar (full 360 degree scan - 8k points at 10 revolutions/sec ~800 points per revolution). And if a sharp jump in distance is detected, the walk ends at the current point (an object has been found). We continue from the new point (which has just exceeded the distance threshold). And so on until the points from the entire lidar revolution run out. Also, the “object” is cut off if the number of points per object is exceeded (a solid wall of points will become a wall of “objects”)
- The “object” closest to the expected position of the beacon (white cylinder in the video) will be considered the “real” position of the beacon (pink cylinder).
- Every N seconds the average deviation of the position of the “real” beacons from the “expected” ones is calculated. (If at least 2 beacons out of 3 are visible)
- This deviation multiplied by a factor less than 1 is subtracted from the robot’s position.
In this way, lidars can correct for the accumulating error from the encoders on the wheels.
This is a bad algorithm, very bad. It barely worked. You had to pray that your own or enemy’s robot wouldn’t block the beacon. It would be even worse if someone stood next to the beacon and became a “beacon” themselves. Our cheap lidars couldn’t distinguish materials. More precisely, they could, but they lost a third of the points, and even without that they could barely see beacons as thick as a cylindrical can of chips (which is what we used).
Screenshot at the time of testing (here it is about a week before the regional stage)
Job Manager
There’s the top level left and two weeks until the competition. What could go wrong?
I took the upper level ambitious - I will parse yaml files. Here I already knew how to use asyncio (not right away, pain at first). I learned a little something similar to OOP and essentially made two DSLs - one for “scripts” and the second for routes. The route yaml could call services from “scripts”.
Asinkyo was pretty useless there, that thing was actually almost completely synchronous.
“Scripts” or “Calls” as I called them allowed me to give names to simple actions (so that the code would not set the servo position to some 50%.
- "ArmUp":
- "servos_service":
- "num": 0
- "state": 80
- "ArmDown":
- "servos_service":
- "num": 0
- "state": 0
I don’t know how, but in such a short time I managed to throw a lot of things into this system: interrupts, calls with arguments. For example, there was a task in which you need to measure the resistance of a resistor to decide whether to turn over a wooden tile or not. My solution was: if a message comes that the measurement showed the required value, we proudly raise the manipulator to turn the tile. Moreover, the timer could also be the source of the interruption (for example, through this an emergency stop was made at 98 seconds).
Most of the features had to be abandoned because two days were not enough to write normal routes and assemble the necessary parts of the robot.
A small piece of the route of robot 2 (beans) - its task was to swap the cubes (the statue and the replica):
tasks:
za_statuetkoi:
- call: AdjustOn
- call: NeNadoPlitka
- change_cost: 50
- move : 1/2.6/0.23
- change_cost: 8
- call: AdjustOn
- move : 0.49/2.58/0.2
- sleep: 0.5
- move : 0.49/2.58/0.2
- call : MicroArmHalf
- call: PumpOn
- sleep: 0.2
- change_cost: 5
- move: 0.365/2.695/0.2
- change_cost: 8
- call: MicroArmUp
The result of the work
We took part in the regional stage in Kaliningrad. We were the only team.
Two weeks of preparation for the All-Russian stage flew by in an instant. A lot had been done. And then we arrive in Moscow, go to Korolev already tired And… we don’t pass the selection (the so-called homologation - checking that the robot avoids collisions). Well, no matter - half a night of war, walking to the hotel, a trillion parameter edits in the morning and we barely pass the selection.
We could have taken second place. It was against us that the Skolkovo team had one of the two bots fail, but things went even worse for us. Well, nothing against us, not the strongest team in the battle for 3rd place… Failure. I don’t know what happened. The robots just stopped. Maybe the position got lost. Maybe I set the robots up crookedly. I don’t know. On the one hand, it’s a shame. On the other hand, that’s it. Lord, finally this happiness and torment is over.
Unlike the mechanics who could enjoy the rest and have fun at the competitions, I worked hard. This is also a bad idea, usually only more harm is caused by last-minute adjustments.
It was fun, damn fun. I learned a lot. Invaluable experience. The project is educational after all and I am very glad that I decided to saw my crooked crutches. And it doesn’t matter that we took 4th place - it’s not bad either, next year the crutches will shine. Working with a team of friends on such a project was really cool. I celebrated our 4th place at the hotel with my comrades with a proud can of Baltika 9.
The best was heat number
1. We also had such a funny opponent who did almost nothing - the team was an elongated bean. But you shouldn’t laugh at them - the following year they put on a real show. Incredible.
But this is a story for the second part of the article, where there will be code on pluses. Otherwise, it’s all python and python. In it, I managed to work on pluses for money at my first job as a programmer and make a candy from the experience of the first year!
Well, here is a video of our robots in a more finished form.