The Statesmen 2025

This repository contains all of the documentation for team The Statesmen in the World Robot Olympiad 2025, Future Engineers category. This README.md file is the engineering notebook. ## Table of Contents * [Our Team](#our-team) * [Atharv Baluja](#atharv-baluja) * [Arham Jain](#arham-jain) * [Yashvardhan Surana](#yashvardhan-surana) * [Robot Photos](#robot-photos) * [Performance Videos](#robot-videos) * [Mobility Management](#mobility-management) * [Motor](#motor) * [Motor Driver](#motor-driver) * [Rear Wheel Drive](#rear-wheel-drive) * [Servo Motor](#servo-motor) * [Ackermann Steering System](#ackermann-steering-system) * [Robot Chassis](#chassis) * [Code Snippets](#code-snippets) * [Problems Faced and Solutions](#problems-we-faced-and-how-we-solved-them) * [Possible Improvements](#possible-improvements) * [Power and Sense Management](#power-and-sense-management) * [Li-Ion Battery](#li-ion-battery) * [Buck Convertor](#lm2596-buck-converter) * [LiPo Battery](#lipo-battery) * [ESP32](#esp32-devkit-sbm) * [Raspberry Pi 5](#raspberry-pi-5-sbc) * [Camera](#camera) * [TF Luna LiDAR Sensor](#tf-luna-lidar-sensor) * [BNO055 Orientation Sensor](#bno055-orentiation-sensor) * [Power System Connections](#connection-diagram-for-power-system) * [Sensor Connections](#connection-diagram-for-sensors) * [Code Snippets](#code-snippets-for-components) * [Problems Faced and Solutions](#problems-we-faced-and-how-we-solved-them-1) * [Possible Improvements](#potential-improvements) * [Obstacle Management](#obstacle-management) * [Open Challenge](#open-challenge) * [Obstacle Challenge](#obstacle-challenge) * [Problems Faced](#problems-we-faced-and-how-we-solved-them-2) * [Possible Improvements](#potential-improvements-1) * [Engineering Factor](#engineering-factor) * [Software Integration](#software-integration) * [System Architecture](#system-architecture) * [Communication Protocol](#communication-protocol) * [Code Modularity](#code-modularity) * [Communication API](#communication-api) * [Debugging](#debugging) * [Robot Construction Guide](#robot-construction-guide) ## Our Team ### Team Photo Team Photo ### Atharv Baluja Age: 14 School: Neerja Modi World School Description: Hey! I am Atharv. I'm a high school freshman from Jaipur, India. This is my second WRO seaon. Last year, I had participated in the Future Innovator category. This year, I wanted a bigger challenge so I am participating in the Future Engineers category. I am working on the software of the robot. Atharv Baluja ### Arham Jain Hello, I am Arham, an 11th-grade student at Neerja Modi School. I have competed in WRO once before in the Future Innovators category two years ago. I was preparing to compete in the Future Engineers category last year before some hurdles derailed my progress. Building on the knowledge and research from last year, I hope to put my effort into the Future Engineers Category again. Arham Jain ### Yashvardhan Surana Hello, I'm Yash. I am a 6th grader from Jaipur. This is my second time participating in WRO. Last year, I had participated in the Future Innovators category. This year, I'm taking part in the Future Engineers category in the team The Statesmen. In The Statesmen, I am in charge of the hardware of the robot. Yashvardhan Surana ## Robot Photos | | | | :--: | :--: | | *Front View* | *Back View* | | | | | *Top View* | *Bottom View* | | | | | *Left View* | *Right View* | ## Robot Videos ### Open Challenge - Default Setup If the video is not loading for you, you can view it [here](https://youtu.be/yJYID6W3fv8) ### Open Challenge - 1 Side Extended If the video is not loading for you, you can view it [here](https://youtu.be/VSAF0O4e32w) ### Open Challenge - 2 Sides Extended If the video is not loading for you, you can view it [here](https://youtu.be/7XdKwXTz3Lk) ### Obstacle Challenge - Setup 1 You can view this video [here](https://www.youtube.com/watch?v=GN4vLTXXRtY) You can watch all the videos on our [youtube channel](https://www.youtube.com/@TheStatesmen-fe) ## Mobility Management Our robot's mobility is managed by multiple components - a BO DC motor, L293D motor driver, an MG90 servo motor connected to a Ackermann steering geometry, and a rear differential. All of these seamlessly work together to achieve efficient movement. ### Motor Aftering considering multiple motors, we finally settleed on a DC BO motor. We considered factors like speed (RPM), torque (kg cm), weight (g), required voltage (v), and current consumption (mA).
DC BO Motor

