kong
Published

AI cat teasing device

The project is an AI-enhanced, interactive laser cat toy with image recognitionto provide entertainment and physical activity for your pets.

BeginnerFull instructions provided246
AI cat teasing device

Things used in this project

Hardware components

Seeed Studio XIAO RP2040
Seeed Studio XIAO RP2040
×1
Seeed Studio XIAO Expansion Board
Seeed Studio XIAO Expansion Board
×1
SG90 Micro-servo motor
SG90 Micro-servo motor
×1
5mW Laser Module emitter - Red Point
Seeed Studio 5mW Laser Module emitter - Red Point
×1

Software apps and online services

Arduino IDE
Arduino IDE
Fusion
Autodesk Fusion
Seeed Studio SenseCraft

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

down cover

new down cover

upper cover

new upper cover

Code

Sketch 1

C/C++
Set the servos all the way to 90 degrees
#include <ESP32Servo.h>
#define SERVO_X_PIN D6
#define SERVO_Y_PIN D7

Servo myservo1; 
Servo myservo2;

void setup() {
 Serial.begin(115200);
 myservo1.attach(servoPin1); 
 myservo2.attach(servoPin2);
}

void loop() {

  myservo1.write(90); 
  myservo2.write(90);  
  delay(30); 
 
}

Sketch 2

C/C++
Mian sketch
#include <ESP32Servo.h>
#include <Seeed_Arduino_SSCMA.h>

// define the pin which conect to servo
#define SERVO_X_PIN D6 // control the angle of x axis
#define SERVO_Y_PIN D7 // control the angle of y axis

// define the laser pin
#define Laser_Pin 8 

// define the x and y limition
#define X_MIN 0
#define X_MAX 240
#define Y_MIN 0
#define Y_MAX 240
#define Central_coor_X 120;
#define Central_coor_Y 120;

// define PID parameter
#define kp 0.1
#define ki 0.001
#define kd 0

// set the sleep time to one hour
#define uS_TO_S_FACTOR 1000000ULL 
#define TIME_TO_SLEEP  60*60     

RTC_DATA_ATTR int bootCount = 0;

// creat Servo object
Servo servoX;
Servo servoY;
SSCMA AI;

// Create initial angle, centre angle, adjust according to reality
const float C_angle_x = 90;
const float C_angle_y = 60;
float Last_angle_x = C_angle_x;
float Last_angle_y = C_angle_y;

int errror = 2;

int speed = 50;

