Lesson 15: Obstacle Avoidance

Intelligent Navigation with Ultrasonic Detection and Avoidance

🚧 Smart obstacle detection and autonomous navigation behavior

🎯 Learning Objectives

Navigation Skills:

  • Implement obstacle detection systems
  • Create intelligent avoidance algorithms
  • Master autonomous navigation behavior

Programming Concepts:

  • Distance-based decision making
  • Mecanum wheel kinematics
  • Real-time sensor integration

🚧 Section 6.6: Obstacle Avoidance

Intelligent Navigation System

In this section, you can learn to detect the distance of obstacles through a glowing ultrasonic module. The robot car can be simultaneously controlled to turn right to avoid obstacles. You'll use a glowing ultrasonic sensor to detect the distance of an obstacle and control the car to move forward or turn accordingly.

🎯 Program Flow

  • Ultrasonic Task: Continuously measure distance and set RGB LED colors
  • Motor Control Task: Control car movement based on ultrasonic distance
  • Parallel Processing: Both tasks run simultaneously for real-time response

🚗 Movement Behavior

  • Distance ≥ 300mm: Car moves forward
  • Distance < 300mm: Car turns right to avoid obstacle
  • RGB Feedback: Same color system as previous lessons

⚠️ Safety Reminders

  • Remove Bluetooth module before uploading to avoid serial conflicts
  • Turn off battery box before connecting USB cable
  • Test in open area with adequate space for turning

/**
 * @file ultrasonic_avoid.ino
 * @author Anonymity(Anonymity@hiwonder.com)
 * @brief 超声波避障(Obstacle avoidance)
 * @version V1.0
 * @date 2024-04-23
 *
 * @copyright Copyright (c) 2023
 *
 */

// Compatible Ultrasound class - matches Hiwonder I2C implementation
#include <Arduino.h>
#include <Wire.h>

// I2C Constants (exact Hiwonder Ultrasound.h definitions)
#define ULTRASOUND_I2C_ADDR         0x77
#define DISDENCE_L                  0    // Distance low 8 bits, unit: mm
#define DISDENCE_H                  1    // Distance high 8 bits
#define RGB_BRIGHTNESS              50   // 0-255
#define RGB_WORK_MODE               2    // RGB mode: 0=user defined, 1=breathing mode
#define RGB1_R                      3    // Probe 1 R value, 0~255
#define RGB1_G                      4    // Probe 1 G value, 0~255  
#define RGB1_B                      5    // Probe 1 B value, 0~255
#define RGB2_R                      6    // Probe 2 R value, 0~255
#define RGB2_G                      7    // Probe 2 G value, 0~255
#define RGB2_B                      8    // Probe 2 B value, 0~255
#define RGB1_R_BREATHING_CYCLE      9    // Breathing cycle for probe 1 R, unit: 100ms
#define RGB1_G_BREATHING_CYCLE      10   // Breathing cycle for probe 1 G, unit: 100ms
#define RGB1_B_BREATHING_CYCLE      11   // Breathing cycle for probe 1 B, unit: 100ms
#define RGB2_R_BREATHING_CYCLE      12   // Breathing cycle for probe 2 R, unit: 100ms
#define RGB2_G_BREATHING_CYCLE      13   // Breathing cycle for probe 2 G, unit: 100ms
#define RGB2_B_BREATHING_CYCLE      14   // Breathing cycle for probe 2 B, unit: 100ms
#define RGB_WORK_SIMPLE_MODE        0
#define RGB_WORK_BREATHING_MODE     1

class Ultrasound {
private:
  bool wireWriteByte(uint8_t addr, uint8_t val) {
    Wire.beginTransmission(addr);
    Wire.write(val);
    return (Wire.endTransmission() == 0);
  }
  
  bool wireWriteDataArray(uint8_t addr, uint8_t reg, uint8_t *val, unsigned int len) {
    Wire.beginTransmission(addr);
    Wire.write(reg);
    for(unsigned int i = 0; i < len; i++) {
      Wire.write(val[i]);
    }
    return (Wire.endTransmission() == 0);
  }
  