Specifications:

  • Voltage: 12V
  • Gear Ratio: 1:50
  • Speed: 400 ± 20% rpm
  • Rated Torque: 0.35 kg·cm
  • Weight: 28g
  • No Load Current Consumption: 40-180 mA
  • You can buy this motor [here](https://robu.in/product/300-rpm-bo-motor-straight) To connect this motor to a Lego-compatible axle, we 3D-printed a custom adapter
    Motor-Axle Adapter
    This is a female-to-female adapter ### Motor Driver At first, we used the L298N Dual H-Bridge motor driver. The problem with that was that it was big because of the heatsink and the module wasn't built efficiently. After evaluating more options, we settled on the L293D motor driver. This is just like the L298N but is a small IC instead of a big module.
    L293D Motor Driver IC

    Specifications:

  • Type: Dual H-Bridge
  • Power Supply Voltage: 4.5v to 36v
  • Logic Voltage: 4.5v to 7v
  • 2 DC Motors
  • Internal ESD Protection
  • We are using the following pin connections for the motor driver:
    1. Enable 1,2 - For PWM speed control of motor A
    2. Input 1 - Direction control pin 1 for motor A
    3. Output 1 - Connected to one terminal of motor A
    4. GND - Ground
    5. GND - Ground
    6. Output 2 - Connected to other terminal of motor A
    7. Input 2 - Direction control pin 2 for motor A
    8. VCC 2 - Power supply (+)
    9. Enable 3,4 - For PWM speed control of motor B
    10. Input 3 - Direction control pin 1 for motor B
    11. Output 3 - Connected to one terminal of motor B
    12. GND - Ground
    13. GND - Ground
    14. Output 4 - Connected to other terminal of motor B
    15. Input 4 - Direction control pin 2 for motor B
    16. VCC 1 - Power Supply (+) for logic
    ### Rear Wheel Drive We have achieved RWD with the motor connected to a lego rear differential. We have used the lego parts to minimize friction.
    Lego Rear Differential Lego Rear Differential
    ![Rear Differential GIF](./other/README-images/MobilityManagement/RearDifferential.gif) ### Servo Motor We are using an ackermann steering geometry to steer the robot. This system runs with an MG90 servo motor.
    MG90 Servo Motor

    Specifications:

  • Angle Constaint: 0° to 360°
  • Input Voltage: 4.8v to 6v
  • Stall Torque: 1.8 kg/cm
  • Here is the pinout of the MG90 Servo Motor:
    1. VCC - Red Wire - 5V
    2. Signal - Orange Wire - PWM
    3. Ground - Black Wire - GND
    To connect the servo motor to the steering geometry, we created a 3D-printed adapter which converted the servo rotor into a lego compatible axle.
    Servo Adapter
    It is a female-to-female adapter. ### Ackermann Steering System We are using the Ackermann steering system. This system allows the front 2 wheels to turn at different angles, allowing for sharper turns and decreasing friction due to drag.
    Ackermann Steering
    Ackermann Steering
    ![Steering System GIF](./other/README-images/MobilityManagement/Ackermann.gif) ### Chassis #### Photos | | | | :--: | :--: | | *Orthographic View 1* | *Orthographic View 2* | | | | | *Top View* | *Bottom View* | | | | | *Left View* | *Right View* | ![Robot Chassis GIF](./other/README-images/Chassis/BotChassis.gif) #### Assembly Guide If the PDF is not loading properly, you can view the guide [here](https://github.com/WRO-Team-TheStatesmen/wro2025-fe-statesmen/blob/main/other/README-images/Chassis/ChassisAssembly.pdf) ### Connection Diagram for Mobility System
    Mobility System Connections
    ### Code Snippets #### Running the L293D Motor Driver with an ESP32 Devkit ``` // Program for running the motors via the L293D motor driver IC on an ESP32 devkit #define IN1 13 #define IN2 12 #define ENA 14 void setup() { pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT); pinMode(ENA, OUTPUT); } void loop() { // Forward digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); analogWrite(ENA, 255); delay(2000); // Stop analogWrite(ENA, 0); delay(1000); // Backwards digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH); analogWrite(ENA, 255); delay(2000); // Stop analogWrite(ENA, 0); } ``` #### Running the MG90 Servo Motor with an ESP32 Devkit ``` // Program for rotating the steering MG90 servo motor connected to an ESP32 devkit [signal pin = 25] const int servoPin = 25; const int servoCenter = 138; // Servo center is the position of the servo at which the steering system is centered void moveServoAngle(int angle){ angle = constrain(angle,0,180); int pulsewidth = map(angle,0,180,500,2400); digitalWrite(servoPin, HIGH); delayMicroseconds(pulsewidth); digitalWrite(servoPin, LOW); delay(20 - pulsewidth/1000); } void steer(int angle) { moveServoAngle(servoCenter + angle); } void setup() { pinMode(servoPin, OUTPUT); } void loop() { steer(0); delay(1000); steer(30); delay(1000); steer(0); delay(1000); steer(-30); delay(1000); } ``` ### Problems We Faced and How We Solved Them
    1. Bot Rear Skidding: At first, we were not using a rear differential but rather the motor connected to a gear which rotated a single axle which both the wheels were on. There was a huge problem with this. As shown in the image below, both wheeels have to move a different distance.

      Wheel Distance Difference
      To fix this, we added the Rear Differential. This allows the 2 wheels to move at different speeds while only using one motor.
    2. Bot Front Skidding: At first, we were using a parallel steering geometry. The same problem occurred. There was a lot of friction and drag. This limited the maximum turning angle of the robot.

      ![Steering System GIF](./other/README-images/MobilityManagement/SteeringSystem.gif) As you can see, in this system, both the wheels are turned equally. This does not achieve peak efficiency. That is why we switched to the ackermann system.
    3. Axle Sliiping Out of Differential Initially, the axle used in the differential was too small. Because of vibrations, it often slipped out of the differential, which caused the gears to come out and completely ruined the robot's movement. If we used longer axles, it would stick out of the robot and could touch other blocks. To fix this, we did two things. Firstly, we structured the chassis of the robot since there were some redundant pieces. We also switched out the rear tyres to the normal blue tyres since it would fix the problem without causing a significant change in the movement of the robot.
    4. No Exact Piece for Ackermann: While trying to create the ackermann steering geometry, we noticed that no single piece could create the steering geometry. At first, we used this piece: Lego Piece 6629 However, using this caused a problem that the robot wasn't fully level. After trying out multiple options, we finally settled on joining 2 pieces to achieve this: Ackermann Assembly Part With this, we encountered another problem. If we tried to turn it more than 40 degree, it would cause a lot of tension and the 2 pieces would disconnect. To increase the maximum turning angle to 60 degrees, we had to superglue the 2 pieces together.
    ### Possible Improvements
    1. Using a Higher RPM Motor so that the robot can move faster
    2. Using a Motor with an Encoder so that the robot movements become more accurate and precise
    3. Using Metal Gears for the Rear Differential so that it can withstand higher speeds
    4. Using a Heat Sink or Active Cooling for the Motor Driver so that it doesn't get too hot much while being used
    5. Using Four-Wheel Drive for better movement
    6. Using All-Wheel Steering for better and faster turning
    7. Using a Custom Gearbox instead of relying on the BO motor's gear ratio to create an optimized gear ratio
    8. Using a Fully 3D Printed Chassis instead of using lego parts so that the robot is as light as possible
    ## Power and Sense Management Our robot uses an 11.1v Li-ion Battery and a 2500 mAh 3C 11.1v LiPo battery for power with a buck converter. An ESP32 Devkit single board microcontroller is used with a Raspberry Pi 5 single board computer. Along with this, 4 TF Luna LiDAR sensors (one at the left, two at the front, and one at the back) are used for disance measurement and a BNO055 orientation sensor to measure the rotation. ### Li-Ion Battery We are using an 11.1v Li-ion battery pack
    11.1V Li-ion Battery

    Specifications:

  • Voltage: 11.1V
  • Capacity: 2200mAh
  • Discharge Rate: 2C
  • Maximum Current: Capcity x Discharge Rate = 4.8A
  • This battery pack consists of 3 18650 3.7v Li-ion cells This battery powers the motors, ESP32, and all the sensors You can buy this battery [here](https://www.amazon.in/2200MAH-Rechargeable-Battery-Robotics-Vehicle/dp/B0DNZJ7823/) ### LM2596 Buck Converter We are using an 11.1v battery, but the ESP32 requires 5v to be provided to it. To convert the 11.1v to 5v, we are using an LM2596 DC-DC Buck Converter
    LM2596 Buck Converter

    Specifications:

  • Supply Voltage: 3v - 40v
  • Output Voltage: 1.5v - 35v (adjustable) --> Set to 5v for us
  • Output Current: Rated 2A, Maximum 3A
  • Switching Frequency: 150KHz
  • Conversion Efficiency: 92%
  • You can buy the LM2596 Buck Converter [here](https://robu.in/product/lm2596s-dc-dc-buck-converter-power-supply/) ### LiPo Battery We are using a lithium polymer (LiPo) battery to power the Raspberry Pi.
    LiPo battery

    Specifications:

  • Output Voltage: 11.1v
  • Capacity: 2500 mAh
  • Discharge Rate: 3C
  • Maximum Current: 3C * 2.5Ah = 7.5A
  • ### ESP32 Devkit SBM We are using an ESP32 Devkit single board microcontroller.
    ESP32 Devkit

    Specifications:

  • Supply Voltage: 5v
  • Power Consumption: 150mA - 250mA
  • GPIO Pins: 25
  • Clock Speed: 240MHz
  • Flash Memory: 4 MB
  • SRAM: 520 KB
  • We chose this microcontroller over others because of its high clock speed and processing power. Here is the pinout diagram for this ESP32: ESP32 Devkit Pinout ### Raspberry Pi 5 SBC We are using the Raspberry Pi 5 Model B 8 GB RAM single board computer. We have chosen this after considering multiple options. We ended up on this one because of its cost and technical specifications mentioned below.
    Raspberry Pi 5

    Specifications:

  • Supply Voltage and Current (Optimal): 5.1v at 5A
  • CPU: Broadcom BCM2712 Quadcore Cortex-A76 - 64-bit SoC @ 2.4GHz
  • RAM: 8GB
  • USB Type A Ports: 4
  • GPIO Pins: 28
  • Here is the pinout of the Raspberry Pi 5 Raspberry Pi 5 Pinout ### Camera We are using the FINGERS 1080p Hi-Res camera. We chose this camera after considereing multiple factors like its field of view, high resolution, and color saturation.
    Fingers 1080p Hi-Res Camera

    Specifications:

  • Power Consumption: 200mA
  • ### TF Luna LiDAR Sensor We are using the TF-Luna LiDAR sensor to find the distances from the robot to the walls. We chose this sensor because of its speed, accuracy, and range.
    TF Luna LiDAR Sensor

    Specifications:

  • Supply Voltage: 5v
  • Power Consumption: 70mA
  • Range: 8m
  • Resolution: 1cm
  • Communication Protocol: I2C / UART
  • Ranging Frequency: 1 to 250 Hz (100 Hz by default)
  • Here is the pinout of the TF Luna LiDAR sensor: By default, it uses the UART communication protocol. However, if we connect the CFG pin to ground, it uses the I2C protocol. We are using the UART protocol. UART works by sending asynchronus data without the use of a clock signal. It uses a start and stop bit. To connect this sensor to a Lego part, we 3D-printed a custom mount
    TF Luna LiDAR
    This is a female-to-female adapter ### BNO055 Orentiation Sensor We are using the BNO055 Absolute Orientation Sensor. This sensor contains an accelerometer, a gyroscope, and a geomagnetic sensor. We chose this sensor after considering factors such as accuracy and weight.
    BNO055 Orientation Sensor

    Specifications:

  • Supply Voltage: 3.3v or 5v
  • Power Consumption: ~12.3mA
  • Communication Protocol: I2C
  • Here is the pinout of the BNO055 sensor: ### Connection Diagram for Power System
    Power System Connections
    ### Connection Diagram for Sensors #### Connection for Sensors Connected to ESP32
    Sesnsor System Connections
    #### Connection for Sensors Connected to Raspberry Pi
    Sesnsor System Connections
    We connected the third TF Luna to the Raspberry Pi. This is because of two reasons: 1. The ESP32 only supports 2 UART connections (excluding the USB port serial) 2. The ESP32 fully handles the open challenge, which requires just the right and left LiDAR. For the obstacle challenge, it merely acts as a node between the sensors and the Raspberry Pi. The third TF Luna is directly connected to the Raspberry Pi. ### Code Snippets for Components #### Running 2 TF Luna Sensors with an ESP32 Devkit ``` // Program to get data from 2 TF-Luna LiDAR sensors connected to the ESP32 via UART connections HardwareSerial tfLuna1(1); HardwareSerial tfLuna2(2); void setup() { Serial.begin(115200); delay(500); tfLuna1.begin(115200, SERIAL_8N1, 22, 23); tfLuna2.begin(115200, SERIAL_8N1, 19, 21); Serial.println("Setup Complete"); } void readTFLuna(HardwareSerial &sensor, const char* label) { if (sensor.available()) { if (sensor.read() == 0x59) { if (sensor.read() == 0x59) { byte low = sensor.read(); byte high = sensor.read(); int distance = (high << 8) + low; sensor.read(); sensor.read(); sensor.read(); sensor.read(); sensor.read(); Serial.print(label); Serial.print(" Distance: "); Serial.print(distance); Serial.println(" cm"); } } } } void loop() { readTFLuna(tfLuna1, "Sensor 1"); readTFLuna(tfLuna2, "Sensor 2"); } ``` #### Running the BNO055 Orientation Sensor with an ESP32 Devkit ``` // Program for running the BNO055 IMU sensor connected to an ESP32 devkit [on SDA pin 32 and SCL pin 33] #include #include #include #include TwoWire myWire = TwoWire(0); Adafruit_BNO055 bno = Adafruit_BNO055(55, 0x28, &myWire); void setup() { Serial.begin(115200); myWire.begin(32, 33); if (!bno.begin()) { Serial.println("Error - BNO055 not detected"); while (1); } delay(1000); bno.setExtCrystalUse(true); Serial.println("BNO055 initialized"); } void loop() { sensors_event_t orientationData; bno.getEvent(&orientationData, Adafruit_BNO055::VECTOR_EULER); Serial.print("Yaw: "); Serial.print(orientationData.orientation.x); Serial.print(" | Roll: "); Serial.print(orientationData.orientation.y); Serial.print(" | Pitch: "); Serial.println(orientationData.orientation.z); delay(500); } ``` #### Running a TF Luna with Raspberry Pi 5 ``` import serial import time ser = serial.Serial( port="/dev/serial0", baudrate=115200, timeout=1 ) def read_lidar(): while True: if ser.read() == b'\x59': if ser.read() == b'\x59': frame = ser.read(7) if len(frame) == 7: distance = frame[0] + (frame[1] << 8) strength = frame[2] + (frame[3] << 8) temperature = (frame[4] + (frame[5] << 8)) / 8 - 256 checksum = frame[6] checksum_calc = (0x59 + 0x59 + sum(frame[:6])) & 0xFF if checksum_calc == checksum: return distance, strength, temperature if __name__ == "__main__": try: while True: result = read_lidar() if result: distance, strength, temperature = result print(f"Distance: {distance} cm | Strength: {strength} | Temp: {temperature:.1f}°C") time.sleep(0.1) except KeyboardInterrupt: ser.close() print("Stopped.") ``` ### Problems We Faced and How We Solved Them
    1. Raspberry Pi Power Issues At first, we were using the Li-ion battery to power the Raspberry Pi. We faced 2 issues in this. Firstly, the Li-ion battery didn't give a proper constant output. There were a lot of voltage spikes and drops. This caused the Raspberry Pi to shut down in the middle of running. To fix this, we stopped powering the Raspberry Pi from the pins but instead gave it power through the USB-C port. This is because the USB-C port has built-in protection circuitry Next, another problem we faced was the the Pi didn't receive enough current. We are using 2C 3.7v 2200mAh cells, which create a 2C 11.1v 2200mAh battery. 2.2 Ah * 2C = 4.4 Ah. The Raspberry Pi needs 5 Amps, and a lot of current is also consumed by the motors and other components. The 4.4 Amp battery was working fine we started running the motors. After that, the Pi gave a low voltage warning and soon shut down. After that, we used a power bank. The problem with it was that was that it had a 5V 3A output, which was enough was a Raspberry Pi 4 but not for a Raspberry Pi 5 Finally, we settled on LiPo battery. This battery had a 2500 mAh capacity with a 3C discharge rate. This allowed for a maximum current of 7.5A. This battery is only used for the Raspberry Pi so that there are no voltage drops due to the motor or other components.
    2. 4 TF Luna sensors not working on ESP32 At first, we tried to connect all the TF Luna sensors to the ESP32. This did not work because the ESP32 only support 2 UART connections (excluding the USB bort Serial). To counter this problem, we connected the front sensors to the Raspberry Pi. This would not cause any issues at all since the front sensors aren't needed for the open challenge (the open challenge is fully run by the ESP32).
    ### Potential Improvements
    1. Using a sing LiPo Battery Instead of Li-Ion and LiPo: We can use a different LiPo battery with a capacity of ~3000 mAh and a discahrge rate of 5C+. This will remove the need for the Li-ion battery.

    2. Using a Supercapacitor Buffer: This will stabilize and smooth out the output from the battery. It will smooth out voltage spikes and dips when motors suddenly draw high current, protecting the components

    3. Using Quaternion Angle Outputs on BNO055: The Euler angle outputs on the BNO055 orientation sensor don't work properly. When other axes are tilted beyond 20 degrees, the x axis value becomes unreliable and distorted. Using the quaternion values will require more code and conversions, but will be more reliable and precise. The problem within the sensor is that its internal firmware (version 0x0311) has a math bug.

    4. Using two BNO055 sensors: The BNO055 sometimes drifts over periods of extended use. To decrease the drift, we can use 2 sensors and average out the values to get a more accurate value

    ## Obstacle Management ### Open Challenge The process for the open challenge is fairly simple. It is fully explained through the flowchart below.
    Open Challenge Flowchart
    ### Obstacle Challenge
    Obstacle Challenge Flowchart
    #### Block Avoid Sequence
    Block Avoid Flowchart
    #### Failsafe Sequence
    Failsafe Sequence Flowchart
    ### Block Detection To detect the blocks, we are running OpenCV on the Raspberry Pi. The block detection is using masking with LAB color ranges. We chose the LAB color space instead of HSV or BGR because LAB is more reliable in different lighting. LAB Color Space ### ESP32-Rasberry Pi Communication Data sent from RPi to ESP32 Data Sent from ESP32 to RPi ### Code Snippet for Detecting Blocks ``` import cv2 import numpy as np import time def detect_biggest_block(frame, SHOW=False): global last_turn, DIRECTION full_h, full_w, _ = frame.shape roi_start = int(full_h * 0.2) frame = frame[roi_start:full_h, 0:full_w] adjusted = cv2.convertScaleAbs(frame, alpha=CONTRAST, beta=BRIGHTNESS) inv_gamma = 1.0 / GAMMA table = np.array([(i / 255.0) ** inv_gamma * 255 for i in np.arange(256)]).astype("uint8") adjusted = cv2.LUT(adjusted, table) lab = cv2.cvtColor(adjusted, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) l_clahe = clahe.apply(l) lab_clahe = cv2.merge((l_clahe, a, b)) adjusted_frame = cv2.cvtColor(lab_clahe, cv2.COLOR_LAB2BGR) lab_frame = cv2.cvtColor(adjusted_frame, cv2.COLOR_BGR2LAB) lab_raw = cv2.cvtColor(frame, cv2.COLOR_BGR2LAB) detections = {"red": [], "green": [], "blue": [], "orange": []} blue_y_values, orange_y_values = [], [] for color, (lower, upper) in COLOR_RANGES.items(): lower_bound = np.array(lower, dtype=np.uint8) upper_bound = np.array(upper, dtype=np.uint8) source_lab = lab_raw if color == "orange" else lab_frame mask = cv2.inRange(source_lab, lower_bound, upper_bound) kernel = np.ones((3, 3), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area = cv2.contourArea(cnt) if area < MIN_AREA.get(color, 0): continue x, y, w_box, h_box = cv2.boundingRect(cnt) cx, cy = x + w_box // 2, y + h_box // 2 detections[color].append((cx, cy, area, color)) if color == "blue": blue_y_values.append(cy) elif color == "orange": orange_y_values.append(cy) if SHOW: cv2.rectangle(frame, (x, y), (x + w_box, y + h_box), colors[color], 2) cv2.circle(frame, (cx, cy), 4, colors[color], -1) cv2.putText(frame, f"{color} ({area})", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, colors[color], 2) max_blue_y = max(blue_y_values) if blue_y_values else None max_orange_y = max(orange_y_values) if orange_y_values else None valid_blocks = [] for color in ["red", "green"]: for (cx, cy, area, col) in detections[color]: if time.time() - last_turn > COOLDOWN: if max_blue_y and cy <= max_blue_y: continue if max_orange_y and cy <= max_orange_y: continue valid_blocks.append((col, cy, cx, area)) closest_block = None if valid_blocks: closest_block = max(valid_blocks, key=lambda b: b[1]) color, cy, cx, area = closest_block cy_full = cy + roi_start x_percent = (cx / full_w) * 100 y_percent = ((full_h - cy_full) / full_h) * 100 cell_w = full_w // 5 cell_h = full_h // 5 col_idx = cx // cell_w row_idx = cy_full // cell_h cell_number = row_idx * 5 + col_idx + 1 else: color, cy, cx, area, cell_number, x_percent, y_percent = (None, None, None, None, None, None, None) if SHOW: cell_w = full_w // 5 cell_h = full_h // 5 display = frame.copy() for i in range(1, 5): cv2.line(display, (i * cell_w, 0), (i * cell_w, frame.shape[0]), (200, 200, 200), 1) for j in range(1, 5): y_line = j * cell_h - roi_start if 0 <= y_line < frame.shape[0]: cv2.line(display, (0, y_line), (frame.shape[1], y_line), (200, 200, 200), 1) num = 1 for r in range(5): for c in range(5): y_pos = r * cell_h + 20 - roi_start if 0 <= y_pos < frame.shape[0]: x_pos = c * cell_w + 10 cv2.putText(display, str(num), (x_pos, y_pos), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (180, 180, 180), 1) num += 1 if closest_block: cv2.rectangle(display, (cx - 20, cy - 20), (cx + 20, cy + 20), (0, 255, 255), 3) cv2.putText(display, f"Closest: {color} Cell:{cell_number}", (cx, cy - 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) cv2.imshow("Block Detection", display) cv2.waitKey(1) return (color if closest_block else None, y_percent if closest_block else None, x_percent if closest_block else None, area if closest_block else None, cell_number if closest_block else None) ``` ### Code Snippet for Moving Straight with KP ``` // Driving in a straight line // Including Libraries #include #include #include #include // Defining Pins #define IN1 13 #define IN2 12 #define ENA 14 // Setting Up Communication with BNO055 TwoWire myWire = TwoWire(0); Adafruit_BNO055 bno = Adafruit_BNO055(55, 0x28, &myWire); // Settings Variables const float target_direction = 0; const float kp = -1; // Robot will move in opposite direction of the error const int servoPin = 25; const int servoCenter = 95; // Servo position at which the steering system is centered // Custom function to rotate the servo (the default PWM of the ESP32 is unreliable) void moveServoAngle(int angle){ angle = constrain(angle,0,180); int pulsewidth = map(angle,0,180,500,2400); digitalWrite(servoPin, HIGH); delayMicroseconds(pulsewidth); digitalWrite(servoPin, LOW); delay(20 - pulsewidth/1000); } // Steer function to automatically account for servo center when called void steer(int direction) { moveServoAngle(servoCenter + direction); } void setup(void) { Serial.begin(115200); pinMode(servoPin, OUTPUT); pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT); pinMode(ENA, OUTPUT); steer(0); myWire.begin(32, 33); if(!bno.begin()) { Serial.println("No BNO055 detected ... Check your wiring or I2C ADDR!"); while(1); } delay(2000); bno.setExtCrystalUse(true); } void loop(void) { digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); analogWrite(ENA, 200); sensors_event_t orientationData; bno.getEvent(&orientationData, Adafruit_BNO055::VECTOR_EULER); float direction = orientationData.orientation.x; if (direction > 180) { direction -= 360; // Normalizes angle from 0 to 360 to -180 to 180 where 0 is front, -90 is left and 90 is right } float error = direction - target_direction; int steer_val = (int) error*kp; // Calculate Correction Required steer_val = constrain(steer_val, -40, 40); Serial.print("Steer: "); Serial.println(steer_val); steer(steer_val); } ``` ### Code Snippet for Wall Following in one direction (Clockwise) (For Open Challenge) Check out [this](#running-2-tf-luna-sensors-with-an-esp32-devkit), [this](#running-the-bno055-orientation-sensor-with-an-esp32-devkit), and [this](#running-the-mg90-servo-motor-with-an-esp32-devkit) for definitions of functions called in the code below ``` void loop(void) { updateTFLuna(Serial1, distance_right); if (counter < 12) { digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); analogWrite(ENA, 255); sensors_event_t orientationData; bno.getEvent(&orientationData, Adafruit_BNO055::VECTOR_EULER); float heading = normalizeAngle(orientationData.orientation.x); float error = normalizeAngle(heading - target_direction); int steer_val = (int)(error * kp); steer_val = constrain(steer_val, -40, 40); steer(steer_val); if (distance_right >= 50) { target_direction += 90; target_direction = normalizeAngle(target_direction); while (true) { updateTFLuna(Serial1, distance_right); bno.getEvent(&orientationData, Adafruit_BNO055::VECTOR_EULER); heading = normalizeAngle(orientationData.orientation.x); error = normalizeAngle(heading - target_direction); steer_val = (int)(error * kp); steer_val = constrain(steer_val, -40, 40); steer(steer_val); digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); analogWrite(ENA, 250); if (abs(error) <= 10) break; } counter++; steer(0); digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); analogWrite(ENA, 250); delay(500); while (true) { updateTFLuna(Serial1, distance_right); if (distance_right < 50 && distance_right > 0) break; delay(10); } } } else { digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); analogWrite(ENA, 255); delay(200); digitalWrite(IN1, LOW); digitalWrite(IN2, LOW); analogWrite(ENA, 0); while (1); // Stay stopped } } ``` The full source code of the robot is provided [here](https://github.com/WRO-Team-TheStatesmen/wro2025-fe-statesmen/tree/main/src) ### Problems We Faced and How We Solved Them
    1. Block not Being Detected in Different Lighting Conditions At first, we were using HSV ranges. A major problem with them was they were extremely unreliable. They caused a lot of noise and didn't work if the lighting changed.

      To fix this, we used the LAB color space. This works better with different lighting conditions. You can see about it
    2. [here](#block-detection)
    3. Colliding into blocks Another major problem was that it was often colliding into the traffic signs as it avoided them. This was when there when a block was placed right after a turn.

      To fix this, we added a reverse failsafe. We basically made it so that if the block was right in front of the robot, the robot would reverse and then continue.
    ### Potential Improvements
    1. Using a 360° LiDAR Sensor: Our current TF Luna LiDAR sensors can only see in 1 direction. If we use a 360° LiDAR sensor, the robot will be able to see everything around it, which can significantly improve its performance.

    2. Using ROS2: Using the Robot Operation System 2 (ROS2) would be much better. This OS is custom-made for robots and has a lot of features. Using this will improve the performance of the robot.

    3. Using PID: We are currently using just KP to move straight. It would be better if we implement the full PID algorithm (KP, KI, and KD). This will make the robot movements smoother
    ## Engineering Factor As shown in the previous sections, we have used our own design for the vehicle. We have also used off the shelf electrical components such as motors and sensors. ## Software Integration Our robot relies not only on the hardware, but our seamless integration of our software with every single component. We have made a modular software architecture that separates low-level hardware control, communication protocols, and high-level decision-making Software Integration Flowchart ### System Architecture The ESP32 acts as a hub between the Raspberry Pi and the motor, servo motor, the BNO055 orientation sensor and 2 LiDAR sensors. The ESP32 is connected to these components. It is only reponsible for sending data to and receiving data from them. It acts as a hub in the middle. ESP32 Integration with Components The Raspberry Pi is the brain of our robot. It is responsible for image processing and making decisions. It is also connected to the front LiDAR sensor because the ESP32 can only handle 2 UART connections. Raspberry Pi Integration with Components ### Communication Protocol The Raspberry Pi needs to have all the data to make proper decisions. It needs data recorded by the ESP32 for that. For this, we have created a csv-based serial communication system between the Raspberry Pi and ESP32. The Raspberry Pi is connected to the ESP32 via a USB cable. The ESP32 sends the data to the Raspberry Pi over a serial connection at a baud rate of 115200, which means 115200 signal changes per second. The ESP32 sends 3 values to the ESP32: | Data Value 1 | Data Value 2 | Data Value 3 | |--------------|--------------------------|---------------------------| | BNO055 Heading (*Orientation.x*) | Left Distance from TF Luna | Right Distance from TF Luna | Data Sent from ESP32 to RPi After the ESP32 receives all the data, it processes it. It detects the location of the block and based on that it decides where the robot should go. For that, it needs to send the data to the ESP32. The Raspberry Pi sends 3 values to the ESP32. | Data Value 1 | Data Value 2 | Data Value 3 | |--------------|--------------------------|---------------------------| | Speed | Movement Direction (Front/Back) | Steer Angle | Data sent from RPi to ESP32 #### Error Checks In data communication, there is also a small chance of errors. To prevent this, whenever data is received it is checked for errors. 1. If more than 3 data values are received, then that whole piece of data is discarded and considered having errors 2. If the data values received contain characters other that numbers, the data is discarded and considered having errors 3. If the value of the data exceeds the bounds, the data is discarded and considered having errors #### Error Checking for Data Received by ESP32 Error Checking ESP32 #### Error Checking for Data Received by ESP32 Error Checking RPi ### Code Modularity (Hardware Abstraction Layer) We have structured our code in a modular way. Each component of the robot is handled by a dedicated function. This ensures that our script remains easily understandable and changes can be made without any trouble. The code can also easily be understood and reused #### Function for TF Luna ``` void updateTFLuna(HardwareSerial &sensor, volatile int &distanceVar) { while (sensor.available() >= 9) { if (sensor.read() == 0x59 && sensor.read() == 0x59) { byte low = sensor.read(); byte high = sensor.read(); int distance = (high << 8) + low; for (int i = 0; i < 5; i++) sensor.read(); if (distance <= 500) { distanceVar = distance; } } } } ``` This function works in a special way. Instead of returning a value, this function updates a global variable. This way, we can make sure that the program doesn't slow down as the clock speed of the TF Luna and the ESP32 is different. Earlier, it was slowing down is it waited until the TF Luna sent a value. Now, it only updates the variable when the TF Luna sends a value. #### Function for BNO055 ``` void updateBNO055(Adafruit_BNO055 &bno, volatile float &headingVar) { sensors_event_t orientationData; bno.getEvent(&orientationData, Adafruit_BNO055::VECTOR_EULER); headingVar = orientationData.orientation.x; } ``` This function works in the same way as the `updateTFLuna()` function #### Function for Motor ``` void setMotor(int speed, int direction) { speed = constrain(speed, 0, 255); if (direction == 0) { digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); } else if (direction == 1) { digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH); } analogWrite(ENA, speed); } ``` #### Function for Servo Motor ``` void moveServoAngle(int angle) { angle = constrain(angle, 0, 180); int pulsewidth = map(angle, 0, 180, 500, 2400); digitalWrite(servoPin, HIGH); delayMicroseconds(pulsewidth); digitalWrite(servoPin, LOW); delay(20 - pulsewidth / 1000); } void steer(int angleOffset) { moveServoAngle(servoCenter + angleOffset); } ``` To control our steering system from the MG90 servo motor, we created 2 functions. `moveServoAngle()` is a low-level function that sends a PWM signal. We used this instead of ledcWrite because of 3 main reasons 1. ledcWrite is unreliable 2. Precise control over pulse width 3. Modularity and clarity `steer()` is a high-level function. This lets us directly decide what direction the robot goes in. It automatically accounts for the center position of the servo motor. If we put the value 0, the robot travels straight, + means right, and - means left. #### Function for LiDAR Sensor (Raspberry Pi) ``` def get_front(port="/dev/ttyAMA0", baud=115200, timeout=0.1): try: with serial.Serial(port, baud, timeout=timeout) as ser: frame = ser.read(9) if len(frame) != 9: return None if frame[0] != 0x59 or frame[1] != 0x59: return None dist = frame[2] | (frame[3] << 8) return dist if dist >= 0 else None except serial.SerialException as e: return None ``` ### Communication API (Communication Layer) We created specialized python functions to have a communication system which can be debugged, edited, and understood easily #### readData() ``` def read_data(DEBUG: bool = False): try: line = ser.readline().decode(errors="ignore").strip() if not line: if DEBUG: print("No data received from serial.") return None, None, None, None parts = line.split(",") if len(parts) != 3: if DEBUG: print(f"Invalid data format: {line}") return None, None, None, None angle = normalize_angle(int(parts[0])) left = int(parts[1]) right = int(parts[2]) front = get_front(port=LIDAR_PORT) if DIRECTION else 100 # A default value of 100 is returned if the robot does not know the direction yet to avoid errors if DEBUG: print(f"Read -> Angle: {angle}, Left: {left}, Front: {front}, Right: {right}") return angle, left, front, right except (ValueError, UnicodeDecodeError) as e: if DEBUG: print(f"Data parsing error: {e}") return None, None, None, None except Exception as e: if DEBUG: print(f"Unexpected error in read_data: {e}") return None, None, None, None ``` This function is used to read the incoming data from the ESP32. It will be run on the Raspberry Pi. #### sendData() ``` def send_data(speed, direction, steer, DEBUG=False): cmd = f"{speed},{direction},{steer}\n" if DEBUG: print(f"Data Sent: {cmd}") ser.write(cmd.encode()) ``` This function is used to send the speed, movement direction (front or back), and steer angle to the ESP32. Once the raspberry pi makes a decision, it can use this function to send that decision. ### Debugging Throughout development, there were multiple challenges in the hardware-software integration, such as incorrect sensor readings, communication errors, etc. To address these issues, we created a structured debugging process. #### Component Debugging (Unit Testing) If there is a problem in any single component, we have a way of debuggin that as well. You can see [here](/src/invididual-components/) that we have individual programs to test out individual components. With this, we can know exactly what problem is being faced. #### Communication Debugging (Integration Testing) We implemented debugging tools directly within our code. As you can see in the `readData()` and `sendData()` functions, there is a DEBUG parameter. If this parameter is set to `True`, then whatever data is being sent / received will be printed. This makes debugging extremely easy. Also, there is another way of checking communication on the ESP32 end. The ESP32 can directly be connected to a laptop. Then, the Arduino IDE can be opened. Since the data is sent via serial in csv format, it can easily be seen through the serial monitor. We can also send data easily via the serial monitor to the ESP32 Error Checking RPi To easily be able to send data to the ESP32 without using the serial monitor, we createed a custom python program with `pygame` to control the robot You can see the code of the program [here](/src/tools/pygame-debugging.py) Pygame Debugging #### Image Processing Debugging Also, we made it so that when the robot is running, we can connect to the Raspberry Pi via VNC and see the live camera feed. This camera feed will show all the blocks detected and how the robot is taking decisions. You can view it [here](/other/README-images/SoftwareIntegration/Debugging.mp4) ## Robot Construction Guide ### Step 1 - Print the Parts Firstly, you will need to print all the parts. The *.STL files to be printed are [here](https://github.com/WRO-Team-TheStatesmen/wro2025-fe-statesmen/tree/main/models). We used the Bambu Lab X1-Carbon 3D printer to print everything. ### Step 2 - Assemble the Chassis Next, you will need to assemble the chassis. The guide to assemble the chassis is given [here](https://github.com/WRO-Team-TheStatesmen/wro2025-fe-statesmen/blob/main/other/README-images/Chassis/ChassisAssembly.pdf). We have used lego parts for the chassis to make it so that we can easily change it. ### Step 3 - Attach the Components Step 1 - Insert the batteries in the chassis in the section shown below Step 3 - 1 You may secure the batteries using double sided tape Step 2 - Screw the TF Luna LiDAR sensors to the 3D printed mounts and attach those mounts to the chassis as shown below TF Luna 1 (Left) Step 3 - 2 - 1 TF Luna 2 (Right) Step 3 - 2 - 2 TF Luna 3 (Front) Step 3 - 2 - 3 For the front TF Luna, you will have to attach a 5-hole beam to the marked place and then attach the mount to that beam Step 3 - Insert the Raspberry Pi in the chassis as shown below Step 3 - 3 Step 4 - Place the PCB with the ESP32, BNO055, LM2596, and other connections on the section shown below Step 3 - 4 ### Step 4 - Connect the Components Connect all the components with each other as per the electrical diagrams in above sections ### Step 5 - Upload the Code to the ESP32 Open the /src/final-code/ folder in the GitHub directory. Depending on if you want to run the open challenge or the obstacle challenge, open the respective directory. In that directory there will be 1 or 2 files. The will be a *.ino file and a *.py file (only for obstacle challenge). Download the files. Open the *.ino file in the Arduino IDE. Connect the ESP32 to the device. Click on "choose board and port" and choose the "DOIT ESP32 Devkit v1" board in the boards section and the correct COM port in the "ports" section Then, click on the upload code button to upload the code to the ESP32 ### Step 6 - Set Up the Raspberry Pi Next, you will need up to set up a Raspberry Pi Step 1 - Install Raspbian OS on an SD card
    1. Get a micro SD card (recommended 32 GB) and put it into your device (you may require a micro SD card adapter)
    2. Install the Raspberry Pi imager
    3. Open the software, choose your micro SD card, and flash the Raspbian OS (64 bit) into the SD card
    Step 2 - Turn on the Raspberry Pi
    1. Insert the micro SD card into the Raspberry Pi
    2. Connect the Raspberry Pi to a screen
    3. Power the Raspberry Pi (preferably using the official adapter)
    Step 3 - Set up the Raspberry Pi
    1. Follow the configuration steps on the screen
    2. Open the terminal
    3. Enter the command sudo apt update
    4. Enter the command sudo apt upgrade
    5. Enter the command sudo raspi-config and then go to interface options > UART > enable
    Step 4 - Install the required libraries and code
    1. Go into the desktop using this command: cd Desktop
    2. Clone the GitHub repository using this command: git clone https://github.com/WRO-Team-TheStatesmen/wro2025-fe-statesmen.git
    3. Create a virtual environment using this command: python3 -m venv venv
    4. Download the required libraries using this command: ./venv/bin/python3 -m pip install opencv-python pyserial numpy
    ### Step 8 - Run the Code To run the code on the Raspberry Pi, all you need to do is run the command `./venv/bin/python3 ./src/final-code/obstacle-challenge/obstacle-challenge.py` ## The Challenge You can view the challenge rules [here](https://wroindia.org/wp-content/uploads/2025/01/WRO-2025-Future-Engineers-Self-Driving-Cars-General-Rules.pdf) You can find the playfield [here](https://wroindia.org/wp-content/uploads/2025/01/WRO-2025_FutureEngineers_Playfield-1.pdf) ## Randomizer We also made a randomizer for automatically setting up a randomization. You can view it [here](/other/obstacle-randomizer.zip)