This repository contains the documentation for ChaBots participation in the WRO Future Engineers 2025 category. Our robot was designed and built by a Mexican students team, passionate about robotics and education.
- 🧑💻 The Team
- 🎯 The Challenge
- 💭 Discussion
- 🤖 Robot Overview
- 🔋 Sense Overview
- ⚙️ Mobility Management
- 💡 Electronics
- 💻 Code Overview
- 🚧 Obstacle Management
- 🛠️ Construction Guide
- 💰 Cost Report
- 📚 Resources
- ©️ License
Age: 20
Role: Captain, Electronics & Software Designer
I am a self-taught robotics enthusiast with experience in embedded systems, software, and mechanical integration. my team ChaBots Ocelot won Mexico Robocup soccer Open second place and achieved multiple national awards in programming and robotics.
"I enjoy setting nearly impossible goals to push myself while learning. I believe that learning should always lead to building something real."
Age: 20
Role: Mechanical Designer
I am a Mechatronics Engineering student passionate about technology and innovation. I have been a contestant for eight years, winning various regional and national competitions, and participating internationally.
"I will take any opportunity to grow"
Age: 22
Role: Software Developer
I am a Software Engineer student and I love robotics and programming. I have been a contestant for 8 years, from regional to national competitions, I have participated in 4 Mexico Robocup Soccer and 1 MakeX Robotics Competition, top 3(2019) and runner-up(2019).
"Anyway, robotics is my passion and I will never forget it."
Age: 22
Role: Coach
I've been involved in robotics for 14+ years being a programmer for most of the projects I've taken part in. I've had may regional, national and international experiences. Now I'm working in sharing my knowledge with more people to push further their level and potential as well as helping them achieve their goals and find their passion.
"I like to face challenges and even more so when it's with more people. Learning and creating something is better when shared."
The WRO Future Engineers challenge pushes students to create fully autonomous self-driving vehicles. Each robot must:
- Navigate a dynamically randomized track
- Detect and avoid colored obstacles (green/red blocks)
- Execute a parallel parking maneuver
Scoring is based on:
- Performance on track
- Obstacle handling
- Documentation quality
- Innovation and engineering rigor
For more indo visit: WRO Official Site
Throughout this project, we faced numerous critical decisions that shaped our robot's final design. Our approach was driven by the principle of creating a robust, reliable system capable of handling the unpredictable nature of autonomous navigation challenges.
We chose a distributed ROS2 architecture for several key reasons:
- Modularity: Each sensor and algorithm runs in isolated nodes, making development, testing, and debugging significantly more manageable
- Scalability: Adding new sensors or algorithms requires minimal changes to existing code
- Real-time Performance: ROS2's real-time capabilities ensure consistent timing across all systems
RPLiDAR C1: Despite being expensive, the 360° scanning capability proved essential for complete environmental awareness. Alternative sensors like ultrasonic arrays couldn't provide the same level of detail and accuracy.
OTOS Sensor: The optical tracking approach was chosen over traditional wheel encoders due to its immunity to wheel slip and superior accuracy in dynamic environments.
Raspberry Pi 5: The increased computational power over previous generations allowed us to run complex computer vision algorithms in real-time while maintaining ROS2 communication overhead.
Our experience at the regional competition was... challenging, to put it mildly. We didn't perform as expected, our robot's performance was disappointing during those crucial moments.
- Sensor calibration issues under competition lighting conditions
The regional result was a wake-up call that forced us to completely re-evaluate our approach.
We invested approximately 500 hours into completely rebuilding and refining every aspect of our system:
Complete Software Stack:
- Migrated from basic control loops to PID controllers with adaptive parameters
- Implemented sensor fusion algorithms for robust state estimation
- Developed multi-modal obstacle avoidance strategies
- Created comprehensive safety systems with multiple fallback mechanisms
Hardware Redesign:
- Switched from 3D-printed chassis to carbon fiber for improved rigidity
- Upgraded to precision steel shafts and custom gearboxes
- Implemented proper electromagnetic interference shielding
- Redesigned cable management for reliability
Testing Infrastructure:
- Built a complete testing environment that simulates competition conditions
- Developed automated testing scripts for regression testing
- Created comprehensive calibration procedures
Our sensor fusion approach combines OTOS, LiDAR, and vision data using Kalman filtering techniques that provide centimeter-level accuracy in dynamic environments.
The adaptive control system automatically adjusts PID parameters based on track geometry, allowing optimal performance in both tight corners and wide corridors.
Achieving 10Hz control loop frequency while processing:
- 360° LiDAR scans at 8Hz
- Computer vision at 30fps
- OTOS updates at 100Hz
- Safety monitoring at 100Hz
- Multi-sensor validation prevents single points of failure
- Graceful degradation when sensors malfunction
- Emergency stop systems with sub-20ms response times
- Automatic recovery from most common failure modes
Our approach to the WRO Future Engineers challenge evolved significantly:
Initial Strategy (Regional): Focus on basic navigation with simple obstacle avoidance Current Strategy (National/International): Comprehensive autonomous system with advanced AI-driven decision making
- Reliability over Speed: Consistent completion beats occasional fast times
- Sensor Redundancy: Multiple sensors for the same measurement prevent catastrophic failures
- Adaptive Algorithms: One-size-fits-all approaches don't work in dynamic environments
- Extensive Testing: Simulated conditions must exceed competition difficulty
Name: Eva
|
|
|
|
360° laser scanner for environmental mapping and obstacle detection.
Tech specs:
- 360° scanning with 0.9° resolution
- Up to 8m range with 10Hz update rate
- Quality filtering for reliable data
- ROS2 integration via
rplidar_rospackage
Link: RPLiDAR C1
High-resolution camera for color object detection.
Tech specs:
- 12MP IMX708 Quad Bayer sensor and features a High Dynamic Range mode
- Supports 1080p30, 720p60, and VGA90 video modes
Link: Raspberry Pi Camera V3
High-precision odometry sensor for accurate position tracking.
Tech specs:
- Measures linear and angular displacement
- High-resolution optical flow sensor
- ROS2 integration via custom
otos_readernode
Link: SparkFun OTOS
The robot's transmission features a custom-designed gearbox, with the base and gears developed in CAD software and manufactured in-house. For fabrication, the team used a Creality K2 Plus Combo printer, chosen for its reliability in handling engineering-grade materials. The material selected was Polymaker PETG-CF (a carbon-fiber-infused PETG), prized for its high stiffness, dimensional stability, and excellent wear resistance, which are critical for durable mechanical components. A key design feature is the use of double helical gears. This geometry was chosen over standard spur gears to ensure smoother, quieter power transmission with reduced vibration and superior load distribution. This significantly improves mechanical efficiency and component lifespan. The drive axle consists of 4 mm steel shafts, which were custom-cut from rod stock. To ensure positive torque transfer from the gearbox to the wheels, the ends of the shafts were manually modified using a Dremel tool to create a "D" shape. This profile prevents slippage between the shaft and the wheel hub, a common failure point in high-torque applications.
| Part | Description | Image |
|---|---|---|
| 6.1.1. Maxon DCX19 | The powerhouse for the gearbox is the Maxon DCX19, a 19 mm brushed DC motor equipped with an integrated planetary gearhead. This motor was selected for its high power density, precision, and proven reliability. The integrated gearhead is the primary source of torque multiplication, providing a final output speed of 600 RPM. This combination delivers the high torque and controlled speed necessary to meet the robot's performance requirements for acceleration and payload handling. | ![]() |
| 6.1.2. Base | We designed the base of the gearbox so that the wheel axle is as close as possible to the steering axis in order to make tighter turns. | ![]() |
| 6.1.3. Gears | The custom-printed double helical gears transfer power from the motor's gearhead output to the wheel axle. This external gear stage was designed with a 1:1 gear ratio. This configuration was chosen because the Maxon motor's integrated gearhead already provided the ideal speed reduction (down to 600 RPM) and torque multiplication. The 1:1 external gears, therefore, act as a direct power transmission, simplifying the design while perfectly matching the motor's output speed to the drive wheels. | ![]() |
| 6.1.4. Wheels | The rear wheel hubs (rims) were also custom-designed and 3D-printed to integrate perfectly with the transmission. The central feature of the hub is a D-shaped bore (hole). This profile is precisely matched to the D-shaped 4 mm steel shafts, ensuring a secure, non-slip mechanical lock. This method guarantees that all torque generated by the motor is effectively transferred from the axle directly to the wheel. | ![]() |
| Part | Description | Image |
|---|---|---|
| 6.2.1. Servo HiTEC HS-85MG | We selected the HiTEC HS-85MG for our robot's Ackermann steering system, primarily due to its robust metal gears (MG). Unlike many standard or smaller servos that use plastic gears, the metal gearing provides the significantly enhanced durability and resistance to stripping that our steering mechanism requires. This is crucial for us to handle the mechanical loads, vibrations, and potential impacts inherent in the system's operation. We also find that this servo packs considerable torque and good precision into a compact "mighty mini" form factor, supported by a top ball bearing. This ensures it provides the strength we need to turn the wheels effectively while maintaining accurate steering angles, minimizing the excessive "slop" or backlash we might see in less robust options. For application, this blend of power, durability, and reliable accuracy in a small package makes it a superior choice over servos that could fail or wear quickly under the demands of steering. | ![]() |
| 6.2.2. Base | We designed the base around the servo, so that everything was symmetrical. We also designed the base to be modular and easily attach to the robot's chassis for easy repairs. We 3D printed the base using carbon fiber filament, as we did all the other robot parts, to increase strength. | ![]() |
| 6.2.3.1. Servo Connector | We designed the servo connector this way because, as an Ackermann system, the wheels needed to be connected independently of each other. If we used a single connector for the wheels, both would have the same turning angle, but by splitting it, each wheel would turn at a different angle. | ![]() |
| 6.2.3.2. Wheel Connectors | As mentioned above, we used two connectors, one per wheel, so they rotated independently. These connectors aren't straight, as this shape allows us to more clearly define the angle differences between the wheels. However, we arrived at this shape experimentally, as if the shape were more pronounced, we reached a point where the wheel wouldn't return to its original position. | ![]() |
| 6.2.4. Wheel Supports | We designed the L-shaped wheel mount so that the wheels could rotate more easily and not collide with the robot's chassis. | ![]() |
| 6.2.5. Wheels | We decided to make our own wheels because we couldn't find any commercial wheels that fit our robot. We previously used the Lego Spike wheels, but they had very little contact surface area, so we decided to create our own wheels using the measurements of Spike's wheels. To do this, we created a rim and 3D printed it. We then used a mold and polyurethane resin to make the rubber. This process is shown in the following video: WRO FutureEngineers Custom Wheels - chaBots NERV | ![]() |
The steering system is mounted on the chassis using 20mm-high M3 posts. The odometer PCB is anchored below the steering system, as this makes better use of space. The gearbox is mounted directly to the rear of the chassis, and the Raspberry Pi 5 is mounted on it using 20mm-high M2.5 posts. The main PCB is mounted in the middle, and the Lidar base is mounted on 20mm-high M3 posts. The Raspberry Pi camera v2 base is mounted on the Lidar base using 40mm-high M3 posts. Using these poles helped us keep the robot as low as possible, allowing the Lidar sensor to be leveled with the runway walls.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
All of the electronics in our robot are designed to be as fast and reliable as possible. This led us to use higher‑end components and a clear division of responsibilities between processors. For instance, it is not a good idea to use a Raspberry Pi 5 directly to control a servo and a DC motor—the OS and user processes introduce latency and consume CPU time. To address this, we use a dedicated microcontroller for time‑critical I/O and control loops: the Teensy 4.0.
Before choosing, we considered both performance and the strict space constraints of our mechanical design. Everything must fit within our allotted envelope without compromising serviceability.
| MCU | Clock speed | Observed PWM behavior* |
|---|---|---|
| Teensy 4.0 | 600 MHz | Excellent |
| Arduino Nano | 16 MHz | Good |
| Raspberry Pi Pico | 133 MHz | Inconsistent at low duty |
*Notes on PWM behavior: Using the same nominal PWM frequency across all three MCUs, we observed different motor responses. On the Teensy 4.0, the motor starts smoothly and can deliver more torque at lower duty cycles. The Raspberry Pi Pico struggled at very low speeds and required careful tuning. The Arduino Nano behaved as expected for an AVR‑based board. We could not find definitive documentation explaining these differences; our working theory is that PWM implementation details (SDK, drivers, timer resolution, and library quality) play a role. Teensy and Arduino platforms are primarily C/C++ with mature vendor‑maintained timer libraries, while typical Pico workflows often lean on MicroPython or community libraries, which may trade convenience for timing granularity.
-
Main battery: 3S (11.1 V) LiPo, ~2200 mAh.
-
Regulation: Pololu D42V55F5 step‑down regulator (5 V, up to 6 A) feeding the Raspberry Pi 5 and the servo.
-
Isolation: The Teensy 4.0 is powered from an isolated supply to protect it from motor/EMI transients; all other subsystems draw from the main rail via appropriate regulators and filtering.
We use the VNH7070AS full‑bridge driver. It comfortably handles the required current (≈8 A continuous in our use case) and supports supply voltages above 20 V, providing generous headroom. Because there is no widely available off‑the‑shelf module for this device, we integrated it directly onto our PCB. This raises the bar for assembly, but the result is more reliable and, in practice, more cost‑effective.
Our PCB uses 6 layers:
| Layer | Type |
|---|---|
| 1 | Signal |
| 2 | GND |
| 3 | Signal |
| 4 | Signal |
| 5 | GND |
| 6 | GND |
This stack‑up, combined with stitching vias, short return paths, and careful placement, yielded a robust, fully integrated board with *low EMI emissions. Despite having the motor as close as *5 cm to sensitive wiring, we can communicate at high baud rates reliably.
Summary: Separating real‑time control (Teensy 4.0) from high‑level compute (Raspberry Pi 5), providing clean power (isolated Teensy rail + regulated 5 V), and integrating an automotive‑grade driver (VNH7070AS) on a 6‑layer PCB gives us the responsiveness and robustness demanded by the challenge.
Our PCB design is implemented across three specialized boards to maximize reliability and minimize electromagnetic interference:
Primary power distribution, motor driver (VNH7070AS), and system control. |
OTOS sensor mount and signal conditioning. |
Robust I/O connections and UART bridge for Pi. |
-
Connector: XT30 (U14/U19) for main power.
-
Transient suppression: TVS diodes on the input and 5 V rails (e.g., SMBJ18 class at the battery side and SMAJ5.0CA on the 5 V rail) clamp surges from motor commutation and cable hot‑plugging.
-
Input filtering: Bulk electrolytic (≈470 µF) plus local ceramics (100 nF) provide low‑ and high‑frequency decoupling. Keep the electrolytic close to the power switch/step‑down input and sprinkle 100 nF near every IC supply pin.
-
(Recommended) Add a resettable fuse (polyfuse) sized for your continuous draw and an ideal‑diode OR high‑side FET for reverse‑polarity protection if you expect frequent battery swaps.
-
Function: A soft‑latching high‑side switch drives a low‑RDS(on) P‑channel MOSFET (TPH1R712MDL) to connect/disconnect the main rail.
-
Control: A momentary/toggle switch (SW3) biases a small driver network (Q3 + resistors) that pulls the MOSFET’s gate. The RC on the gate provides gentle inrush and reduces connector arcing.
-
Why high‑side: Keeps grounds common and only switches the positive rail, avoiding ground‑bounce issues with USB/UART connections to the PC.
-
Module: Pololu D42V55F5 (5 V @ up to 6 A). Place it so that current from the battery flows battery → switch → regulator → loads.
-
Post‑filtering: An LC section (e.g., *L1 = 22 µH, **C9 ≈ 470 µF, plus *100 nF ceramics) reduces switching ripple seen by the Pi and the servo.
-
Surge/ESD: A SMAJ5.0CA across the 5 V rail provides additional protection. Keep diode and bulk cap leads short and wide.
-
Grounding: Return the regulator ground straight to the GND plane and keep the high‑di/dt loop (switch → inductor → diode/cap → switch) as tight as possible.
-
Role: Real‑time control (PWM generation, sensor timing, safety interlocks). The Pi handles high‑level logic/vision.
-
Supply isolation: The Teensy is fed from a clean/isolated input to protect it from motor noise. Decouple 3.3 V with multiple 0.1 µF and a 10 µF bulk close to VIN/3V3.
-
I/O mapping: Dedicated pins for PWM to the motor driver and for *servo. A *UART header (H3) exposes TX/RX/GND for diagnostics.
-
Signal integrity: If runs exceed ~10–15 cm, add 33–100 Ω series resistors on digital lines (IN1/IN2/PWM) to damp edges and reduce ringing into the VNH driver.
-
Device: VNH7070AS integrated H‑bridge; internal MOSFETs handle current spikes and integrate flyback paths.
-
Interface: Pins INA/INB/PWM/EN/CS (nomenclature varies per package) connect to the Teensy. Keep logic ground common with power ground at a single low‑impedance point.
-
Decoupling: Place ≥ 470 µF bulk right at VCC of the driver and multiple 100 nF ceramics. Add an input TVS (SMBJ18 class) across motor supply near the driver.
-
Snubbing: For very noisy motors, an RC snubber (e.g., 100 nF + 1–2 Ω, tuned empirically) across the motor terminals can reduce EMI. Keep motor leads twisted and short.
-
Thermal: Provide a solid copper pour under the thermal pad with many thermal vias to GND to spread heat to inner planes.
-
Servo header: Powered from the regulated 5 V rail; keep return path next to the 5 V trace.
-
Battery sense / telemetry (optional): If you expose the pack to the MCU, use a divider + RC and consider *TVS/small *series resistor to protect the ADC.
-
NeoPixel header (P1): Decouple with 100 nF at the connector and, if long strips are used, a large electrolytic (≥1000 µF) at the first LED.
-
Stack‑up: 6‑layer with three GND planes (L2/L5/L6) provides low impedance return and shields signals.
-
Star power: Route battery → switch → regulator → loads with star‑like branching; do not daisy‑chain sensitive logic behind motor currents.
-
Keep loops tight: Especially the driver’s switching loop and the regulator loop. Use wide pours for battery and motor paths.
-
Segregate zones: Physically separate power (battery, driver, regulator) from logic (Teensy, level‑signals). Cross at right angles if they must cross.
-
Stitching vias: Surround the driver and the high‑current paths with plenty of GND stitching vias to contain fields and lower EMI (already used here).
-
Power the board with a current‑limited bench supply (e.g., 0.5–1 A) and verify no abnormal draw.
-
Measure 5 V rail at the Pi and servo connector under light load.
-
With the motor disconnected, toggle the power switch—check gate voltage of the high‑side MOSFET for clean transitions.
-
Connect a small DC motor and run *10–20% PWM; scope *VCC and GND near the driver for spikes.
-
Increase load gradually; verify the driver’s thermal pad stays below spec and that bulk caps do not heat.
-
Verify UART debug works; confirm CS/FAULT lines (if used) change as expected under stall or overcurrent tests.
This is an autonomous robot developed with ROS2 using Python. The robot can navigate autonomously, detect obstacles, and detect colored objects. We decided on using ROS2 since it allows us to develop the different features of our robot modularly, which also makes each component and feature more manageable.
graph TB
subgraph "Hardware Layer"
HW1[RPLIDAR A1/A2/A3]
HW2[SparkFun OTOS Sensor]
HW3[Camera Module]
HW4[Teensy Microcontroller]
HW5[Motors & Actuators]
end
subgraph "ROS2 Driver Layer"
D1[rplidar_ros]
D2[otos_reader]
D3[Camera Driver]
end
subgraph "Processing Layer"
P1[vision_node]
P2[wall_follower]
P3[laser_filters]
end
subgraph "Control Layer"
C1[teensy_comm_node]
C2[teensy_obs_node]
end
subgraph "Middleware"
MW1[ROS2 Topics]
MW2[Transform Tree]
MW3[Foxglove Bridge]
end
subgraph "Visualization & Debug"
V1[RViz2]
V2[Foxglove Studio]
V3[Trajectory Markers]
end
%% Hardware to Drivers
HW1 --> D1
HW2 --> D2
HW3 --> D3
%% Drivers to Processing
D1 --> P3
D3 --> P1
P3 --> P2
%% Processing to Control
D1 --> C1
D1 --> C2
D2 --> C1
D2 --> C2
P1 --> C2
%% Control to Hardware
C1 --> HW4
C2 --> HW4
HW4 --> HW5
%% Middleware connections
D1 --> MW1
D2 --> MW1
D2 --> MW2
P1 --> MW1
P2 --> MW1
C1 --> MW1
C2 --> MW1
MW3 --> MW1
%% Visualization
MW1 --> V1
MW3 --> V2
P2 --> V3
classDef hardware fill:#ff9999
classDef driver fill:#99ccff
classDef processing fill:#99ff99
classDef control fill:#ffcc99
classDef middleware fill:#cc99ff
classDef visualization fill:#ffff99
class HW1,HW2,HW3,HW4,HW5 hardware
class D1,D2,D3 driver
class P1,P2,P3 processing
class C1,C2 control
class MW1,MW2,MW3 middleware
class V1,V2,V3 visualization
- File:
src/teensy_communication/launch/robot.launch.py - Function: Process sensor data and send the desired direction of the Ackermann and the speed of the motors.
- Features:
- Teensy configuration
- Odometry and sensor management
- File:
src/vision_node/vision_node/color_detection_node.py - Function: Green, red, and purple object detection
- Features:
- HSV filtering for specific colors
- Distance and angle calculation
- Noise filtering
- File:
src/otos_reader/otos_reader/otos_node.py - Function: Provides precise odometry using OTOS sensor
- Features:
- Software bias correction
- EMA filtering for smoothing
- ZUPT detection (Zero Velocity Update)
- Odometry and TF transforms publishing - Distance and angle calculation
┌──────────┐ ┌─────────────┐ ┌──────────────┐
│ Sensors │────▶│ ROS2 Nodes │────▶│ Control │
│ │ │ │ │ Algorithms │
│ • OTOS │ │ • otos_node │ │ │
│ • LiDAR │ │ • rplidar │ │ ┌──────────┐ │
│ • Camera │ │ • vision │ │ │ Decision │ │
│ • IMU │ │ • teensy │ │ │ Making │ │
└──────────┘ └─────────────┘ │ └──────────┘ │
└──────┬───────┘
│
┌──────────────┐ ┌─────────────┐ │
│ Actuators │◀────│ Commands │◀──────┘
│ │ │ │
│ • Motors │ │ /cmd_vel │
│ • Servo │ │ /motor_cmd │
│ │ │ /servo_cmd │
└──────────────┘ └─────────────┘
Using RPLIDAR C1 Ros2 package we can read the topic from our own node,
void on_scan(const sensor_msgs::msg::LaserScan::SharedPtr msg)
{
const float angle_min = msg->angle_min;
const float angle_inc = msg->angle_increment;
const float pi = static_cast<float>(M_PI);
lidarMSG.clear();
for (size_t i = 0; i < msg->ranges.size(); ++i)
{
const float ang = angle_min + angle_inc * static_cast<float>(i);
if (ang < -0.5235f && ang > -2.6180f || ang > pi)
continue; // 0..180°
const float r = msg->ranges[i];
if (!std::isfinite(r) || r < msg->range_min || r > msg->range_max)
continue;
lidarMSG.push_back({ang, pointAngX(ang, r), pointAngY(ang, r), r});
}
}This callback is called only when a the lidar node post a new message in topic, this module only stores a cloud of points sent by the lidar, the next step is procces this information to get the actual position of the robot in the track, we use the OTOS to update this information every call, because of the lidar only posting at 10hz is to slow, it means it only have new information every 100ms that make imposible to have a smoth move, the OTOS post at 100Hz, meaning we can smoth our trajectory 10 times more using this, we dont use absolute infromation like coordinates in x or y axis, we take the changue in cm betwen the new read and the last read and add it to the cloud of points, also we calculate some infromation to help our robot to not colide with the walls, like the distance in front, left and rigth, this help to calculate optimal PD controller values relative to robots position and speed.
void getOffsetsFromLidar()
{
if (new_otos_data.load())
{
const float yaw_prev = deg2rad(lastYaw.load());
const float dx_w = posX_.load() - lastPosX.load();
const float dy_w = posY_.load() - lastPosY.load();
const float dth = wrapPi(deg2rad(yaw.load() - lastYaw.load()));
const float c0 = std::cos(yaw_prev), s0 = std::sin(yaw_prev);
const float dx_b = c0 * dx_w + s0 * dy_w;
const float dy_b = -s0 * dx_w + c0 * dy_w;
const float c = std::cos(-dth), s = std::sin(-dth);
for (auto &spt : lidarMSG)
{
float x = spt.x - dx_b;
float y = spt.y - dy_b;
float xr = c * x - s * y;
float yr = s * x + c * y;
spt.x = xr;
spt.y = yr;
spt.angle = wrapPi(std::atan2(yr, xr));
spt.mag = std::hypot(xr, yr);
}
lastPosX.store(posX_.load());
lastPosY.store(posY_.load());
lastYaw.store(yaw.load());
new_otos_data.store(false);
}
float rightDis = 0;
float leftDis = 0;
float frontDis = 0;
int sum_left = 0;
int sum_right = 0;
int sum_front = 0;
float totalDis = 0;
float setpoint = 0;
const float phi = (90.0f - absolute_angle.load()) * static_cast<float>(M_PI) / 180.0f;
float sumX = 0, sumY = 0;
for (const auto &s : lidarMSG)
{
const float ang = s.angle;
const float ang_eff = ang + phi; // MISMA rotación para todos
sumX += s.x;
sumY += s.y;
if (ang_eff >= 0.0f && ang_eff < 0.78f)
{ // izquierda ~ 0..45°
leftDis += s.mag * std::cos(ang_eff - 0.0f);
++sum_left;
}
if (ang_eff > 2.35f && ang_eff <= static_cast<float>(M_PI))
{ // derecha ~ 135..180°
rightDis += s.mag * std::cos(ang_eff - static_cast<float>(M_PI));
++sum_right;
}
if (ang_eff >= 1.39f && ang_eff <= 1.74f)
{ // frente ~ 80..100°
frontDis += s.mag * std::cos(ang_eff - M_PI_2);
++sum_front;
}
}
absolute_angle.store(rad2deg(std::atan2(sumY, sumX)));
leftDis /= sum_left;
rightDis /= sum_right;
frontDis /= sum_front;
totalDis = std::fabs(leftDis) + std::fabs(rightDis);
anchoCorredor.store(totalDis);
setpoint = totalDis / 2.0f;
frontWallDistance.store(frontDis);
centeringOffset.store(setpoint - std::fabs(rightDis));
}
Talking about optimal values, we have a simple track map function to be able to reach max speed in every type of turn, becasue the track isnt symetric, it can struggle with the same valiue if the wall is closer, fo example at 60 cm instead of the standar 100 cm, this function also let us now in what turn it is and know when to stop and to get the drive direction too.
void getActualSector()
{
float orientation = heading360.load();
int thisSector = actualSector.load();
int thisSectorUpperLimit = sectoresAngs[0][thisSector];
int thisSectorLowerLimit = sectoresAngs[1][thisSector];
if (thisSector == 0)
{
orientation >= 180 ? orientation -= 360 : orientation = orientation;
if (orientation < -75)
{
actualSector.store(3);
inTurn.store(false);
if (driveDirection.load() == 0)
{
driveDirection.store(2);
}
}
else if (orientation > 75)
{
actualSector.store(1);
inTurn.store(false);
if (driveDirection.load() == 0)
{
driveDirection.store(1);
}
}
}
else if (static_cast<int>(orientation) > thisSectorUpperLimit + 30)
{
thisSector++;
inTurn.store(false);
thisSector > 3 ? thisSector = 0 : thisSector = thisSector;
actualSector.store(thisSector);
}
else if (static_cast<int>(orientation) < thisSectorLowerLimit - 30)
{
thisSector--;
inTurn.store(false);
thisSector < 0 ? thisSector = 3 : thisSector = thisSector;
actualSector.store(thisSector);
}
}
The obstacle avoidance system (teensy_obs_node.cpp) implements a multi-sensor approach for autonomous navigation in environments with colored obstacles. This system integrates LIDAR, odometry, and computer vision to provide intelligent obstacle detection and avoidance capabilities.
The obstacle avoidance node operates as a state machine with three primary operational modes:
- Normal Navigation: Wall-following behavior with sector-based movement
- Obstacle Avoidance: Color-based object detection and avoidance maneuvers
- Turn Execution: Multi-step turning algorithms for corner navigation
// Vision system subscribers
object_distance_sub_ = create_subscription<std_msgs::msg::Float32>("/object/distance", 10, ...);
object_angle_sub_ = create_subscription<std_msgs::msg::Float32>("/object/angle", 10, ...);
object_color_sub_ = create_subscription<std_msgs::msg::Float32>("/object/color", 10, ...);
object_status_sub_ = create_subscription<std_msgs::msg::Float32>("/object/status", 10, ...);The system subscribes to vision node outputs providing:
- Distance: Object distance in centimeters
- Angle: Relative angle to object in degrees
- Color: Object classification (0 = green, 1 = red)
- Status: Detection status (0 = no object, 1 = object detected)
The node processes LIDAR data to extract directional distance measurements:
// LIDAR sector analysis
if(a < -1.3962f && a > -1.7453f) { sumBack += r; ++totalBack; } // Back sector
if (a > 1.39 && a < 1.7453f) { sumFront += r; ++totalFront; } // Front sector
if( a > 0.0f && a < 0.5235f) { sumLeft += r; ++totalLeft; } // Left sector
if( a > 2.79252f && a < 3.141592f) { sumRight += r; ++totalRight; } // Right sectorGreen Obstacle Avoidance:
if(color == 0){ // Green obstacle
angle = angle - 20; // Bias left by 20 degrees
if(angle < 0){
float returnANG = 90 + angle;
// Steer away from green obstacle
mover(returnANG, 50, 0);
}
}Red Obstacle Avoidance:
else if(color == 1){ // Red obstacle
angle = angle + 20; // Bias right by 20 degrees
if(angle > 0){
float returnANG = 90 + angle;
// Steer away from red obstacle
mover(returnANG, 50, 0);
}
}The main control loop implements a hierarchical decision structure:
void on_timer() {
getActualSector(); // Update sector tracking
int isObs = object_status_.load();
if(inturn.load()){
rutinaGirar(); // Execute turn routine
return;
}
else if(isObs == 1){
// Object detected - execute avoidance
executeObstacleAvoidance();
}
else{
// Normal navigation
orientar(); // Maintain heading
checkTurnConditions();
}
}The system implements three distinct turning algorithms based on available space:
if(outWallDistance >= 0.8f){
turntype_.store(3); // Wide turn - standard maneuver
}
else if(outWallDistance < 0.8f && outWallDistance >= 0.3f){
turntype_.store(2); // Medium turn - complex multi-step
}
else if(outWallDistance < 0.3f){
turntype_.store(1); // Close turn - backup required
}Type 1 - Close Turn (Backup Required):
- Step 0: Align to target heading
- Step 1: Reverse until sufficient clearance (
distBack < 0.5m)
Type 2 - Medium Turn (Complex Maneuver):
- Step 0: Approach corner with 45° offset
- Step 1: Reverse turn with heading correction
- Step 2: Complete turn in reverse
Type 3 - Wide Turn (Standard):
- Step 0: Forward approach until close to wall
- Step 1: Execute directional turn (150° left / 30° right)
- Step 2: Complete turn in reverse
- Control Frequency: 100 Hz (10ms cycle time)
- Serial Communication: 2 Mbps UART with checksum validation
- Thread Safety: Atomic variables for all shared state
- LIDAR Processing: Real-time sector analysis
- Vision Integration: Object detection at 30 FPS
- Odometry Fusion: 100 Hz pose updates
// Fixed speed for obstacle round
const int OBSTACLE_SPEED = 50; // PWM value
// Distance thresholds
const float TURN_THRESHOLD = 1.0f; // Front distance to initiate turn
const float ALIGNMENT_TOLERANCE = 5.0f; // Heading error tolerance (degrees)
const float CLOSE_DISTANCE = 0.3f; // Close turn threshold
const float WIDE_DISTANCE = 0.8f; // Wide turn threshold- Continuous front distance monitoring
- Emergency stop capability
- Multi-sensor validation before maneuvers
- Finite state machine prevents invalid transitions
- Atomic operations ensure thread-safe state updates
- Timeout mechanisms for stuck conditions
- Automatic retry for failed turn sequences
- Fallback to normal navigation if vision system fails
- Robust serial communication with error detection
The system combines vision and LIDAR data for enhanced obstacle detection:
- Vision System: Provides precise object classification and angular position
- LIDAR System: Validates distances and provides environmental context
- Fusion Logic: Cross-validates detections and selects optimal avoidance strategy
This multi-modal approach ensures robust performance in complex environments with varying lighting conditions and obstacle configurations.
The implementation aligns with the WRO Future Engineers challenge requirements by enabling autonomous navigation, obstacle detection, and avoidance in a dynamic environment. The system's modular design allows for easy adaptation to different track layouts and obstacle placements, ensuring compliance with competition rules.
flowchart TD
START[Timer Callback<br/>10ms period]
MOTION_COMP[Motion Compensation<br/>• Get pose delta from OTOS<br/>• Transform LIDAR points<br/>• Update robot frame]
SECTOR_DETECT[Sector Detection<br/>• Determine current sector 0-3<br/>• Detect drive direction<br/>• Track sector transitions]
CORRIDOR_ANALYSIS[Corridor Analysis<br/>• Calculate front distance<br/>• Calculate left/right distances<br/>• Compute centering offset]
FIRST_LAP{First Lap?}
SECTOR_MAPPING["Sector Width Mapping<br/>• Record corridor width<br/>• Store in sectors[4] array<br/>• Complete first lap when all filled"]
OPTIMAL_PARAMS[Calculate Optimal Parameters<br/>• Speed based on current/next sector<br/>• Kp tuning for corridor width<br/>• Turn parameters optimization]
FRONT_CHECK{Front Distance < 1.5m?}
SPEED_CTRL_TURN[Turn Speed Control<br/>• Target: optimalSpeedTurn<br/>• Angle correction with optimalKpTurn<br/>• Reduce speed for tight turns]
SPEED_CTRL_STRAIGHT[Straight Speed Control<br/>• Target: optimalSpeed<br/>• Angle correction with optimalKp<br/>• Velocity reduction for angle error]
LAP_CHECK{Heading > 1076°?}
END_ROUND[End Round Mode<br/>• Set endRound = true<br/>• Stop autonomous control<br/>• Manual positioning mode]
SERIAL_OUT[Serial Output<br/>• Pack angle + PWM + direction<br/>• Add checksum<br/>• Send to Teensy via UART]
START --> MOTION_COMP
MOTION_COMP --> SECTOR_DETECT
SECTOR_DETECT --> CORRIDOR_ANALYSIS
CORRIDOR_ANALYSIS --> FIRST_LAP
FIRST_LAP -->|Yes| SECTOR_MAPPING
FIRST_LAP -->|No| OPTIMAL_PARAMS
SECTOR_MAPPING --> FRONT_CHECK
OPTIMAL_PARAMS --> FRONT_CHECK
FRONT_CHECK -->|Yes| SPEED_CTRL_TURN
FRONT_CHECK -->|No| SPEED_CTRL_STRAIGHT
SPEED_CTRL_TURN --> LAP_CHECK
SPEED_CTRL_STRAIGHT --> LAP_CHECK
LAP_CHECK -->|Yes| END_ROUND
LAP_CHECK -->|No| SERIAL_OUT
END_ROUND --> SERIAL_OUT
SERIAL_OUT --> START
Motion Compensation (getOffsetsFromLidar):
- Extracts yaw delta from OTOS quaternion
- Transforms LIDAR points using rotation matrices:
x_robot = cos(yaw_prev) * dx_world + sin(yaw_prev) * dy_worldy_robot = -sin(yaw_prev) * dx_world + cos(yaw_prev) * dy_world
- Updates absolute angle as centroid of transformed points:
atan2(sumY, sumX)
Sector Detection (getActualSector):
Hysteresis: ±30° bounds before sector transition
Sector 0 (0°): if heading < -75° or > 75°
Sector 1 (90°): if heading 15° to 165°
Sector 2 (180°): if heading 105° to 255°
Sector 3 (270°): if heading 195° to 345°
driveDirection: 1=clockwise, 2=counter-clockwiseCorridor Analysis (getOffsetsFromLidar segment):
Front sector: 80-100° → frontDis (meters)
Left sector: 0-45° → leftDis (meters)
Right sector: 135-180° → rightDis (meters)
centeringOffset = (rightDis - leftDis) / 2.0
anchoCorredor = leftDis + rightDis (total width)Parameter Optimization (getOptimalValues):
| Current Width | Next Width | optimalSpeed | optimalKp | optimalSpeedTurn | optimalKpTurn |
|---|---|---|---|---|---|
| ≥0.80m | ≥0.80m | 5.0 m/s | 0.20 | 4.0 m/s | 0.30 |
| ≥0.80m | <0.80m | 1.0 m/s | 0.50 | 1.8 m/s | 0.60 |
| <0.80m | ≥0.80m | 1.7 m/s | 0.50 | 1.7 m/s | 0.75 |
| <0.80m | <0.80m | 3.0 m/s | 0.50 | 1.5 m/s | 0.60 |
Speed Control (controlACDA):
// PD-based speed adjustment
error = targetSpeed - actualSpeed
aproxPwm = [35 PWM if <0.6, 40 if <1.2, 60 if >1.2]
pwm = (error * 8.25) + ((error - lastError) / 0.01) * 0.1
pwm = clamp(aproxPwm + pwm, lastPwm ± 10, 0-255)Angle Processing (angleProccesing):
angularError = 90° - absolute_angle
steeringAngle = 30 * tanh(angularError / 40) // nonlinear response
// Maintains servo smoothness, prevents jerky oscillationsSpeed Reduction by Angle (objectiveAngleVelPD):
// Reduces speed proportional to centering error
e = 90° - absolute_angle // wrapped to [-180, 180]
derivada = EMA(de/dt, alpha=0.3) // filtered derivative
reduction = 0.04 * |e| + 0.005 * |derivada|
return clamp(reduction, vel_min=0.0, vel_max=0.5)Serial Command Format (empaquetar):
[0xAB] [angle_high] [angle_low] [pwm] [direction] [xor_checksum]
angle = uint16 in tenths of degrees (0-3600 = 0-360°)
pwm = uint8 (0-255)
direction = 0 (forward), 1 (reverse)
checksum = XOR of all preceding bytes
flowchart TD
START[Timer Callback<br/>10ms period]
PARKING[Parking Lot Exit<br/>• Phase-based movement<br/>• Direction detection<br/>• Position tracking]
SECTOR_UPDATE[Update Current Sector<br/>• Track heading changes<br/>• Detect sector transitions<br/>• Enable turn when sector changes]
IN_TURN{Currently in Turn?}
TURN_ROUTINE[Execute Turn Routine<br/>• Type 1: Close turn back up<br/>• Type 2: Medium turn complex<br/>• Type 3: Wide turn standard<br/>• Multi-step state machine]
OBJ_STATUS{Object Detected?}
OBJ_COLOR{Object Color?}
GREEN_AVOID[Green Object Avoidance<br/>• Left side: ALWAYS evade<br/>• Right side: Safe cone only<br/>• Max deviation: -20°]
RED_AVOID[Red Object Avoidance<br/>• Right side: ALWAYS evade<br/>• Left side: Safe cone only<br/>• Max deviation: +20°]
NORMAL_NAV[Normal Navigation<br/>• Orient to target heading<br/>• PD control for alignment<br/>• Check for turn conditions]
TURN_CHECK{Front < 1.0m AND<br/>Aligned to target?}
INITIATE_TURN[Initiate Turn<br/>• Set target heading ±90°<br/>• Determine turn type<br/>• Enter turn state machine]
SEND_COMMANDS[Send Motor Commands<br/>• Calculate steering angle<br/>• Set speed PWM 40<br/>• Serial transmission]
START --> PARKING
PARKING --> SECTOR_UPDATE
SECTOR_UPDATE --> IN_TURN
IN_TURN -->|Yes| TURN_ROUTINE
IN_TURN -->|No| OBJ_STATUS
OBJ_STATUS -->|Detected| OBJ_COLOR
OBJ_STATUS -->|None| NORMAL_NAV
OBJ_COLOR -->|Green 0| GREEN_AVOID
OBJ_COLOR -->|Red 1| RED_AVOID
OBJ_COLOR -->|Unknown| NORMAL_NAV
GREEN_AVOID --> SEND_COMMANDS
RED_AVOID --> SEND_COMMANDS
NORMAL_NAV --> TURN_CHECK
TURN_CHECK -->|Yes| INITIATE_TURN
TURN_CHECK -->|No| SEND_COMMANDS
TURN_ROUTINE --> SEND_COMMANDS
INITIATE_TURN --> SEND_COMMANDS
SEND_COMMANDS --> START
Parking Lot Exit (outOfParkingLot):
Phase 1: Detect direction
- left_wall < right_wall → direction = 1 (left)
- right_wall < left_wall → direction = 2 (right)
Phase 2: Forward drive until Y < 0.03m
Phase 3: Set servo angle
- direction 1 → servo = 150° (left)
- direction 2 → servo = 50° (right)
- wait 1000ms for mechanical settling
Phase 4: Reverse movement
- direction 1 → reverse until Y < -0.04m
- direction 2 → reverse until Y < -0.05m
Phase 5: Forward again until Y > 0.15m
Phase 6: Center servo (90°), exit parking phaseLIDAR Segmentation (on_scan):
Front sector: 80-100° (1.39-1.745 rad)
Left sector: 0-30° (0-0.524 rad)
Right sector: 160-180° (2.79-3.14 rad)
Back sector: -135° to -100° (-2.356 to -1.745 rad)
Each region averaged: dist_front_, dist_Left_, dist_Right_, dist_back_
absolute_angle = atan2(sumY, sumX) * 180/πTurn Type Selection (determineTurnType):
outWallDistance = distance to wall on turning side
if (outWallDistance < 0.30m):
TYPE = 1 // Close turn - requires backup
else if (0.30m ≤ outWallDistance < 0.70m):
TYPE = 2 // Medium turn - complex maneuver
else (outWallDistance >= 0.70m):
TYPE = 3 // Wide turn - standard rotationTurn Execution (rutinaGirar):
Type 1 (Close Turn):
Step 0: Orient toward target heading (Kp = 1.0)
servo = 90 + (targetYaw - currentYaw) * 1.0
pwm = 40 (forward)
Step 1: Reverse until back_wall < 0.5m
servo = 90 + (targetYaw - currentYaw)
pwm = 0 (reverse)
Exits when: back_wall distance reached AND heading alignedType 2 (Medium Turn):
Step 0: Forward at 90° until front < 0.7m
servo = 90, pwm = 40
Step 1: Angle-correct toward (targetYaw - 45°)
servo = 90 + correction, pwm = 40
Step 2: Reverse diagonal approach
servo = (targetYaw - 45°), pwm = 0
Step 3: Final centering in reverse
servo = targetYaw, pwm = 0
Completes when: position settled + heading alignedType 3 (Wide Turn):
Step 0: Slow forward at 90° until front < 0.3m
servo = 90, pwm = 30 (slow)
Step 1: Sharp rotate to turn angle
servo = 150° (left) or 50° (right), pwm = 40
wait 2s for rotation completion
Step 2: Reverse to center line
servo = targetYaw, pwm = 0
Exits when: centered and aligned to target headingObstacle Avoidance Logic:
GREEN Object (priority left):
distance_range = [30cm, 100cm]
prop = (maxDis - distance) / range // proximity weight
if (object_angle < -30°): // Left side
ALWAYS evade: offset = -OffSetmax * prop
else if (-30° ≤ object_angle ≤ 30°): // Centered
Safe cone: w_phi = cos²(angle / 30°)
offset = -OffSetmax * w_phi * prop
else: // Right side (object_angle > 30°)
Only evade if in safe cone
servo_target = 90 + offset
Smoothing: max 3°/tick changeRED Object (priority right):
// Inverse logic of green
if (object_angle > 30°): // Right side
ALWAYS evade: offset = +OffSetmax * prop
else if (-30° ≤ object_angle ≤ 30°):
Safe cone: w_phi = cos²(angle / 30°)
offset = +OffSetmax * w_phi * prop
else: // Left side
Only evade if in safe cone
servo_target = 90 + offset
Smoothing: max 3°/tick changeSector Navigation (orientar):
Target heading by sector:
Sector 0 → targetYaw = 0°
Sector 1 → targetYaw = 90°
Sector 2 → targetYaw = 180°
Sector 3 → targetYaw = 270°
Heading control:
error = wrap_to_180(targetYaw - currentYaw)
servo = 90 + (error * 1.0) // Simple proportional
servo = clamp(servo, 40°, 160°)| Metric | Open Round | Obstacle Round |
|---|---|---|
| Control Loop Rate | 100 Hz (10ms) | 100 Hz (10ms) |
| Typical Speed | 60-80 PWM | 40 PWM (fixed) |
| Speed Range | 1-5 m/s | 0-3 m/s + parking |
| Turn Time | ~2-3 seconds | ~3-5 seconds |
| Corridor Following Accuracy | ±5cm centering | ±10cm (avoidance) |
| Maximum Steering Angle | ±30° | ±20° (avoidance) |
| Decision Points Per Lap | ~8-10 | ~12-15 (variable) |
| First Lap Overhead | ~30 seconds | None (exits parking) |
Using OpenCV to detect green, red, and purple objects in the camera feed. The algorithm filters colors in HSV space, finds contours, and calculates distance and angle based on object size and position.
# HSV filtering for green
mask_green = cv2.inRange(hsv, lower_green, upper_green)
# HSV filtering for red (two ranges)
mask_r1 = cv2.inRange(hsv, lower_red1, upper_red1)
mask_r2 = cv2.inRange(hsv, lower_red2, upper_red2)
mask_red = cv2.bitwise_or(mask_r1, mask_r2)
# HSV filtering for purple
mask_purple = cv2.inRange(hsv, lower_purple, upper_purple)
# Noise filtering
mask_green = cv2.morphologyEx(mask_green, cv2.MORPH_OPEN, kernel)
mask_red = cv2.morphologyEx(mask_red, cv2.MORPH_OPEN, kernel)
mask_purple = cv2.morphologyEx(mask_purple, cv2.MORPH_OPEN, kernel)
# Distance calculation
distance = (KNOWN_WIDTH * FOCAL_LENGTH) / bounding_box_widthUsing the sum of every point from the lidar we get a vector that tell us where to go, this for it self isnt optimal cause it tell us where is more space, so we add a vector pointing to the IMU target, this help us to get more smooth and optimal trajectories more than just drive where you can.
float angleProccesing(float kpNoLinear = 0.75f, float maxOut = 30.0f, bool yawMode = false, float yawKp = 0.025f)
{
float yawHelpError = wrap_deg180(heading360.load() - sectoresAbsAng[actualSector.load()]) * yawKp;
if (inTurn.load())
{
if (driveDirection.load() == 1)
{ // horario
yawHelpError = wrap_deg180(heading360.load() - wrap_360(sectoresAbsAng[actualSector.load()] - 90.0f)) * 0.5;
}
else if (driveDirection.load() == 2)
{ // antihorario
yawHelpError = wrap_deg180(heading360.load() - wrap_360(sectoresAbsAng[actualSector.load()] + 90.0f)) * 0.5;
}
}
float angleInput = absolute_angle.load();
yawMode ? angleInput = angleInput + yawHelpError : angleInput = angleInput;
float angularError = 90.0f - angleInput;
float beta = kpNoLinear / maxOut;
return maxOut * std::tanh(angularError / (maxOut / kpNoLinear));
}
With this info now is time to make the robot move to that direction, its not enough with send a simple pwm, because speed have to changue in turns and in corridors, and also is not the same if is in a wide corridor and the next also is, or if its in a wide corridor and the next is narrow, so to go to the max speed we can we use a ACDC controller, this make the robot have the ability to go not only at a constant speed measured in m/s, this make the robot able to activly brake if is needed.
int controlACDA(float targetSpeed)
{
float pwm = 0, jerk = 10;
float error = targetSpeed - speed.load();
float aproxPwm = 35.0f;
if (targetSpeed < 0.6f)
{
aproxPwm = 35.0f;
}
else if (targetSpeed < 1.2f)
{
aproxPwm = 40.0f;
}
else
{
aproxPwm = 60.0f;
}
float lastPwmLocal = lastPwm.load();
float kp = 8.25f; // Valor a determinar
float kd = 0.1f; // Valor a determinar
pwm = (error * kp) + ((error - lastError.load()) / 0.01) * kd;
pwm = clampf(clampf(pwm + aproxPwm, lastPwmLocal - jerk, lastPwmLocal + jerk), 0, 255);
lastPwm.store(pwm);
lastError.store(error);
if (error < -0.5f || targetSpeed == 0)
return 0;
if (error < -0.1f)
return 1;
return static_cast<int>(pwm);
}
This helps a lot but is not the only controller we have, cause the speed isnt the same if is correctly aligned or if is near to collide with the walls.
float objectiveAngleVelPD(float vel_min, float vel_max)
{
const float alpha = 0.3f; // suavizado EMA
const float dt = 0.01f; // 10 ms (timer)
float a = absolute_angle.load();
if (!std::isfinite(a))
return vel_min; // sin reducción cuando no hay ángulo
// Error envuelto a [-180, 180]
float e = 90.0f - a;
while (e > 180.0f)
e -= 360.0f;
while (e < -180.0f)
e += 360.0f;
// Derivada cruda con el error previo
float e_prev = lastVelErr.load();
float raw_derivada = (e - e_prev) / dt; // deg/s "amplificado"
lastVelErr.store(e);
// EMA correcto: y(k) = y(k-1) + alpha * (x(k) - y(k-1))
float der_prev = de_f.load();
float derivada = der_prev + alpha * (raw_derivada - der_prev);
de_f.store(derivada);
const float kp = 0.04f; // m/s por grado
const float kd = 0.005f; // m/s por (grado/seg filtrado)
float reduccion = kp * std::fabs(e); /*+ kd * std::fabs(derivada);*/
return clampf(reduccion, vel_min, vel_max); // p.ej. [0.0f, 0.8f]
}For navigation we first map the track, the first lap is made at a lowe speed and we save dara, after this we calculate optimal speed and PD values in every section of the track.
void getOptimalValues()
{
float actualSize = getCurrentSectorSize();
float nextSize = getNextSectorSize();
if (actualSize >= 0.80f)
{
if (nextSize >= 0.80f)
{
optimalSpeed.store(5.0f);
optimalKp.store(0.20f);
optimalSpeedTurn.store(4.0f);
optimalKpTurn.store(0.30f);
yawMult.store(0.125f);
turnYawMult.store(0.1f);
turnDis.store(1.2f);
}
else if (nextSize < 0.80f)
{
optimalSpeed.store(1.0f);
optimalKp.store(0.50f);
optimalSpeedTurn.store(1.8f);
optimalKpTurn.store(0.60f);
yawMult.store(0.125f);
turnYawMult.store(0.125f);
turnDis.store(0.7f);
}
}
else if (actualSize < 0.80f)
{
if (nextSize >= 0.80f)
{
optimalSpeed.store(1.7f);
optimalKp.store(0.50f);
optimalSpeedTurn.store(1.7f);
optimalKpTurn.store(0.75f);
yawMult.store(0.125f);
turnYawMult.store(0.05f);
turnDis.store(1.0f);
}
else if (nextSize < 0.80f)
{
optimalSpeed.store(3.0f);
optimalKp.store(0.50f);
optimalSpeedTurn.store(1.5f);
optimalKpTurn.store(0.60f);
yawMult.store(0.125f);
turnYawMult.store(0.125f);
turnDis.store(0.8f);
}
}
}- OTOS: Units in meters and degrees
- LiDAR: RPLiDAR C1
- Camera: 1280x720, RGB888 format
- Focal Length: 1131 pixels
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ START │───>│ NAVIGATION │───> │ DETECTION │
│ │ │ │ │ │
│ • Calibrate │ │ • Simple │ │ • Identify │
│ • Reset │ │ SLAM │ │ objects │
│ • Wait │ │ • Avoid │ │ • Calculate │
└─────────────┘ │ obstacles │ │ position │
└─────────────┘ └─────────────┘
^ |
└────────────────────┘
| Topic | Type | Description |
|---|---|---|
/scan |
LaserScan | LiDAR data |
/odom |
Odometry | OTOS odometry |
/camera/image_raw |
Image | Camera image |
/cmd_vel |
Twist | Velocity commands |
/objects/detection |
Float32MultiArray | Detected objects data |
/objects/status |
Float32 | Number of detected objects |
Our system leverages several well-maintained open-source ROS2 packages to provide robust sensor integration and perception capabilities:
-
Repository: rplidar_ros
-
License: BSD
-
Purpose: Real-time LiDAR data acquisition and ROS2 integration
-
Version: Compatible with ROS2 Humble and later
-
Key Features:
- Publishes
sensor_msgs/LaserScanmessages at configurable rates (up to 10 Hz) - Angle range: 0° to 360° with 0.9° resolution
- Supports quality filtering to remove unreliable measurements
- Built-in motor speed control via PWM
- Configurable output frame ID for multi-robot scenarios
- Publishes
-
Configuration:
rplidar_node: ros__parameters: serial_port: "/dev/ttyUSB0" serial_baudrate: 115200 frame_id: "lidar_link" angle_compensate: true scan_mode: "Sensitivity"
-
Published Topics:
/scan(sensor_msgs/LaserScan): Raw laser scan data
-
Subscribed Topics:
- None (hardware-only input)
-
Framework: libcamera (Official Raspberry Pi camera driver)
-
Purpose: High-performance camera capture for vision processing
-
Version: Python bindings for libcamera
-
Key Features:
- 12MP IMX708 sensor with High Dynamic Range (HDR) support
- Zero-copy buffer management for minimal latency
- Configurable ISP (Image Signal Processor) tuning
- Native support for various formats: RGB888, YUV420, JPEG
- Automatic exposure and white balance control
- Frame rates up to 90 FPS (VGA resolution)
-
Configuration:
from picamera2 import Picamera2 picam2 = Picamera2() config = picam2.create_preview_configuration( main={"format": 'RGB888', "size": (1280, 720)}, camera_properties={"NoiseReductionMode": libcamera.controls.draft.NoiseReductionModeEnum.HighQuality} ) picam2.configure(config)
-
Custom ROS2 Node:
- File:
src/vision_node/vision_node/color_detection_node.py - Publishes
/camera/image_raw(sensor_msgs/Image) - Subscribes to control topics for dynamic parameter adjustment
- Implements real-time HSV color filtering for obstacle detection
- File:
-
Repository: tf2
-
License: BSD
-
Purpose: Coordinate frame management and transforms
-
Key Features:
- Manages spatial relationships between robot components
- Static transforms: camera → lidar → base_link
- Dynamic transforms: odom → base_link (updated from OTOS)
- Integration with visualization tools (RViz2)
-
Static Transforms (in launch file):
static_transform_broadcaster = StaticTransformBroadcasterNode( arguments=[ "lidar_link", "base_link", "0.0", "0.0", "0.15", "0", "0", "0", "camera_link", "lidar_link", "0.0", "0.02", "0.04", "-1.57", "0", "0" ] )
-
Purpose: High-precision odometry from SparkFun OTOS sensor
-
File Location:
src/otos_reader/ -
License: MIT (Custom implementation)
-
Communication: UART over I2C to Qwiic connector
-
Key Features:
- 100 Hz update rate for smooth odometry
- Publishes
nav_msgs/Odometrymessages - Implements ZUPT (Zero Velocity Update) detection for stationary calibration
- EMA (Exponential Moving Average) filtering for noise reduction
- Automatic bias correction for long-term accuracy
-
Published Topics:
/odom(nav_msgs/Odometry): Position, velocity, and orientation/tf(tf2_msgs/TFMessage): Odometry frame transform
-
Parameters:
self.declare_parameter('port', '/dev/ttyAMA0') # I2C/Serial port self.declare_parameter('calibration_distance', 0.5) # meters self.declare_parameter('ema_alpha', 0.2) # Smoothing factor
- Repository: common_interfaces
- License: Apache 2.0
- Purpose: Standard message definitions for interoperability
- Used Message Types:
sensor_msgs/LaserScan: LiDAR point cloud (polar format)sensor_msgs/Image: Camera framesnav_msgs/Odometry: Pose and velocity informationgeometry_msgs/Twist: Velocity commands (linear/angular)std_msgs/Float32: Scalar values (distances, angles)
- Repository: lifecycle
- License: Apache 2.0
- Purpose: Node lifecycle management
- Implemented States:
unconfigured: Node initialized but not readyinactive: Configured but not runningactive: Processing sensor datafinalized: Cleanup on shutdown
To set up all ROS2 dependencies:
# Install system packages
sudo apt-get install -y \
ros-humble-sensor-msgs \
ros-humble-geometry-msgs \
ros-humble-nav-msgs \
ros-humble-std-msgs \
ros-humble-tf2 \
ros-humble-tf2-geometry-msgs
# Clone and build custom packages
cd ~/ros2_ws/src
git clone https://github.com/Slamtec/rplidar_ros.git -b ros2
git clone https://github.com/chaBotsMX/chaBots-NERV-WRO-Future-Engineers-2025.git
cd ~/ros2_ws
rosdep install --from-paths src --ignore-src -r -y
colcon build --symlink-install- RPLiDAR ROS2: Actively maintained by Slamtec. Issues and PRs welcome.
- geometry2/tf2: Core ROS2 package, part of official distribution
- Standard Messages: Part of ROS2 core infrastructure
- OTOS Custom Package: Maintained within our repository with regular calibration updates
# Launch complete robot
ros2 launch teensy_communication robot.launch.py
# Object detection only
ros2 run vision_node color_detection_node
# OTOS odometry only
ros2 run otos_reader otos_nodesrc/
├── vision_node/ # Color object detection
│ └── color_detection_node.py
├── otos_reader/ # OTOS odometry
│ └── otos_node.py
└── teensy_communication/ # General coordination
└── launch/
└── robot.launch.py
└── src/
├── teensy_comm_node.cpp
├── teensy_obs_node.cpp
- Foxglove Studio: Real-time visualization
- RViz: Trajectories and laser maps
- OpenCV Windows: Camera view with detections
- ROS2 Logs: Debug information via console
The robot's obstacle management system (teensy_obs_node) implements a sophisticated multi-sensor approach for autonomous navigation in environments with colored obstacles. This system integrates LIDAR, odometry, and computer vision to provide intelligent obstacle detection and avoidance capabilities operating at 100 Hz (10ms cycle).
- Primary: Enhanced color detection via PiCamera2 system (30 FPS)
- Verification: LIDAR distance measurements for obstacle confirmation and multi-directional navigation
- Backup: OTOS position tracking for navigation consistency and heading maintenance
The system processes object detection outputs containing:
- Distance: Object distance in centimeters (color-based detection)
- Angle: Relative angle to object in degrees (-180° to 180°)
- Color Classification: 0 = GREEN, 1 = RED
- Status: Detection flag (0 = no object, 1 = detected)
The LIDAR processes real-time distance measurements across four directional sectors:
Front Sector: 1.39 to 1.74 radians (79-100°)
Back Sector: -1.74 to -1.39 radians (-100 to -79°)
Left Sector: 0.0 to 0.52 radians (0-30°)
Right Sector: 2.79 to 3.14 radians (160-180°)
The system operates as a state machine with three primary modes:
- Sector-based movement: Robot maintains orientation within four 90° sectors
- Heading correction: PID-based orientation control to maintain target yaw
- Turn initiation: Triggers when front distance < 1.0m AND heading error < 7°
- Speed: Constant PWM of 40 for controlled movement
The system implements color-specific evasion strategies:
GREEN OBSTACLE - Pass Always by Left Side:
Decision Logic:
├─ If angle < 0° (object on left):
│ └─ Always evade LEFT (maximum offset: -20°)
│
└─ If angle ≥ 0° (object on right):
├─ If within "safe cone" (30° threshold):
│ └─ Evade LEFT (reduced by angular weighting)
│
└─ If outside safe cone:
└─ No evasion required (maintain heading)
Distance Weighting:
├─ Min Distance: 30cm (full avoidance)
├─ Max Distance: 100cm (no avoidance)
└─ Proportional weight: (maxDis - distance) / (maxDis - minDis)
RED OBSTACLE - Pass Always by Right Side:
Decision Logic (inverse of green):
├─ If angle ≥ 0° (object on right):
│ └─ Always evade RIGHT (maximum offset: +20°)
│
└─ If angle < 0° (object on left):
├─ If within "safe cone" (30° threshold):
│ └─ Evade RIGHT (reduced by angular weighting)
│
└─ If outside safe cone:
└─ No evasion required (maintain heading)
Evasion Control:
- Servo command smoothing: ±3°/tick (anti-jerk limiting)
- Angular correction: Proportional to heading error
- Speed: Constant PWM of 40
- Steering range: 60° to 150° (servo centered at 90°)
The system dynamically selects turning strategy based on available space:
Turn Type Classification:
Lateral Distance Measurement:
├─ ≥ 0.70m → Type 3: Wide Turn (Standard 90° maneuver)
├─ 0.30-0.70m → Type 2: Medium Turn (Complex multi-step)
└─ < 0.30m → Type 1: Close Turn (Backup required)
Type 1 - Close Turn (Backup Maneuver):
- Step 0: Align to target heading (90° center, PWM 40)
- Condition:
|correction| < 20°
- Condition:
- Step 1: Reverse in place (PWM 40, reverse direction)
- Condition:
back_distance < 0.5m
- Condition:
Type 2 - Medium Turn (Complex Multi-Step):
- Step 0: Forward approach with 45° offset (PWM 30)
- Condition:
front_distance < 0.7m
- Condition:
- Step 1: Reverse turn with heading correction (PWM 40)
- Condition:
|correction| < 20°
- Condition:
- Step 2: Secondary reverse maneuver (PWM 40)
- Condition:
|correction| < 10°
- Condition:
- Step 3: Final reverse phase (PWM 40)
- Condition:
back_distance < 0.5m
- Condition:
Type 3 - Wide Turn (Standard Maneuver):
- Step 0: Forward approach to corner (PWM 40)
- Condition:
front_distance < 0.30m
- Condition:
- Step 1: Directional turn based on side
- Left turn: 150° (PWM 40, reverse)
- Right turn: 30° (PWM 40, reverse)
- Condition: Heading aligned to target (< 5° error)
- Step 2: Final reverse phase (PWM 40)
- Condition:
back_distance < 0.5m
- Condition:
The robot organizes space into four 90° sectors with defined movement targets:
Sector Layout:
Front (0°)
↑
Sector 0
315° 45°
Sector 3 Sector 1
225° 135°
Sector 2
↓
Back (180°)
Target Orientations:
├─ Sector 0: 0° (Forward)
├─ Sector 1: 90° (Right)
├─ Sector 2: 180° (Backward)
└─ Sector 3: 270° (Left)
Sector Transitions:
- Robot tracks current sector based on heading accumulator
- Automatic sector transitions at boundary angles (45°, 135°, 225°, 315°)
- Direction preference system (left/right) initialized on first turn
- Turn allowed flag resets upon sector entry
The system implements dynamic PWM adjustment based on error:
Speed Control Algorithm (controlACDA):
├─ Base PWM Selection:
│ ├─ Target < 0.6 m/s → 35 PWM
│ ├─ 0.6-1.2 m/s → 40 PWM
│ └─ > 1.2 m/s → 60 PWM
│
├─ PID Correction:
│ ├─ Kp = 8.25 (proportional gain)
│ ├─ Kd = 0.1 (derivative gain)
│ └─ Jerk limit = 10 PWM/cycle
│
└─ Speed Protection:
├─ Zero command: |error| ≤ -0.5 m/s
├─ Reduced command: -0.1 < error ≤ -0.5 m/s
└─ Full control: error > -0.1 m/s
- Continuous front distance monitoring (1.0m threshold)
- Immediate turn initiation upon obstacle detection
- Multi-sensor validation before maneuvers
- Finite state machine prevents invalid state transitions
- Atomic operations ensure thread-safe state updates across threads
- Timeout mechanisms prevent stuck conditions
- Automatic retry for failed turn sequences
- Fallback to normal navigation if vision system fails
- Robust UART communication with XOR checksum validation (2 Mbps baud)
Serial Frame Format (6 bytes):
[Header] [Error_High] [Error_Low] [PWM] [Direction] [Checksum]
0xAB uint8_t uint8_t uint8_t uint8_t uint8_t
- Error: 16-bit steering command (40-160 degrees)
- PWM: Motor speed (0-255)
- Direction: 0 = Forward, 1 = Reverse
- Checksum: XOR of all previous bytes
Deterministic Color-Based Strategy
- Predefined passing rules (green left, red right) ensure consistent navigation patterns
- Eliminates ambiguity in obstacle avoidance decisions
- Makes behavior predictable and reproducible for competition scoring
Adaptive Turn Mechanism
- Three-level turn selection (close, medium, wide) optimizes maneuver efficiency
- Reduces time wasted on inappropriate turning strategies
- Handles variable corridor widths without manual recalibration
Multi-Sensor Fusion
- Vision provides precise object classification and angular position
- LIDAR validates distances and supplies environmental context
- Cross-validation of detections reduces false positives
- Heading accumulator maintains orientation memory across sensor failures
Models file folder: models/
- Step 1: 3D designing
- Step 2: 3D printing
- Step 3: Electronic layout
- Step 4: Wiring
- Step 5: Mounting
- Step 6: Programming
- Step 7: Testing
- 3D Printer (Creality K2 Plus, QIDI Q1 Pro)
- Polymaker PTG CF filament
- Mini Electric Soldering Iron Kit TS101
- Dremel Tool
- Screwdriver Set Fanttik
| Item | Qty | Unit Cost (MXN) | Total (MXN) |
|---|---|---|---|
| Teensy 4.0 | 1 | $800 | 800 |
| Raspberry Pi 5 | 1 | $2,800 | $2,800 |
| RPlidar C1 | 1 | $2,500 | $2,500 |
| Raspberry Pi Camera 12mp V3 | 1 | $920 | $920 |
| Raspberry Pi 5 Camera Cable | 1 | $64 | $64 |
| 2.2Ah LiPo 11.1V Battery | 1 | $600 | $600 |
| 1Ah LiPo 3.3V Battery | 1 | $70 | $70 |
| Maxon Motor DCX19 | 1 | $8,500 | $8,500 |
| HS85MG Micro Servo | 1 | $2,000 | $2,000 |
| SparkFun OTOS | 1 | $2,400 | $2,400 |
| POLYMAKER PLA Filament (prototypes) | 1kg | $900 | $900 |
| POLYMAKER PLA-CF Filament (finals) | 0.5kg | $450 | $450 |
| Carbon Fiber | 1 | $2,000 | $2,000 |
| SMD Components & Misc. | - | $1,500 | $1,500 |
| PCB Manufacturing | 1 | $800 | $800 |
| Spike Wheels (LEGO) | 4 | $150 | $600 |
| EV3 Wheels (LEGO) | 2 | $10 | $20 |
| Total | $26,924 |
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software.
Document maintained by Chabots | Last updated: Sept 2025
Written with StackEdit.




