  int wireReadDataArray(uint8_t addr, uint8_t reg, uint8_t *val, unsigned int len) {
    if (!wireWriteByte(addr, reg)) return -1;
    Wire.requestFrom(addr, len);
    unsigned char i = 0;
    while (Wire.available() && i < len) {
      val[i] = Wire.read();
      i++;
    }
    return i;
  }
  
public:
  Ultrasound() {
    Wire.begin();
  }
  
  void Breathing(uint8_t r1, uint8_t g1, uint8_t b1, uint8_t r2, uint8_t g2, uint8_t b2) {
    uint8_t value = RGB_WORK_BREATHING_MODE;
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB_WORK_MODE, &value, 1);
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB1_R_BREATHING_CYCLE, &r1, 1);
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB1_G_BREATHING_CYCLE, &g1, 1);
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB1_B_BREATHING_CYCLE, &b1, 1);
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB2_R_BREATHING_CYCLE, &r2, 1);
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB2_G_BREATHING_CYCLE, &g2, 1);
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB2_B_BREATHING_CYCLE, &b2, 1);
  }
  
  void Color(uint8_t r1, uint8_t g1, uint8_t b1, uint8_t r2, uint8_t g2, uint8_t b2) {
    uint8_t value = RGB_WORK_SIMPLE_MODE;
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB_WORK_MODE, &value, 1);
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB1_R, &r1, 1);
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB1_G, &g1, 1);
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB1_B, &b1, 1);
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB2_R, &r2, 1);
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB2_G, &g2, 1);
    wireWriteDataArray(ULTRASOUND_I2C_ADDR, RGB2_B, &b2, 1);
  }
  
  uint16_t GetDistance() {
    uint16_t distance;
    wireReadDataArray(ULTRASOUND_I2C_ADDR, DISDENCE_L, (uint8_t *)&distance, 2);
    return distance;
  }
};

// OFFICIAL HIWONDER OBSTACLE AVOIDANCE CODE
#define STOP 0
#define FORWARD 1
#define BACKWARD 2
#define FILTER_N 3                ///< 递推平均滤波法(Recursive average filtering method)

Ultrasound ultrasound;  ///< 实例化超声波类(Instantiate the ultrasound class)

const static uint8_t keyPin = 3;
const static uint8_t pwm_min = 50;
const static uint8_t motorpwmPin[4] = { 10, 9, 6, 11} ;
const static uint8_t motordirectionPin[4] = { 12, 8, 7, 13};

bool keyState;          ///< 按键状态检测(Detect button status)
bool taskStart = 0;
uint16_t dis;
uint16_t last_avoid_dis;
uint16_t ultrasonic_distance(void);
int filter_buf[FILTER_N + 1];
int Filter(void);
void Motor_Init(void);
void Velocity_Controller(uint16_t angle, uint8_t velocity,int8_t rot,bool drift);
void Motors_Set(int8_t Motor_0, int8_t Motor_1, int8_t Motor_2, int8_t Motor_3);

void setup() {
  Serial.begin(9600);
  Serial.setTimeout(500);
  pinMode(keyPin, INPUT);
  Motor_Init();
  printf("2");
}

void loop() {
  keyState = analogRead(keyPin);
  printf("111");
  if(!keyState) taskStart = 1;
  if(taskStart) {
    ultrasonic_distance();
    dis = ultrasonic_distance();
    Velocity_Controller( 0, 100, 0, 0);
    while(dis < 300) {
      dis = ultrasonic_distance();
      Velocity_Controller( 0, 0, -100, 0);
    }  
  }
}

 /**
 * @brief 滤波(filter)
 * @param filter_sum / FILTER_N
 * @arg None
 * @retval None
 * @note None
 */
int Filter() {
  int i;
  int filter_sum = 0;
  filter_buf[FILTER_N] = ultrasound.GetDistance();     ///< 读取超声波测值(Read the ultrasonic ranging value)
  for(i = 0; i < FILTER_N; i++) {
    filter_buf[i] = filter_buf[i + 1];               ///< 所有数据左移,低位仍掉(Shift all data to the left, and clear the low bit)
    filter_sum += filter_buf[i];
  }
  return (int)(filter_sum / FILTER_N);
}

 /**
 * @brief 超声波距离数据获取(Obtain ultrasonic distance data)
 * @param None
 * @arg None
 * @retval distance
 * @note None
 */
