Toby the Robo Dog ๐ถ
Too lazy to buy your pet food? Why not get a robot dog.
The project is a continuation of Mini Virtual Pet. My goal for the project is to create a physical robot dog that one can interact with using a desktop application based on the mini virtual petโs interface and gameplay.
Build Process
I started out by researching all the components needed for creating my robot, which are 4 servos and one ultrasonic sensor. I used an Arduino as the main control board.
I connected all four servos to the Arduino and started to model and 3d print some test enclosures for the servos
Then I began to work on the robotโs legs
After finishing the basic components, I put together an unibody frame that can hold all servos while carrying an Arduino board.
I connected all the essential parts and wrote the Arduino program for the robot to walk.
Test Walk
I added an ultrasonic distance sensor so that users can physically interact with the robot.
I looked up many methods to communicate with the Arduino using my laptop and found that bluetooth is the most efficient one.
I started to work on a virtual interface for user to control the robot via their computers. I took the design concept from a tamagochi game that I made before.
There are two indicators, โheartโ and โhappy faceโ.
Heart indicates the health of the pet, the pet will die if it gets to zero.
Happy face indicates the mood of the pet; the yellow face will turn red, showing the pet is mad if the happy level gets below 40.
There are two options on the home page, FEED and REMOTE
FEED lets the user to choose what to feed the pet (milk, apple ,or meat).
REMOTE lets the player control the robot
The robot is physically interactive outside of the REMOTE tap, meaning that it will response to the user automatically.
When on the REMOTE page, the user can press S on the keyboard to make the robot sit, U to make the robot stand, and D to lay the robot down.
If health gets to zero the pet will die and the robot will show dead.
Robot Code
robot_v2.ino
#include <Servo.h>
Servo myservo1;
Servo myservo2;
Servo myservo3;
Servo myservo4;
int caseState;
int interval = 100;
int pos;
int pos2;
int echoPin = 13;
int trigPin = 12;
float counter;
float pingTime;
float distance;
float speedOfSound = 776.5;
int timer;
//bluetooth
char val;
void setup() {
pinMode(trigPin, OUTPUT);
pinMode(echoPin, INPUT);
myservo1.attach(9);
myservo2.attach(10);
myservo3.attach(11);
myservo4.attach(6);
Serial.begin(9600);
}
void loop() {
if (Serial.available()) {
val = Serial.read();
}
if (val == 'A') caseState = 0;
if (val == 'B') caseState = 1;
if (val == 'C') caseState = 2;
if (val == 'D') caseState = 3;
if (val == 'E') caseState = 4;
if (val == 'X') {
if (distance <= 4) {
caseState = 3;
timer--;
if (timer <= 0 && distance <= 4) {
caseState = 0;
}
} else {
caseState = 2;
}
}
logic();
proximity();
//debug();
}
logic.ino
void logic() {
long start = millis();
switch (caseState) {
case 0:
timer = 0;
myservo1.write(180);
myservo3.write(0);
myservo2.write(0);
myservo4.write(180);
break;
case 1:
myservo1.write(90);
myservo3.write(90);
myservo2.write(90);
myservo4.write(90);
break;
case 2:
timer = (int)random(50, 300);
counter = 128 + 127 * cos (0.25 * PI / interval * (interval - start));
pos = map(counter, 0, 255, 40, 135);
pos2 = map(counter, 0, 255, 135, 40);
myservo1.write(pos);
myservo3.write(pos2);
myservo2.write(pos);
myservo4.write(pos2);
break;
case 3:
myservo1.write(155);
myservo3.write(90);
myservo2.write(25);
myservo4.write(90);
break;
case 4:// dead
myservo1.write(40);
myservo3.write(0);
myservo2.write(30);
myservo4.write(40);
break;
}
}
proximity.ino
void proximity() {
digitalWrite(trigPin, LOW);
digitalWrite(trigPin, HIGH);
digitalWrite(trigPin, LOW);
pingTime = pulseIn(echoPin, HIGH);
pingTime = pingTime / 1000000;
pingTime = pingTime / 3600;
distance = speedOfSound * pingTime;
distance = distance / 2;
distance = distance * 63360;
}
debug.ino
void debug(){
Serial.print("Servo Degree(9,10)= \t");
Serial.print(pos);
Serial.print("\tServo Degree(11,6)= \t");
Serial.print(pos2);
Serial.print("\tDistance= \t");
Serial.print(distance);
Serial.print("\tcaseState= \t");
Serial.print(caseState);
Serial.print("\tTimer= \t");
Serial.print(timer);
Serial.println();
}
Interface Code
virtual_pet_interface.ino
import processing.serial.*;
Serial port;
int caseState;
int duration = 5000;
int interval = 100;
int health = 100;
int happy =100;
int selectState;
int s;
int u;
int d;
long prevTime;
float heartc;
float happyc;
float counter;
PImage apple;
PImage milk;
PImage meat;
boolean feedMilk = false;
boolean feedApple = false;
boolean feedMeat = false;
void setup() {
size(600, 600);
// reference: https://processing.org/reference/libraries/serial/Serial.html
port = new Serial(this, Serial.list()[1], 9600); //establish bluetooth connection
println("Available serial ports:" + port);
smooth();
noStroke();
textSize(30);
apple = loadImage("img/apple.png");
milk = loadImage("img/milk.png");
meat = loadImage("img/meat.png");
}
void draw() {
background(255);
long start = millis();
counter = 128 + 127 * cos(0.25 * PI/interval * (interval - start));
heartc = map(counter, 0, 255, 255, 0);
happyc = map(counter, 0, 255, 255, 0);
if (start - prevTime >= duration) {
prevTime = start;
if (health <= 5) {
health = 0;
caseState = 3; //go to end game
} else {
health -= 2;
}
if (happy <= 8) {
happy = 0;
} else {
happy -= random(3, 8);
}
}
display();
emotion();
logic();
}
void reset() {
caseState = 0;
selectState = 0;
health = 100;
happy = 100;
}
void mouseClicked() {
if (selectState == 1) {
caseState = 1;
selectState =3;
}
if (selectState == 2) {
caseState = 2;
selectState = 5;
}
if (selectState == 4) {
caseState = 0;
selectState =0;
}
if (selectState == 9) {
caseState = 0;
selectState =0;
}
if (caseState == 3) reset();
if (feedMilk == true) {
if (health <= 95) {
health+=5;
} else {
health += 100-health;
}
}
if (feedApple == true) {
if (health <= 97) {
health+=3;
} else {
health += 100-health;
}
}
if (feedMeat == true) {
if (health <= 92) {
health+=8;
} else {
health += 100-health;
}
}
}
logic.ino
void logic() {
pushMatrix();
translate(10, 10);
select();
switch(caseState) {
case 0:
port.write('X');
if (mouseY >=63 && mouseY <=113) {
if (mouseX >=125 && mouseX <=245) selectState = 1;
if (mouseX >= 300 && mouseX <=430) selectState = 2;
} else {
selectState = 0;
}
break;
case 1:
port.write('X');
if (mouseY >=63 && mouseY <=113) {
if (mouseX >= 330 && mouseX <=480) selectState = 4;
} else {
selectState = 3;
}
break;
case 2:
if (mouseY >=63 && mouseY <=113) {
if (mouseX >= 330 && mouseX <=480) selectState = 9;
} else {
selectState = 5;
}
if (keyPressed) {
if (key == 's' || key == 'S') {
port.write('D');
s = #9EA9BA;
if(happy < 100){
happy++;
}else{
happy+=0;
}
} else {
s = 0;
}
if (key == 'u' || key == 'U') {
port.write('B');
u = #9EA9BA;
if(happy < 100){
happy++;
}else{
happy+=0;
}
} else {
u = 0;
}
if (key == 'd' || key == 'D') {
port.write('A');
d = #9EA9BA;
if(happy < 100){
happy++;
}else{
happy+=0;
}
} else {
d = 0;
}
}
break;
case 3:
text("I'M DEAD :'(", 200, 450);
selectState = 8;
happy = 0;
port.write('E');
break;
}
popMatrix();
}
void select() {
switch(selectState) {
case 0: //no hover
fill(0);
text("FEED", 150, 100);
text("REMOTE", 310, 100);
break;
case 1: // hover feed
fill(20);
rect(125, 63, 120, 50);
fill(255);
text("FEED", 150, 100);
fill(0);
text("REMOTE", 310, 100);
break;
case 2: //hover remote
fill(20);
rect(300, 63, 135, 50);
fill(0);
text("FEED", 150, 100);
fill(255);
text("REMOTE", 310, 100);
break;
case 3: //feed no selection state
fill(0);
image(milk, 100, 50);
//rect(260, 50, 60, 100); position rect
image(apple, 180, 60);
image(meat, 265, 60);
text("GO BACK", 340, 100);
if (mouseX >= 100 && mouseX <= 160 && mouseY >= 50 && mouseY <= 150) {
if (mousePressed == true) {
feedMilk = true;
if (health <= 95) {
fill(#3CA9EA);
text("Health + 5", mouseX-40, mouseY+60);
} else {
if (health == 100) {
fill(#76E87D);
text("I'M FULL!", mouseX-40, mouseY+60);
} else {
fill(#3CA9EA);
text("Health + " + (100-health), mouseX-40, mouseY+60);
}
}
} else {
text("/MILK/ Health+5", mouseX-40, mouseY+60);
feedMilk = false;
}
}
if (mouseX >= 180 && mouseX <= 240 && mouseY >= 50 && mouseY <= 150) {
if (mousePressed == true) {
feedApple = true;
if (health <= 97) {
fill(#3CA9EA);
text("Health + 3", mouseX-40, mouseY+60);
} else {
if (health == 100) {
fill(#76E87D);
text("I'M FULL!", mouseX-40, mouseY+60);
} else {
fill(#3CA9EA);
text("Health + " + (100-health), mouseX-40, mouseY+60);
}
}
} else {
text("/APPLE/ Health+3", mouseX-40, mouseY+60);
feedApple = false;
}
}
if (mouseX >= 260 && mouseX <= 320 && mouseY >= 50 && mouseY <= 150) {
if (mousePressed == true) {
feedMeat = true;
if (health <= 92) {
fill(#3CA9EA);
text("Health + 8", mouseX-40, mouseY+60);
} else {
if (health == 100) {
fill(#76E87D);
text("I'M FULL!", mouseX-40, mouseY+60);
} else {
fill(#3CA9EA);
text("Health + " + (100-health), mouseX-40, mouseY+60);
}
}
} else {
text("/MEAT/ Health+8", mouseX-40, mouseY+60);
feedMeat = false;
}
}
break;
case 4: // feed selected go back
fill(20);
rect(330, 63, 150, 50);
image(milk, 100, 50);
image(apple, 180, 60);
image(meat, 265, 60);
fill(255);
text("GO BACK", 340, 100);
break;
case 5: //remote no select
fill(s);
text("SIT", 50, 100);
fill(u);
text("UP", 150, 100);
fill(d);
text("DOWN", 230, 100);
fill(0);
text("GO BACK", 340, 100);
break;
case 8:
fill(0);
text("ClICK TO RESTART",150,100);
break;
case 9: //remote selected go back
fill(0);
text("SIT", 50, 100);
//rect(260, 50, 60, 100); position rect
text("UP", 150, 100);
text("DOWN", 230, 100);
fill(20);
rect(330, 63, 150, 50);
fill(255);
text("GO BACK", 340, 100);
break;
}
}
display.ino
void display() {
pushMatrix();
translate(width/2, height/2);
fill(0);
if (health !=0) {
if (happy>=40) {
fill(#FFF5A3); //yellow
} else {
fill(#FF7372);// red
}
} else {
fill(#9EA9BA);//dead
}
ellipseMode(CENTER);
ellipse(0, 0, 200, 200);
popMatrix();
pushMatrix();
translate(40, 500);
beginShape();
if (health <= 60) {
fill(heartc);
text("HUNGRY!", 200, 450);
} else {
fill(0);
text("", 200, 450);
}
vertex(50, 15);
bezierVertex(50, -5, 90, 5, 50, 40);
vertex(50, 15);
bezierVertex(50, -5, 10, 5, 50, 40);
endShape();
text(health, 100, 33);
if (happy <= 40) {
fill(happyc);
} else {
fill(0);
}
text(":-) " + happy, 360, 33);
popMatrix();
}
void emotion() {
fill(255);
ellipse(260, 280, 20, 20);
ellipse(330, 280, 20, 20);
if (happy >= 40) {
pushMatrix();
translate(50, 20);
bezier(200, 295, 200, 370, 300, 370, 300, 295); //smiley face
line(150, 295, 350, 295);
popMatrix();
} else {
if (health !=0) {
pushMatrix();
translate(85, 70);
bezier(190, 260, 210, 240, 225, 240, 240, 260);
fill(#FF7372); //mad color
translate(0, 5);
bezier(180, 260, 210, 240, 225, 240, 260, 260);//sad face
popMatrix();
} else {
fill(#9EA9BA); //dead color
}
}
}