/*
Print the reason of wakeup
*/
void print_wakeup_reason(){
  esp_sleep_wakeup_cause_t wakeup_reason;

  wakeup_reason = esp_sleep_get_wakeup_cause();

  switch(wakeup_reason)
  {
    case ESP_SLEEP_WAKEUP_EXT0 : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
    case ESP_SLEEP_WAKEUP_EXT1 : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
    case ESP_SLEEP_WAKEUP_TIMER : Serial.println("Wakeup caused by timer"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD : Serial.println("Wakeup caused by touchpad"); break;
    case ESP_SLEEP_WAKEUP_ULP : Serial.println("Wakeup caused by ULP program"); break;
    default : Serial.printf("Wakeup was not caused by deep sleep: %d\n",wakeup_reason); break;
  }
}

void sleep_in_loop(){
  delay(100);
  Serial.println("wake");
  Serial.println("Going to sleep now");
  esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
  Serial.println("Setup ESP32 to sleep for every " + String(TIME_TO_SLEEP) + " Seconds");

  Serial.flush(); 
  esp_deep_sleep_start();
  Serial.println("This will never be printed");
}

// 停止转动
void stop_rotate(void) {
  digitalWrite(D6, HIGH);
  delayMicroseconds(1500);
  digitalWrite(D6, LOW);
}

/*
Brief:
Detedt the cat
Collect the imge info three times, if the the average score is over 60 that means there is a cat 
In the image 

input:

return: 
Return Ture -- there is a cat in the image 
Return False -- did not find a cat yet 
*/
bool check_cat(){
  Serial.println("checking ");
  int total_score = 0;

  float average_score;
  int c_n;
  int j = 0;

  while( j < 3 ){
    if(!AI.invoke()) {
      if (AI.boxes().size() > 0){
          for (int i = 0; i < AI.boxes().size(); i++){
//    if only want to detect the cat use if(AI.boxes()[i].target == 0)
              if(AI.boxes()[i].target == 0 || AI.boxes()[i].target == 1){
                c_n = i;
                total_score += int(AI.boxes()[i].score);
              }
            }
      }
      j++;
    }
    
  }
// you can tune total_score/3The contrast value to better identify cats/dogs.
  if(total_score/3 > 60){
    Serial.println(total_score/3);
    return true;
  }else{
    return false;
  }

}

/*
Brief:
Attract cat
shaking to attract the cat while detecting if the cat is in the camera

Input:
angleX : current x-axis angle
angleY : current y-axis angle
Attach : Whether to attract or not, 0 is required to attract.
*/
void attact_cat(int angleX, int angleY, int Attact){
  int d = 1;
  int n = random(1, 5); //ramdom pick a number from 1~5

  //Increases as the y-axis angle becomes smaller
  float vibrate = map(angleY,100,35,5,10);
  if(Attach == 0){
    switch (n) {
      case 1:
        for(int i = 0; i < 6; i++){
          servoX.write(angleX + vibrate);
          vibrate = -vibrate;
          delay(100);
        }
        break;
      case 2:
      for(int i = 0; i < 6; i++){
        servoY.write(angleY + vibrate);
        vibrate = -vibrate;
        delay(100);
      }
      break;
      case 3:
        delay(100);
      break;
      case 4:
        delay(100);
      break;
    }

  }
delay(100);
}

/*
Brief:
Move laser/camera to specified angle

Input:
angleX : current x-axis angle
angleY : current y-axis angle
target_x : target x-axis angle
target_y : target y-axis angle

Return value: 
No return value
*/
void go_to_target(int angleX, int angleY, int target_x, int target_y){
  int offet_set_X = target_x - angleX;
  int offet_set_Y = target_y - angleY;

  float deltaDergeeX = offet_set_X * kp;
  float deltaDergeeY = offet_set_Y * kp;

  for(int i = 0; i < 10; i++){
    float angleX = Last_angle_x + deltaDergeeX;
    float angleY = Last_angle_y + deltaDergeeY;
    servoX.write(angleX);
    servoY.write(angleY);
    Last_angle_x = angleX;
    Last_angle_y = angleY;
    delay(speed);
  }

}

/*
brief:
Move laser/camera to centre
Input:
angleX : current x-axis angle
angleY : current y-axis angle

Return value: 
No return value
*/
void back_to_centrel(int angleX, int angleY){
  int offet_set_X = C_angle_x - angleX;
  int offet_set_Y = C_angle_y - angleY;

  float deltaDergeeX = offet_set_X * kp;
  float deltaDergeeY = offet_set_Y * kp;

  for(int i = 0; i < 10; i++){
    float angleX = Last_angle_x + deltaDergeeX;
    float angleY = Last_angle_y + deltaDergeeY;
    servoX.write(angleX);
    servoY.write(angleY);

    Last_angle_x = angleX;
    Last_angle_y = angleY;
    delay(speed);
  }

}

/*
brief
Moving laser folding motion in x axis

Input:
angleX : current x-axis angle
angleY : current y-axis angle

Return value: 
No return value
*/
void forward_back_X(int angleX, int angleY) {
  int total_dergee = 4;

  int n = 0;
  int Attach = 0;

  for (int i = 0; i < total_dergee; i++) {
    while(!check_cat()){
      attact_cat(Last_angle_x,Last_angle_y, Attach);
      n++;
      if(n > 20){
        digitalWrite(Laser_Pin, LOW); 
        Attach = 1;
      }
    }
    n = 0;
    Attach = 0;
    if (i < total_dergee / 4) {
      angleX = 120;
    } else if (i < total_dergee / 2) {
      angleX =  60;
    } else if (i < total_dergee / 4 * 3) {
      angleX = 120;
    } else {
      angleX = 60;
    }
    angleY = 60;

    servoX.write(angleX);
    servoY.write(angleY);
    Last_angle_x = angleX;
    Last_angle_y = angleY;
    delay(speed);
  }

}

/*
brief
Move the laser along the Y-axis folding line movement, can be modified according to the following code to do real curved movement:

void Laser_Curve_X(int angleX, int angleY) {
  int total_dergee = 4*12;
  const int remember_x = angleX;
  const int remember_y = angleY;
  for (int i = 0; i < total_dergee; i++) {
      // Serial.println(i);
    if (i < total_dergee / 4) {
      angleX += 3;
    } else if (i < total_dergee / 2) {
      angleX -= 3 ;
    } else if (i < total_dergee / 4 * 3) {
      angleX += 3;
    } else {
      angleX -= 3;
    }
    angleY += 3;
    if(angleX >= 150 || angleY >= 150 ){
      i = total_dergee;
      break;
    }
    servoX.write(angleX);
    servoY.write(angleY);
    Last_angle_x = angleX;
    Last_angle_y = angleY;
    delay(50);
  }
  angleX = remember_x;
  angleY = remember_y;
  Last_angle_x = angleX;
  Last_angle_y = angleY;
  servoX.write(remember_x);
  servoY.write(remember_y);
    // Laser_Curve_Back_X(Last_angle_x, Last_angle_y);
  Serial.println("Laser_Curve_X done");
  delay(1000);
}

Input:
angleX : current x-axis angle
angleY : current y-axis angle

Return value: 
No return value

*/
void Laser_Curve_X(int angleX, int angleY) {
  int total_dergee = 4;
  int n = 0;
    int Attach = 0;
  for (int i = 0; i < total_dergee; i++) {
    while(!check_cat()){
      attact_cat(Last_angle_x,Last_angle_y,Attach);
      n++;
      if(n > 20){
        digitalWrite(Laser_Pin, LOW); 
        Attach= 1;
      }else{

      }
    }
    n = 0;
    Attach= 0;
    digitalWrite(Laser_Pin, HIGH);
    if (i < total_dergee / 4) {
      angleX = 130;
    } else if (i < total_dergee / 2) {
      angleX = 70 ;
    } else if (i < total_dergee / 4 * 3) {
      angleX = 130;
    } else {
      angleX = 70;
    }
    angleY += 2 ;
    servoX.write(angleX);
    servoY.write(angleY);
    Last_angle_x = angleX;
    Last_angle_y = angleY;
    delay(speed);
  }

}

/*
brief:
Move the laser along the Y-axis in reverse zigzag motion

Input:
angleX : current x-axis angle
angleY : current y-axis angle

Return value: 
No return value
*/
void Laser_Curve_Back_X(int angleX, int angleY) {
  int total_dergee = 4;
  int n = 0; //吸引次数
  int Attach = 0;

  for (int i = 0; i < total_dergee; i++) {
    while(!check_cat()){
      attact_cat(Last_angle_x,Last_angle_y,Attach);
      n++;
      if(n > 20){
        digitalWrite(Laser_Pin, LOW); 
        Attach= 1;
        // delay(5000);
      }
    }
    n = 0;
    Attach=0;
    digitalWrite(Laser_Pin, HIGH);
    if (i < total_dergee / 4) {
      angleX = 80;
    } else if (i < total_dergee / 2) {
      angleX = 130;
    } else if (i < total_dergee / 4 * 3) {
      angleX = 70;
    } else {
      angleX = 130;
    }
    angleY -= 5;

    servoX.write(angleX);
    servoY.write(angleY);
    Last_angle_x = angleX;
    Last_angle_y = angleY;
    delay(speed);
  }
 
}

/*
brief:
Moving the laser along the X-axis folding line

Input:
angleX : current x-axis angle
angleY : current y-axis angle

Return value: 
No return value
*/
void Laser_Curve_Y(int angleX, int angleY) {
  int total_dergee = 4;
  const int remember_x = angleX;
  const int remember_y = angleY;
  int n = 0;
  int Attach = 0;
  for (int i = 0; i < total_dergee; i++) {
    while(!check_cat()){
      attact_cat(Last_angle_x,Last_angle_y,Attach);
      n++;
      if(n > 20){
        digitalWrite(Laser_Pin, LOW); 
        Attach= 1;
      }
    }

    n = 0;
    Attach=0;
    digitalWrite(Laser_Pin, HIGH);
    if (i < total_dergee / 4) {
      angleY = 70;
    } else if (i < total_dergee / 2) {
      angleY = 45;
    } else if (i < total_dergee / 4 * 3) {
      angleY = 70;
    } else {
      angleY = 45;
    }
    angleX += 5;
    servoX.write(angleX);
    servoY.write(angleY);
    Last_angle_x = angleX;
    Last_angle_y = angleY;
    delay(speed);
  }

}

/*
Brief:
Moving the laser in a reverse zigzag motion along the X-axis

Input:
angleX : current x-axis angle
angleY : current y-axis angle

Return value: 
No return value
*/
void Laser_Curve_Back_Y(int angleX, int angleY) {
  int total_dergee = 4;
  int n = 0;
  int Attach = 0;

  for (int i = 0; i < total_dergee; i++) {
    while(!check_cat()){
      attact_cat(Last_angle_x,Last_angle_y,Attach);
      n++;
      if(n > 20){
        digitalWrite(Laser_Pin, LOW); 
        Attach= 1;
      }
    }
    n = 0;
    Attach=0;
    digitalWrite(Laser_Pin, HIGH);
    if (i < total_dergee / 4) {
      angleY = 45;
    } else if (i < total_dergee / 2) {
      angleY = 70;
    } else if (i < total_dergee / 4 * 3) {
      angleY = 45;
    } else {
      angleY = 70;
    }
    angleX -= 5;

    // if(angleX <= 30){
    //   Serial.println("too much");
    //   i = total_dergee;
    //   break;
    // }
// Serial.println("angleY");
// Serial.println(angleY);
    servoX.write(angleX);
    servoY.write(angleY);
    Last_angle_x = angleX;
    Last_angle_y = angleY;
    delay(speed);
  }

}

/*
Brief:
Move the laser in a circular motion

Input:
angleX : current x-axis angle
angleY : current y-axis angle

Return value: 
No return value
*/
void Laser_Cricle(int angleX, int angleY) {
  int total_dergee = 4;

  int n = 0;
    int Attach = 0;
  for (int i = 0; i < total_dergee; i++) {
    while(!check_cat()){
      attact_cat(Last_angle_x,Last_angle_y,Attach);
      n++;
      if(n > 20){
        digitalWrite(Laser_Pin, LOW); 
        Attach= 1;
      }
    }
    n = 0;
    Attach=0;
    digitalWrite(Laser_Pin, HIGH);
    if (i < total_dergee / 4) {
      angleX = 140;
      angleY = 45;
    } else if (i < total_dergee / 2) {
      angleX = 90;
      angleY = 30;
    } else if (i < total_dergee / 4 * 3) {
      angleX = 60;
      angleY = 45;
    } else {
      angleX = 90;
      angleY = 60;
    }

    servoX.write(angleX);
    servoY.write(angleY);
    Last_angle_x = angleX;
    Last_angle_y = angleY;
    delay(speed);
  }

}

/*
brief:
Move the laser in a reverse circle motion

Input:
angleX : current x-axis angle
angleY : current y-axis angle

Return value: 
No return value
*/
void Laser_Cricle_Back(int angleX, int angleY) {
  int total_dergee = 4;

  int n = 0;
    int Attach = 0;
  for (int i = 0; i < total_dergee; i++) {
    while(!check_cat()){
      attact_cat(Last_angle_x,Last_angle_y,Attach);
      n++;
      if(n > 20){
        digitalWrite(Laser_Pin, LOW); 
        Attach= 1;
      }
    }
  
    n = 0;
    Attach=0;
    digitalWrite(Laser_Pin, HIGH);
    if (i < total_dergee / 4) {
      angleX = 50 ; 
      angleY = 45;
    } else if (i < total_dergee / 2) {
      angleX = 90;
      angleY = 30;
    } else if (i < total_dergee / 4 * 3) {
      angleX = 140;
      angleY = 45;
    } else {
      angleX = 90;
      angleY = 60 ;
    }

    if(angleX <= 30 || angleY <= 40 ){
      Serial.println("too much");
      i = total_dergee;
      break;
    }

    servoX.write(angleX);
    servoY.write(angleY);
    Last_angle_x = angleX;
    Last_angle_y = angleY;
    delay(speed);
  }

}

/*
Brief:
PID control can be used for following, although it will be too slow compared to a cat you can try 
It for human face following

Input:
x : x-axis coordinate of the object in the camera.
y : y-axis coordinate of the object in the camera.

Return value: 
No return value
*/
void server_control(int x, int y)
{
  // 将摄像头参数值映射到舵机角度范围 
  float targetAngleX = map(x, X_MAX, X_MIN, 0, 180);
  float targetAngleY = map(y, Y_MIN, Y_MAX, 0, 180);

  //  得出真实偏移量
  int offet_set_X = targetAngleX - 90;
  int offet_set_Y = targetAngleY - 90;

  float deltaDergeeX = offet_set_X * kp;
  float deltaDergeeY = offet_set_Y * kp;

  float angleX = Last_angle_x + deltaDergeeX;
  float angleY = Last_angle_y + deltaDergeeY + errror;

  servoX.write(angleX);
  servoY.write(angleY);
  Last_angle_x = angleX;
  Last_angle_y = angleY;      
}

void setup() {
  Serial.begin(9600);

  AI.begin();

  // set the laser pin to output
  pinMode(Laser_Pin, OUTPUT);  

  // set the servo pin to output 
  digitalWrite(SERVO_X_PIN, HIGH);
  digitalWrite(SERVO_Y_PIN, HIGH);
  delayMicroseconds(1500);
  digitalWrite(SERVO_X_PIN, LOW);
  digitalWrite(SERVO_Y_PIN, LOW);
  servoX.attach(SERVO_X_PIN);
  servoY.attach(SERVO_Y_PIN);
  servoX.write(Last_angle_x);
  servoY.write(Last_angle_y);

  delay(1000); 

  //increase and print the boot count on every reboot 
  ++bootCount;
  Serial.println("Boot number: " + String(bootCount));
 
  print_wakeup_reason();

  /*
  Set the wakeup time to 1 hour 
  */
  esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
  Serial.println("Setup ESP32 to sleep for every " + String(TIME_TO_SLEEP) +
  " Seconds");
}

void loop() {
  
  Serial.println("one loop finish");
  if (!AI.invoke()) {
    //Since it was found that lasers can also be used to tease dogs, if you need to tease cats //exclusively you can change the condition to
//if (AI.boxes().size() > 0 && AI.boxes()[0].score >= 70 && target == 0 )
// You can adjust the contrast value of AI.boxes()[0].score to better recognise cats/dogs.
    if (AI.boxes().size() > 0 && AI.boxes()[0].score >= 70 )  //&& target == 0  // 如果是猫
    {
      server_control(AI.boxes()[0].x, AI.boxes()[0].y);
      delay(500);

      //trun on the laser
      digitalWrite(Laser_Pin, HIGH);
      Serial.println("invoke success");
      // active the laser motion
      for (int i = 0; i < 20; i ++){
        delay(50); 
        int n = random(1, 6); //随机选 1~5的值
        Serial.print(" movtion is ");
        switch (n) {
          case 1:
          Serial.println(n);
            Laser_Curve_X(Last_angle_x, Last_angle_y);
            back_to_centrel(Last_angle_x, Last_angle_y);
            break;
          case 2:
          Serial.println(n);
            Laser_Cricle(Last_angle_x, Last_angle_y);
            back_to_centrel(Last_angle_x, Last_angle_y);
            break;
          case 3:
          Serial.println(n);
            Laser_Curve_Y(Last_angle_x, Last_angle_y);
            back_to_centrel(Last_angle_x, Last_angle_y);
            break;
          case 4:
            Serial.println(n);
            Laser_Curve_Back_Y(Last_angle_x, Last_angle_y);
            back_to_centrel(Last_angle_x, Last_angle_y);
            break;
          case 5:
            Serial.println(n);
            Laser_Curve_Back_X(Last_angle_x, Last_angle_y);
            back_to_centrel(Last_angle_x, Last_angle_y);
            break;
          // case 6:
          // Serial.println(n);
          //   forward_back_X(Last_angle_x, Last_angle_y);
          //   back_to_centrel(Last_angle_x, Last_angle_y);
          //   break;
          }
        }
        digitalWrite(Laser_Pin, LOW); 
        back_to_centrel(Last_angle_x, Last_angle_y);
        Last_angle_x = 90;
        Last_angle_y = 60;
        servoX.write(Last_angle_x);
        servoY.write(Last_angle_y);
        // delay(10000);
        sleep_in_loop();
        Serial.println("rest");
      // angleX = Last_angle_x ;
      // angleY = Last_angle_y ;
      }

      delay(500);

  } 
  delay(500);
}

Sketch 3

C/C++
Sample code of Grove vision AI V2,
#include <Seeed_Arduino_SSCMA.h>

SSCMA AI;

void setup()
{
    AI.begin();
    Serial.begin(9600);
}

void loop()
{
    if (!AI.invoke())
    {
        Serial.println("invoke success");
        Serial.print("perf: prepocess=");
        Serial.print(AI.perf().prepocess);
        Serial.print(", inference=");
        Serial.print(AI.perf().inference);
        Serial.print(", postpocess=");
        Serial.println(AI.perf().postprocess);

        for (int i = 0; i < AI.boxes().size(); i++)
        {
            Serial.print("Box[");
            Serial.print(i);
            Serial.print("] target=");
            Serial.print(AI.boxes()[i].target);
            Serial.print(", score=");
            Serial.print(AI.boxes()[i].score);
            Serial.print(", x=");
            Serial.print(AI.boxes()[i].x);
            Serial.print(", y=");
            Serial.print(AI.boxes()[i].y);
            Serial.print(", w=");
            Serial.print(AI.boxes()[i].w);
            Serial.print(", h=");
            Serial.println(AI.boxes()[i].h);
        }
        for (int i = 0; i < AI.classes().size(); i++)
        {
            Serial.print("Class[");
            Serial.print(i);
            Serial.print("] target=");
            Serial.print(AI.classes()[i].target);
            Serial.print(", score=");
            Serial.println(AI.classes()[i].score);
        }
        for (int i = 0; i < AI.points().size(); i++)
        {
            Serial.print("Point[");
            Serial.print(i);
            Serial.print("] target=");
            Serial.print(AI.points()[i].target);
            Serial.print(", score=");
            Serial.print(AI.points()[i].score);
            Serial.print(", x=");
            Serial.print(AI.points()[i].x);
            Serial.print(", y=");
            Serial.println(AI.points()[i].y);
        }
    }
}

Credits

kong
2 projects • 2 followers
Contact

Comments

Please log in or sign up to comment.