uint16_t ultrasonic_distance(){
  uint8_t s;
  uint16_t distance = Filter();                                               ///< 获得滤波器输出值(Get the output value of the filter)
  Serial.print("Distance: ");Serial.print(distance);Serial.println(" mm"); ///< 获取并且串口打印距离,单位mm(Get and print the distance via serial port, unit: mm)

  if (distance > 0 && distance <= 80){
      ultrasound.Breathing(1, 0, 0, 1, 0, 0);                                 ///< 呼吸灯模式,周期0.1s,颜色红色(Red breathing light mode, in 0.1s)
   }
   
  else if (distance > 80 && distance <= 180){
      s = map(distance,80,180,0,255);
      ultrasound.Color((255-s), 0, 0, (255-s), 0, 0);                         ///< 红色渐变(Red gradient)
   }
   
   else if (distance > 180 && distance <= 320){
      s = map(distance,180,320,0,255);
      ultrasound.Color(0, 0, s, 0, 0, s);                                     ///< 蓝色渐变(Blue gradient)
   }
   
   else if (distance > 320 && distance <= 500){
      s = map(distance,320,500,0,255);
      ultrasound.Color(0, s, 255-s, 0, s, 255-s);                             ///< 绿色渐变(Green gradient)
   }
  else if (distance > 500){
      ultrasound.Color(0, 255, 0, 0, 255, 0);                                 ///< 绿色(Green)
   }
  return distance;  
}

 /* 电机初始化函数 */
void Motor_Init(void){
  for(uint8_t i = 0; i < 4; i++){
    pinMode(motordirectionPin[i], OUTPUT);
  }
  Velocity_Controller( 0, 0, 0, 0);
}

/**
 * @brief 速度控制函数(Speed control function)
 * @param angle   用于控制小车的运动方向,小车以车头为0度方向,逆时针为正方向。("angle" controls the robot's motion direction, with the front of the robot as 0 degrees and counterclockwise as the positive direction)
 *                取值为0~359(range from 0 to 359)
 * @param velocity   用于控制小车速度,取值为0~100。("velocity" controls the robot's speed, with a value range of 0 to 100)
 * @param rot     用于控制小车的自转速度,取值为-100~100,若大于0小车有一个逆("rot" controls the robot's self-rotation speed, with a value range of -100 to 100)
 *                 时针的自转速度,若小于0则有一个顺时针的自转速度。(If it is greater than 0, the robot has a counterclockwise self-rotation speed. If it is less than 0, the robot has a clockwise self-rotation speed)
 * @param drift   用于决定小车是否开启漂移功能,取值为0或1,若为0则开启,反之关闭。("drift" determines whether the robot enables drift. Value range: 0 or 1. If it is 0, drift is enabled; otherwise, it is disabled)
 * @retval None
 */
void Velocity_Controller(uint16_t angle, uint8_t velocity,int8_t rot,bool drift) {
  int8_t velocity_0, velocity_1, velocity_2, velocity_3;
  float speed = 1;
  angle += 90;
  float rad = angle * PI / 180;
  if (rot == 0) speed = 1;///< 速度因子(Speed factor)
  else speed = 0.5; 
  velocity /= sqrt(2);
  if (drift) {
    velocity_0 = (velocity * sin(rad) - velocity * cos(rad)) * speed;
    velocity_1 = (velocity * sin(rad) + velocity * cos(rad)) * speed;
    velocity_2 = (velocity * sin(rad) - velocity * cos(rad)) * speed - rot * speed * 2;
    velocity_3 = (velocity * sin(rad) + velocity * cos(rad)) * speed + rot * speed * 2;
  } else {
    velocity_0 = (velocity * sin(rad) - velocity * cos(rad)) * speed + rot * speed;
    velocity_1 = (velocity * sin(rad) + velocity * cos(rad)) * speed - rot * speed;
    velocity_2 = (velocity * sin(rad) - velocity * cos(rad)) * speed - rot * speed;
    velocity_3 = (velocity * sin(rad) + velocity * cos(rad)) * speed + rot * speed;
  }
  Motors_Set(velocity_0, velocity_1, velocity_2, velocity_3);
}
 
