Concept:
The idea was to create a bot that can be controlled with hand movement using machine learning with the P5.ml library. The P5 sketch has an in-car design with the steering wheel and the Arduino comprises 4dc motors, two ultrasonic sensors, and some LEDs.
Interactive Design:
For user interactivity, I decided to use the P5.js. The computer video webcam helps the machine learning library to detect the movement of the user’s hand and map these movements to the size of the canvas. This information is used to control the steering wheel, which controls the Arduino.
Arduino Code:
For the Arduino aspect, the motors are controlled using switch cases. The Arduino receives cases from the P5 sketch and based on those cases, the Arduino knows how to control the 4 DC motors. The Arduino uses the ultrasonic sensors to detect obstacles and sends this information to the P5 sketch to be viewed by the user.
#include <Servo.h>//include the Servo library/ #include <time.h>//include the time library for seeding random number Servo myservo;//creating servo object const int ain1Pin = 3;//setting pins for motor for left side which is connected in parallel const int ain2Pin = 4; const int pwmAPin = 5; const int bin1Pin = 8;//setting pinf for motor for right side const int bin2Pin = 7; const int pwmBPin = 6; const int trigPin1 = 11;//setting pins for front ultrasonic sensor const int echoPin1= A0;//I used A0 pin cause I didnt have space...Please pardon me const int trigPin2 = 10;//setting pins for back ultrasonic sensor const int echoPin2 = A1;//here too const int warningbuzzer=12;//setting the buzzer to 12 //servo motor const int headmovePin=9;//setting the servo motor pin to 9 int wallstop=0;//initializing the obstacle detection unsigned long previousMillis = 0;//setting millis to 0 const long interval = 5000;//setting interval for the millis void setup() { myservo.attach(headmovePin);//pass the servo pin to the servo library randomSeed(time(NULL));//seed random number using the current time pinMode(ain1Pin, OUTPUT);//setting the pins as output and input pinMode(2, OUTPUT);//light checker pinMode(ain2Pin, OUTPUT);//motor pin pinMode(pwmAPin, OUTPUT); // not needed really pinMode(bin1Pin, OUTPUT); pinMode(bin2Pin, OUTPUT); pinMode(pwmBPin, OUTPUT); // not needed really // Start serial communication so we can send data // over the USB connection to our p5js sketch pinMode(trigPin1, OUTPUT); pinMode(echoPin1, INPUT); pinMode(trigPin2, OUTPUT); pinMode(echoPin2, INPUT); pinMode(warningbuzzer, OUTPUT); Serial.begin(9600);//setting the serial band while (!Serial.available()) { digitalWrite(LED_BUILTIN, HIGH); // on/blink while waiting for serial data Serial.println("0,0"); // send a starting message delay(300); // wait 1/3 second digitalWrite(LED_BUILTIN, LOW); delay(1000);//delay for a second } } void loop() { wallstop=crushstop(); //When the serial is detected do this while (Serial.available()) {//while the serial connection exists digitalWrite(LED_BUILTIN, HIGH); // led on while receiving data wallstop=crushstop();//update the wallstop from the crush function unsigned long currentMillis = millis();//set current millis if (currentMillis - previousMillis >= interval) { // save the last time you blinked the LED previousMillis = currentMillis; movehead();//if the interval is reached, move the head } int movement = Serial.parseInt();//get the movement case from p5 if (Serial.read() == '\n') {//when we read a new line, switch(movement){//execute the following commmands based on the case case 0://no movement analogWrite(pwmAPin, 0); digitalWrite(ain1Pin, HIGH); digitalWrite(ain2Pin, LOW); analogWrite(pwmBPin, 0); digitalWrite(bin1Pin, LOW); digitalWrite(bin2Pin, HIGH); break; case 1://forward analogWrite(pwmAPin, 255); digitalWrite(ain1Pin, LOW); digitalWrite(ain2Pin, HIGH); analogWrite(pwmBPin, 255); digitalWrite(bin1Pin, HIGH); digitalWrite(bin2Pin, LOW); break; case 2://reverse analogWrite(pwmAPin, 255); digitalWrite(ain1Pin, HIGH); digitalWrite(ain2Pin, LOW); analogWrite(pwmBPin, 255); digitalWrite(bin1Pin, LOW); digitalWrite(bin2Pin, HIGH); break; case 3://right front analogWrite(pwmBPin, 255); digitalWrite(bin1Pin, HIGH); digitalWrite(bin2Pin, LOW); break; case 4://left front analogWrite(pwmAPin, 255); digitalWrite(ain1Pin, LOW); digitalWrite(ain2Pin, HIGH); break; case 5://right back analogWrite(pwmBPin, 255); digitalWrite(bin1Pin, LOW); digitalWrite(bin2Pin, HIGH); break; case 6://left back analogWrite(pwmAPin, 255); digitalWrite(ain1Pin, HIGH); digitalWrite(ain2Pin, LOW); break; default://if not case is gotten within our expected range, stop analogWrite(pwmAPin, 0); digitalWrite(ain1Pin, HIGH); digitalWrite(ain2Pin, LOW); analogWrite(pwmBPin, 0); digitalWrite(bin1Pin, LOW); digitalWrite(bin2Pin, HIGH); break; } delay(5); Serial.println(wallstop);//send the detection to p5 } } } int crushstop(){//this function returns 1 when an obstacle is ahead and 2 when an obstacle is behind int wallstop=0;//initialize wallstop digitalWrite(trigPin1, LOW); delayMicroseconds(2); //basically shooting beems and using the time it takes to bounce back to calculate distance digitalWrite(trigPin1, HIGH); delayMicroseconds(10); digitalWrite(trigPin1, LOW); // Time it takes for the pulse to travel back from the object long int duration1 = pulseIn(echoPin1, HIGH); // Universal conversion of time into distance in cm int distance1 = duration1 * 0.034 / 2;//divided by two beause its a two way thing digitalWrite(trigPin2, LOW); delayMicroseconds(2); digitalWrite(trigPin2, HIGH); delayMicroseconds(10); digitalWrite(trigPin2, LOW); // Time it takes for the pulse to travel back from the object long int duration2 = pulseIn(echoPin2, HIGH); // Universal conversion of time into distance in cm int distance2 = duration2 * 0.034 / 2; if(distance1<5){//if collision is detected behind, send 2 wallstop=2; tone(warningbuzzer,2000);//play the tone delay(5); } else if(distance2<5){ wallstop=1;//if collision is detected infront, send 1 tone(warningbuzzer,2000);//play the tone delay(5); } else{ wallstop=0; noTone(warningbuzzer);//if nothing is detected dont play a tone } return wallstop;//return this info } void movehead(){ myservo.write(random(0, 180));//move the head to a random number between 0 and 180 delay(15); }
P5.js:
For this part, the P5.js sketch receives the user hand positions info from the P5.ml library and maps it to get its corresponding points on the canvas. After, these values are averaged and the average-X value is used to control the steer’s left or right turn. The average-Y value is used to control the forward and backward movement. Based on this cases are developed and sent to the Arduino for execution of tasks.
The P5 sketch is divided into five parts
Serial connection:
This part is responsible for connecting the Arduino to the P5 sketch and since its not my code I will not post it
Intro Page:
The next part is the intro page. Is welcomes the user into the project and also gives some info to the user about how to use the vehicle. I made the background picture myself using photoshop and then I added some buttons and sounds
class ipage{ constructor(IB,S1,S2,F1,F2){//receive all needed variables as in images and sound this.BG=IB;//the background this.S1=S1;//sound this.playbool=false;//boolean to start game this.helpbool=false;//boolean to open help page this.F2=F2//font } show(){//this shows the designs in the page this.BG.resize(windowWidth,windowHeight);//resize the introimage image(this.BG,0,0);//displaying background textAlign(CENTER); this.createhelp();//calling the help function for the help page this.playbox();//calling the playbox button function if(this.helpbool){//if the helpbool is true display the help page this.helppage(); } return this.playbool;//this returns true if the play button is pressed } playbox(){//this function displays the start button rectMode(CORNER); fill(150,180,40); if(mouseX>windowWidth*0.1&&mouseX<(windowWidth*0.1)+350&&mouseY>windowHeight*0.8&&mouseY<(windowHeight*0.8)+150){ fill(150,0,0);//creating the hover effect } if(mouseX>windowWidth*0.1&&mouseX<(windowWidth*0.1)+350&&mouseY>windowHeight*0.8&&mouseY<(windowHeight*0.8)+150&&mouseIsPressed&&!this.helpbool){ S2.pause();// the play is pressed, play pause the background music this.S1.play();//playing the start button pressed sound this.playbool=true;//sets the playboolean to true } rect(windowWidth*0.1,windowHeight*0.8,350,150,60);//drawing the start button fill(0); textFont(F1); textSize(90); textAlign(CENTER); text("Start",windowWidth*0.1+180,windowHeight*0.8+110); } createhelp(){ fill(100); if(dist(mouseX,mouseY,windowWidth*0.9,windowHeight*0.1)<=30){ fill(60);//create hover effect } if(dist(mouseX,mouseY,windowWidth*0.9,windowHeight*0.1)<=30&&mouseIsPressed){ this.helpbool=true;//set boolean to open help page } circle(windowWidth*0.9,windowHeight*0.1,60);//these following code just creates the help button fill(255,255,0); textSize(30); textFont(NORMAL); text("?",windowWidth*0.9,windowHeight*0.1+10); } helppage(){ fill(100); rect(50,50,windowWidth*0.95,windowHeight*0.9,50); textFont(this.F2); fill(255); textSize(80); text('WELCOME',windowWidth*0.5,windowHeight*0.25); textSize(30); text('Move your Hand Up and Down while pressing the mouse to control the gear',windowWidth*0.5,windowHeight*0.4); text('Move your Hand left and right to control the steer',windowWidth*0.5,windowHeight*0.5); text('Click on the Q key to go to the Homepage',windowWidth*0.5,windowHeight*0.6); text('Press the space bar to connect to Arduino',windowWidth*0.5,windowHeight*0.7); text('GOOD LUCK!',windowWidth*0.5,windowHeight*0.8); fill(100); if(dist(mouseX,mouseY,windowWidth*0.5,windowHeight*0.9)<=30){ fill(60);//create hover effect } if(dist(mouseX,mouseY,windowWidth*0.5,windowHeight*0.9)<=30&&mouseIsPressed){ this.helpbool=false;//set boolean to close help page } circle(windowWidth*0.5,windowHeight*0.9,60);//these following code just creates the ok button fill(255,255,0); textSize(30); textFont(NORMAL); text("OK",windowWidth*0.5,windowHeight*0.9+10); } }
Help Page:
This page is just to give the user info about the game and wish them luck as they embark on the Journey.
the code is found in the intro class and the page is controlled with the use of Boolean variables.
Dashboard:
Coming into the main page, the Dashboard shows the gearbox and the collision screen that alerts the user when the user is close to an obstacle.
class dash{//this class prints the dashboard, the gear and the detection screen constructor(dashY){ this.Y=dashY;//the Y coordinate to which the dashboard is drawn this.movementFB=0;//the front and back movement counter this.gearY=0;//this controls the gearmovement } showdash(){ push();//creating the dashboard strokeWeight(5)//set stroke weight to 5 fill(193, 154, 107);//fill with brown beginShape();//we draw the dashboard curveVertex(0,this.Y); curveVertex(0,this.Y); curveVertex(windowWidth/2,this.Y-50);//creating the curve look curveVertex(windowWidth,this.Y); curveVertex(windowWidth,windowHeight); curveVertex(0,windowHeight); curveVertex(0,this.Y); curveVertex(0,this.Y); endShape(); pop(); } showgear(gearY){//this function shows the gear this.gearY=gearY;//capies value of the gearY from handpose rectMode(CENTER);//set rectange mode textSize(50); fill(0);//fill with black rect(windowWidth*0.5,this.Y+110,150,300,50); if(mouseIsPressed){//if the mouse is pressed means gear is being controlled if(this.gearY<windowHeight*0.35){ this.movementFB=1;//set gear to drive } else if(this.gearY>windowHeight*0.5){ this.movementFB=2;//set gear to reverse } else{ this.movementFB=0;//set gear to P } } textAlign(CENTER);//align text to center textSize(50); fill(0,128,0);//fill with green switch(this.movementFB){ case 0: text('P',windowWidth*0.5,this.Y+110);//p for parking break; case 1: text('D',windowWidth*0.5,this.Y+110);//D for drive break; case 2: text('R',windowWidth*0.5,this.Y+110);//R for reverse break; } textAlign(LEFT); textSize(12); return this.movementFB;//return the gear movement info } showScreen(wallstop){//this shows the detection rectMode(CENTER); textSize(50); textAlign(CENTER); fill(0); rect(windowWidth*0.75,this.Y+110,500,300,50);//create screen fill(128,0,0);//fill the text with red if(wallstop==1){//if the front ultrasonic sensor is the one sensing text('OBJECT',windowWidth*0.75,this.Y+50); text('DETECTED',windowWidth*0.75,this.Y+130); text('AHEAD',windowWidth*0.75,this.Y+210); } else if(wallstop==2){//if the back ultrasonic is the one sensing text('OBJECT',windowWidth*0.75,this.Y+50); text('DETECTED',windowWidth*0.75,this.Y+130); text('BEHIND',windowWidth*0.75,this.Y+210); } else{ fill(0,128,0);//fill the text with green text('No',windowWidth*0.75,this.Y+50); text('OBJECT',windowWidth*0.75,this.Y+130); text('DETECTED',windowWidth*0.75,this.Y+210); } textAlign(LEFT); textSize(12); } }
In order to show the gear box info it gets info from the poseNet calculations done in the sketch and for the collision screen it uses info from the ultrasonic sensors.
The Steering wheel:
Though also part of the Dashboard, this is a separate class because it controls the left and right movement using info from the other parts. That is, is uses info from the gear box and collision screen. It is not a picture downloaded from somewhere but a hand coded diagram.
class Steer{//this class creates the steer and controls the car movement front and back constructor(ctr,F2,X=windowWidth/3,Y=windowHeight/2){//takes the steerX,the font and the position for the steer to be placed this.angle=0;//equate the angle to 0 this.X=X;//set the X and Y for the placement of the steering wheel this.Y=Y; this.ctr=ctr; this.movementLR=0;//create this Left right variable to store the movement this.F2=F2; } show(ctr){//takes steer x this.ctr=ctr;//update the ctr with steerX push();//designing the steering using stack so that it can be rotated entirely at once translate(this.X,this.Y);//making the orijin these rotate(this.angle);//causes the rotation noFill(0); strokeWeight(80); rectMode(CENTER); circle(0,0,500); fill(0); stroke(0); strokeWeight(12); beginShape(); curveVertex(-230,-80); curveVertex(-230,-80); curveVertex(0,-120); curveVertex(230,-80); curveVertex(230,0); curveVertex(80,70); curveVertex(40,230); curveVertex(-40,230); curveVertex(-80,70); curveVertex(-230,0); curveVertex(-230,-80); curveVertex(-230,-80); fill(0); endShape(); fill(100); noStroke() textSize(50) textAlign(CENTER); textFont(this.F2); text('REKAS',0,0);//steering wheel/car brand pop(); if(this.ctr>0&&this.ctr<windowWidth){//while the value is within our range this.angle=map(this.ctr,0,windowWidth,-PI/2,PI/2);//update angle based on this } } steerTurn(movementFB,wallstop){//this function controls the turning of the steer if(this.angle<-PI/7.5&&movementFB==1&&wallstop!=1){ this.movementLR=4;//front left } else if(this.angle<-PI/7.5&&movementFB==2&&wallstop!=2){ this.movementLR=6;//back left } else if(this.angle>=-PI/7.5&&this.angle<-PI/8){ this.movementLR=0;//to prevent bugs } else if(this.angle>=-PI/7&&this.angle<PI/8&&movementFB==1&&wallstop!=1){ this.movementLR=1;//move straight ahead } else if(this.angle>=-PI/7&&this.angle<PI/7&&movementFB==2&&wallstop!=2){ this.movementLR=2;//reverse } else if(this.angle>=PI/7&&this.angle<PI/5){ this.movementLR=0;//yeah } else if(this.angle>=PI/5&&movementFB==1&&wallstop!=1){ this.movementLR=3;//front right } else if(this.angle>=PI/5&&movementFB==2&&wallstop!=2){ this.movementLR=5;//back right } else{ this.movementLR=0;//dont move } return this.movementLR; } }
Sketch:
This is the last part that kind of sums everything up. This is where all the other components come together to produce this artwork. This is also where the poseNet functions and full screen Functions are declared.
let mySteer;//variable going to store steer object let myDash;//gonna store the dashboard object let handpose;//gonna store posenet object let video;//gonna store video object let predictions = [];//gonna store set of predictions from posenet let steerX=300;//gonna store the average x coordinate of the hand let gearY=250;//gonna store the average y coordinate of the hand let movementLR=0;//gonnna store the left and right movement let movementFB=0;//gonna store the front and back movement let wallstop=0;//gonna store the obstacle detection let steercontrol=0;//gonna control steer to make it feel smooth let IntroBackground;//intropage background let S1;//sound 1(button) let S2;//sound 2 let F1; let F2; let introp;//gonna store intropage object let introbool=true;//going to control the intropage display let gamebool=false;//going to control the mainpage display function preload(){ //in this preload function we will load all the uploads we need before we even start the game. IntroBackground=loadImage("intro1.jpg");//this is for the background S1=loadSound("button.mp3");//these sets are for the sounds S2=loadSound("msound.mp3"); F1=loadFont("font1.ttf");//these sets are for the fonts F2=loadFont("font2.ttf"); } function setup() { createCanvas(windowWidth, windowHeight); video = createCapture(VIDEO);//capture video using camera video.size(width, height);//set the size of the video to that of the screen steercontrol=windowWidth/2//set the steercontrol for smoothness handpose = ml5.handpose(video);//get posenet from the video feed using the ml library // This sets up an event that fills the global variable "predictions" // with an array every time new hand poses are detected handpose.on("predict", results => { predictions = results; }); // Hide the video element, and just show the canvas video.hide(); introp=new ipage(IntroBackground,S1,S2,F1,F2);//create intropage object mySteer=new Steer(steerX,F2,windowWidth*0.5,windowHeight*0.85);//create steer object myDash=new dash(windowHeight*0.85);//create dashboard object S2.loop();//start playing the sound but with loop property } function draw() { if(introbool){//if the introbool is true show intropage gamebool=introp.show();//update gamebool from intro.show function if(gamebool){introbool=false;}//if the gamebool is true,set intro to false } else{ background(50,150,255); myDash.showdash()//show the dash drawKeypoints();//call this function for geting info from the video hand detection movementFB=myDash.showgear(gearY);//update the front back movement from the showgear function movementLR=mySteer.steerTurn(movementFB,wallstop);//update the leftright movement from the steerturn function if(steerX>0&&steerX<windowWidth){//if the steerX is within the range we want mySteer.show(steerX);//show the steer with this value steercontrol=steerX;//update the steercontrol incase we stop getting data } else{//if the steerX is not in our range, if(steercontrol<windowWidth/2-5){steercontrol+=10;}//using our steercontrol,slowly move the steer to the center else if(steercontrol>windowWidth/2+5){steercontrol-=10;} mySteer.show(steercontrol); } myDash.showScreen(wallstop);//show the screen with the wallstop getten form the Arduino } } function keyPressed() {//if spaebar is pressed connect to arduino if (key == " ") { // important to have in order to start the serial connection!! setUpSerial();//connect to arduino } } function readSerial(data) { if (data!=null){//if the data is not null ////////////////////////////////// //READ FROM ARDUINO HERE (handshake) ////////////////////////////////// wallstop= int(trim(data)); ////////////////////////////////// //SEND TO ARDUINO HERE (handshake) ////////////////////////////////// let sendToArduino = movementLR + "\n"; writeSerial(sendToArduino); } } function drawKeypoints() { let totalX=0;//set variable to store the sum of the x coordinates of all the predictions let totalY=0;//same for y let avgctr=0;//set a counter to count the predictions let len=0;//I dont use len here but i was experimenting somthing for (let i = 0; i < predictions.length; i += 1) { const prediction = predictions[i]; len=predictions.lenght*prediction.landmarks.length; for (let j = 0; j < prediction.landmarks.length; j += 1) { const keypoint = prediction.landmarks[j]; totalX+=windowWidth-map(keypoint[0],0,video.width,0,windowWidth);//map the points to our window size and sum it totalY+=map(keypoint[1],0,video.height,0,windowHeight); avgctr++;//increase this too } } steerX=totalX/avgctr;//update steerX with the average of X gearY=totalY/avgctr;//same for Y but with average of Y } function windowResized() { resizeCanvas(windowWidth, windowHeight);//resize the canvas to go to fullpage } function keyTyped() { // $$$ For some reason on Chrome/Mac you may have to press f twice to toggle. Works correctly on Firefox/Mac if (key === 'f') { toggleFullscreen();//if f is pressed, show fullpage } if(key==='q'){//if q is pressed go to h=intro page if(introbool==false){ introbool=true; introp.playbool=false; S2.play(); } } // uncomment to prevent any default behavior // return false; } // Toggle fullscreen state. Must be called in response // to a user event (i.e. keyboard, mouse click) function toggleFullscreen() { let fs = fullscreen(); // Get the current state fullscreen(!fs); // Flip it! }
Rekas Bot:
My inspiration for this project comes from my zeal to use the ml library in p5 and I had fun developing this.
Test Video:
Arduino Circuit Diagram:
Schematic Diagram:
Clarification on the Schematic motor drawing:
Future Improvements:
I Tried to implement Bluetooth but I was not able to because it kept on failing even though I had connected and set up everything right so I wish to be able to find a way to make this connection more reliable and also I plan to make the machine learning more reliable in future.
IM Showcase:
So just before the showcase started, I added one line of code which made the bot move randomly as though it had life.
if(introbool){//if the introbool is true show intropage gamebool=introp.show();//update gamebool from intro.show function movementLR=int(random(0,6));//do random stuff if(gamebool){introbool=false;}//if the gamebool is true,set intro to false }
I am really proud of this last decision.
and when you want to drive it, it stops the random movements.
Thank you.