/**
 * @brief PWM与轮子转向设置函数(PWM and wheel turning setting function)
 * @param Motor_x   作为PWM与电机转向的控制数值。根据麦克纳姆轮的运动学分析求得。("Motor_x" is the control value for PWM and motor rotating. Calculated based on the kinematics analysis of mecanum wheels)
 * @retval None
 */
void Motors_Set(int8_t Motor_0, int8_t Motor_1, int8_t Motor_2, int8_t Motor_3) {
  int8_t pwm_set[4];
  int8_t motors[4] = { Motor_0, Motor_1, Motor_2, Motor_3};
  bool direction[4] = { 1, 0, 0, 1};///< 前进 左1 右0(Forward; left 1; right 0)
  for(uint8_t i; i < 4; ++i) {
    if(motors[i] < 0) direction[i] = !direction[i];
    else direction[i] = direction[i];

    if(motors[i] == 0) pwm_set[i] = 0;
    else pwm_set[i] = map(abs(motors[i]), 0, 100, pwm_min, 255);

    digitalWrite(motordirectionPin[i], direction[i]); 
    analogWrite(motorpwmPin[i], pwm_set[i]); 
  }
}

← Previous: Lesson 14
Course Overview
Next: Lesson 16 →
vector<Point> detectLine(Mat frame) {
  Mat gray, edges;
  cvtColor(frame, gray, COLOR_BGR2GRAY);
  Canny(gray, edges, 50, 150);
  
  vector<Vec4i> lines;
  HoughLinesP(edges, lines, 1, CV_PI/180, 50, 50, 10);
  
  vector<Point> linePoints;
  for (auto line : lines) {
    Point start(line[0], line[1]);
    Point end(line[2], line[3]);
    linePoints.push_back(start);
    linePoints.push_back(end);
  }
  
  return linePoints;
}

void followLineWithVision() {
  Mat frame = robot.captureFrame();
  vector<Point> linePoints = detectLine(frame);
  
  if (!linePoints.empty()) {
    // Calculate line direction and adjust robot heading
    float lineAngle = calculateLineAngle(linePoints);
    float robotHeading = robot.getHeading();
    float correction = lineAngle - robotHeading;
    
    // Smooth steering based on line position
    int leftSpeed = 70 + (correction * 2);
    int rightSpeed = 70 - (correction * 2);
    
    robot.setMotorSpeeds(leftSpeed, rightSpeed);
  } else {
    // No line detected - search pattern
    robot.turnRight(15);
    delay(200);
  }
}
                

💡 Key Insight

Computer vision allows robots to understand their environment like humans do, making decisions based on visual information rather than just proximity sensors.

🧠 Section 2: Machine Learning Integration (20 minutes)

Adaptive Behavior Learning

Implement simple machine learning to help the robot adapt to its environment:

class AdaptiveNavigation {
private:
  float successRates[4] = {0.5, 0.5, 0.5, 0.5}; // N, E, S, W
  int attempts[4] = {0, 0, 0, 0};
  int successes[4] = {0, 0, 0, 0};
  
public:
  int chooseBestDirection() {
    int bestDir = 0;
    float bestRate = successRates[0];
    
    for (int i = 1; i < 4; i++) {
      if (successRates[i] > bestRate) {
        bestRate = successRates[i];
        bestDir = i;
      }
    }
    
    return bestDir;
  }
  
  void recordOutcome(int direction, bool success) {
    attempts[direction]++;
    if (success) successes[direction]++;
    
    // Update success rate
    successRates[direction] = (float)successes[direction] / attempts[direction];
    
    Serial.print("Direction ");
    Serial.print(direction);
    Serial.print(" success rate: ");
    Serial.println(successRates[direction]);
  }
};

AdaptiveNavigation navigator;

void intelligentExploration() {
  int chosenDirection = navigator.chooseBestDirection();
  
  // Execute movement in chosen direction
  bool success = moveInDirection(chosenDirection);
  
  // Learn from the outcome
  navigator.recordOutcome(chosenDirection, success);
}
                

Pattern Recognition

Train the robot to recognize and respond to environmental patterns:

struct EnvironmentPattern {
  float lightLevel;
  float temperature;
  float distance[4];  // N, E, S, W distances
  String action;      // Learned optimal action
  float confidence;   // How confident we are in this action
};

class PatternLearner {
private:
  EnvironmentPattern patterns[50];
  int patternCount = 0;
  
public:
  String predictAction(float light, float temp, float distances[4]) {
    float bestMatch = 0;
    String bestAction = "explore";
    
    for (int i = 0; i < patternCount; i++) {
      float similarity = calculateSimilarity(light, temp, distances, patterns[i]);
      
      if (similarity > bestMatch && patterns[i].confidence > 0.7) {
        bestMatch = similarity;
        bestAction = patterns[i].action;
      }
    }
    
    return bestAction;
  }
  
  void learnPattern(float light, float temp, float distances[4], String action, bool successful) {
    // Find or create pattern
    int patternIndex = findSimilarPattern(light, temp, distances);
    
    if (patternIndex == -1 && patternCount < 50) {
      // Create new pattern
      patternIndex = patternCount++;
      patterns[patternIndex].lightLevel = light;
      patterns[patternIndex].temperature = temp;
      for (int i = 0; i < 4; i++) {
        patterns[patternIndex].distance[i] = distances[i];
      }
      patterns[patternIndex].confidence = 0.5;
    }
    
    if (patternIndex != -1) {
      // Update pattern
      patterns[patternIndex].action = action;
      if (successful) {
        patterns[patternIndex].confidence = min(1.0, patterns[patternIndex].confidence + 0.1);
      } else {
        patterns[patternIndex].confidence = max(0.0, patterns[patternIndex].confidence - 0.1);
      }
    }
  }
};
                

🎯 Pro Tip

Start with simple pattern recognition and gradually increase complexity. The robot should learn from both successful and failed attempts to improve its decision-making.

🔗 Section 3: Advanced Sensor Fusion (20 minutes)

Multi-Modal Sensor Integration

Combine multiple sensor types for robust navigation and decision-making:

struct SensorReading {
  float ultrasonicDistance;
  float imuHeading;
  float lightLevel;
  float temperature;
  Point visualTarget;
  bool visualTargetValid;
  unsigned long timestamp;
};

class AdvancedSensorFusion {
private:
  SensorReading readings[10];  // Circular buffer
  int currentIndex = 0;
  
public:
  SensorReading getCurrentReading() {
    SensorReading reading;
    reading.ultrasonicDistance = robot.getDistance();
    reading.imuHeading = robot.getHeading();
    reading.lightLevel = robot.getLightLevel();
    reading.temperature = robot.getTemperature();
    reading.visualTarget = robot.detectVisualTarget();
    reading.visualTargetValid = (reading.visualTarget.x != -1);
    reading.timestamp = millis();
    
    // Store in circular buffer
    readings[currentIndex] = reading;
    currentIndex = (currentIndex + 1) % 10;
    
    return reading;
  }
  
  NavigationDecision makeIntelligentDecision() {
    SensorReading current = getCurrentReading();
    NavigationDecision decision;
    
    // Confidence-weighted decision making
    float ultrasonicConfidence = calculateUltrasonicConfidence(current);
    float visualConfidence = current.visualTargetValid ? 0.9 : 0.1;
    float imuConfidence = 0.8;
    
    // Combine multiple sensor inputs
    if (visualConfidence > 0.7 && current.visualTargetValid) {
      // Trust visual system most when target is clearly visible
      decision = navigateToVisualTarget(current.visualTarget);
      decision.confidence = visualConfidence;
    }
    else if (ultrasonicConfidence > 0.6) {
      // Use ultrasonic for obstacle avoidance
      decision = avoidObstacles(current.ultrasonicDistance);
      decision.confidence = ultrasonicConfidence;
    }
    else {
      // Fall back to IMU-based exploration
      decision = exploreWithHeading(current.imuHeading);
      decision.confidence = imuConfidence;
    }
    
    return decision;
  }
};
                

Predictive Navigation

Use sensor history to predict future conditions and plan accordingly:

void predictiveNavigation() {
  // Analyze recent sensor trends
  float distanceTrend = calculateDistanceTrend();
  float headingStability = calculateHeadingStability();
  
  if (distanceTrend < -5) {
    // Distance decreasing rapidly - obstacle approaching
    Serial.println("Predictive: Obstacle approaching, preparing evasive action");
    prepareEvasiveManeuver();
  }
  else if (headingStability < 0.3) {
    // Heading unstable - possible wheel slippage or external force
    Serial.println("Predictive: Heading unstable, reducing speed");
    robot.setMaxSpeed(50);
  }
  else if (distanceTrend > 10 && headingStability > 0.8) {
    // Clear path ahead and stable heading - safe to increase speed
    Serial.println("Predictive: Clear path, increasing speed");
    robot.setMaxSpeed(100);
  }
}

float calculateDistanceTrend() {
  if (currentIndex < 5) return 0;  // Not enough data
  
  float recentAvg = 0, olderAvg = 0;
  for (int i = 0; i < 3; i++) {
    int idx = (currentIndex - 1 - i + 10) % 10;
    recentAvg += readings[idx].ultrasonicDistance;
  }
  for (int i = 3; i < 6; i++) {
    int idx = (currentIndex - 1 - i + 10) % 10;
    olderAvg += readings[idx].ultrasonicDistance;
  }
  
  return (recentAvg / 3) - (olderAvg / 3);
}
                

🎯 Section 4: Intelligent Mission Planning (20 minutes)

Complex Mission Framework

Create sophisticated missions that combine multiple objectives and adapt to changing conditions:

enum MissionState {
  EXPLORING,
  TARGET_ACQUIRED,
  APPROACHING_TARGET,
  COLLECTING_DATA,
  RETURNING_HOME,
  MISSION_COMPLETE
};

class IntelligentMission {
private:
  MissionState currentState = EXPLORING;
  Point homePosition;
  vector<Point> targetsFound;
  vector<Point> targetsVisited;
  unsigned long missionStartTime;
  
public:
  void executeMission() {
    switch (currentState) {
      case EXPLORING:
        if (exploreForTargets()) {
          currentState = TARGET_ACQUIRED;
        }
        break;
        
      case TARGET_ACQUIRED:
        Point nearestTarget = findNearestUnvisitedTarget();
        if (navigateToTarget(nearestTarget)) {
          currentState = APPROACHING_TARGET;
        }
        break;
        
      case APPROACHING_TARGET:
        if (isAtTarget()) {
          currentState = COLLECTING_DATA;
        }
        break;
        
      case COLLECTING_DATA:
        if (collectDataAtTarget()) {
          markTargetVisited();
          if (allTargetsVisited() || missionTimeExpired()) {
            currentState = RETURNING_HOME;
          } else {
            currentState = EXPLORING;
          }
        }
        break;
        
      case RETURNING_HOME:
        if (navigateToHome()) {
          currentState = MISSION_COMPLETE;
        }
        break;
        
      case MISSION_COMPLETE:
        robot.stop();
        reportMissionResults();
        break;
    }
  }
  
  bool exploreForTargets() {
    // Use computer vision to scan for targets
    Mat frame = robot.captureFrame();
    vector<Point> newTargets = detectTargets(frame);
    
    for (Point target : newTargets) {
      if (!isTargetKnown(target)) {
        targetsFound.push_back(target);
        Serial.println("New target discovered!");
        return true;
      }
    }
    
    // Continue exploration pattern
    intelligentExploration();
    return false;
  }
};
                

🛠️ Hands-On Activity: Intelligent Search and Rescue Mission (25 minutes)

Project: Smart Search and Rescue Robot

Program your miniAuto to conduct an intelligent search and rescue mission using computer vision, machine learning, and advanced sensor fusion.

Mission Requirements:

🏆 Advanced Challenges

📝 Assessment & Homework

📊 Quick Assessment

  • Demonstrate computer vision object detection
  • Show adaptive learning in action
  • Explain sensor fusion decision-making
  • Present intelligent mission results

🏠 Homework Assignment

  • Research advanced AI techniques in robotics
  • Design a complex multi-objective mission
  • Study real-world autonomous systems
  • Prepare for final competition project
← Previous: Lesson 14 📚 Semester Overview 📝 Take Lesson 15 Quiz Next: Lesson 16 →