Front-End Code: p5.js
After getting the Arduino up and connected to the cloud, we can start working on the front-end interface in p5, which is a JavaScript library for interactive experiences. Firstly be sure to include the following line in your html file to import the p5.js library:
<script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.min.js"></script>
Variable Declarations
let IO_USERNAME = "YOUR AIO USERNAME";
let IO_KEY = "YOUR AIO KEY";
let allfeeds;
let laser_value;
let xServoMax = 180;
let yServoMax = 72;
let xservo_value;
let xServoAngle = 0;
let yservo_value;
let yservo_updated;
let yServoAngle = 0;
let lastCheckedTime = 0;
let pwrImg;
let pwrHover = false;
let pwrX = 150;
let pwrY = 15;
let pwrD = 100;
let joystick_ballImg;
let joystick_ballX = 146;
let joystick_ballY = 276;
let joystick_ballD = 110;
let joystick_backImg;
let joystick_backX = 50;
let joystick_backY = 180;
let joystick_backD = 300;
let joystickHover = false;
let joystickPressed = false;
let joystick_xoffset;
let joystick_yoffset;
let joystick_dragMinX = 110;
let joystick_dragMaxX = 190;
let joystick_dragMinY = 210;
let joystick_dragMaxY = 310;
let textX = 150;
let textY = 500;
let errorBool = false;
There are quite a few lines here, but all we're doing is declaring and initializing all of the global variables we'll be using later on. Some of the notable hard-coded values here are the horizontal and vertical servo max, which indicate how much the servos will actually be moving. Both servos can hypothetically move from 0 to 180 degrees, but from my testing I determined a max of 180 for horizontal and 72 for vertical to be ideal.
The rest of these values are the locations for which we'll place the visual elements on the canvas, stored in pixels, and the distance that we want to constrain the movement of the joystick.
Preload and Setup Functions
function preload(){
pwrImg = loadImage('YOUR POWER IMAGE FILE PATH');
joystick_ballImg = loadImage('YOUR BALL IMAGE FILE PATH');
joystick_backImg = loadImage('YOUR JOYSTICK IMAGE FILE PATH');}
function setup() {
createCanvas(400, 600);
frameRate(30);
noStroke();
pwrImg.resize(pwrD, pwrD);
joystick_backImg.resize(joystick_backD, joystick_backD);
joystick_ballImg.resize(joystick_ballD, joystick_ballD);
getData();}
In preload, we load in the images that will make up our control interface. These can just be native p5 circles, but I decided to lightly design my interface in Photoshop. If anyone is interested in using these images, please feel free to contact me.
In setup, we create the p5 canvas, which I decided to match the screen size for my phone. Then we set the frame rate to be 30. This is an important step, because Adafruit IO limits server requests to 30 per minute so we want to limit the amount of times the data is refreshing. This throttling creates a difficult challenge which I'll further address later on.
Beyond that, we just need to resize the interface images to the desired dimensions. In setup I also call the getData function once to populate the feed values right off the bat.
Draw Function
function draw() {
clear();
checkButtons();
//update every second
if(millis() > lastCheckedTime+1000){
getData();
lastCheckedTime = millis();}
if(!allfeeds){
return}
//populate values from IO feed
laser_value = allfeeds.feeds[0].last_value;
xservo_value = allfeeds.feeds[1].last_value;
yservo_value = allfeeds.feeds[2].last_value;
//set color for power button
if(laser_value == "OFF"){
tint(255, 0, 0);}
else{
tint(0, 255, 0);}
//draw images for buttons
image(pwrImg, pwrX, pwrY);
noTint();
image(joystick_backImg, joystick_backX, joystick_backY);
image(joystick_ballImg, joystick_ballX, joystick_ballY);
//draw error text
if(errorBool){
fill(255, 0, 0);
textSize(14);
text("too many requests", textX, textY);}}
At the start of each loop, we want to clear the canvas to make sure the drag events operate properly. Then we call the checkButtons function to see if we're hovering over anything. Next, we want to update the data values every second to make sure our data still matches what's on the cloud. We also have a quick check here which will end the loop if the data hasn't populated yet.
After receiving the data through getData, we move that data into the appropriate variables of the laser and servos. Next we set the color of the power button based on the status of the laser - naturally, green is on and red is off. We draw all the interface elements on the screen.
Finally, I added error text which will show up if the data requests are being throttled (which happens quite frequently in testing at a max of 30 per seconds).
Mouse Interactions
function buttonHover(x, y, width, height){
if(mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height){
return true;}
else{return false;}}
function checkButtons(){
//check if mouse is hovering over any buttons
pwrHover = buttonHover(pwrX, pwrY, pwrD, pwrD);
joystickHover = buttonHover(joystick_ballX, joystick_ballY, joystick_ballD, joystick_ballD);}
function mousePressed(){
let data;
//process laser logic
if(pwrHover){
if(laser_value == "ON"){
data = {"value":"OFF"};}
else{
data = {"value":"ON"};}
postData(data, "laser");}
joystickPressed = joystickHover;
joystick_xoffset = mouseX - joystick_ballX;
joystick_yoffset = mouseY - joystick_ballY;}
function mouseDragged(){
//drag interaction for joystick
let xOffset = mouseX - joystick_xoffset;
let yOffset = mouseY - joystick_yoffset;
if(joystickPressed){
//constrain joystick movement within x radius
if(xOffset < joystick_dragMinX){
xOffset = joystick_dragMinX;}
else if(xOffset > joystick_dragMaxX){
xOffset = joystick_dragMaxX;}
//constrain joystick movement within y radius
if(yOffset < joystick_dragMinY){
yOffset = joystick_dragMinY;
}
else if(yOffset > joystick_dragMaxY){
yOffset = joystick_dragMaxY;}
joystick_ballX = xOffset;
joystick_ballY = yOffset;
//translate into 0 - 180 range with intervals of 4
xServoAngle = map(joystick_ballX, joystick_dragMinX, joystick_dragMaxX, xServoMax, 0);
xServoAngle = Math.ceil(xServoAngle/4) * 4;
//translate into 0 - 72 range with intervals of 3
yServoAngle = map(joystick_ballY, joystick_dragMinY, joystick_dragMaxY, yServoMax, 0);
yServoAngle = Math.ceil(yServoAngle/3) * 3;}}
function mouseReleased(){
joystickPressed = false;
//send values to servo
if(xServoAngle != xservo_value){
console.log("X Servo Angle: "+xServoAngle);
postData({"value":xServoAngle}, "xservo");}
if(yServoAngle != yservo_value){
console.log("Y Servo Angle: "+yServoAngle);
postData({"value":yServoAngle}, "yservo");}}
The bulk of the front-end code revolves around translating mouse movements on the screen into actions to send to the Arduino. Firstly, button hover checks if the mouse is currently over either of the interface elements (power button or joystick).
Next, we define, mousePressed, abuilt-in function called any time the mouse is clicked. In there we have some logic to check if the mouse is pressed while over the power button - if so, we send data to the cloud telling it to switch the status of the laser.
The following logic in mousePressed and mouseDragged serves to track the user's movement while clicking on the joystick and also constrain that movement within a certain radius. All of that logic is processed into variables called joystick_ballX and joystick_ballY. These variables essentially store how far the joystick was just moved from the center.
We then map these variables into our defined range of servo movement. This means we take the furthest values that the user can move the joystick and translate it into a range understandable by the servo. I also added a bit more logic to send these values as intervals of 3 or 4 so the movement is more discrete and won't overwhelm the cloud with requests.
Finally, the mouse being released is what prompts us to send the data to the cloud. Originally I was sending this request as the mouse was dragged, but the server limit became overloaded very quickly. So now when the user stops dragging the joystick, the script will do a check to see if the current cloud servo value is the same and if not, it will post the servo data to the appropriate feed.
Touch Interactions
function touchingButton(touch, x, y, d){
if(touch.x >= x && touch.x <= x + d && touch.y >= y && touch.y <= y + d){
return true;}
else{return false;}}
function touchStarted(){
var touch = touches[0]
if (touchingButton(touch, pwrX, pwrY, pwrD)){
if(laser_value == "ON"){
data = {"value":"OFF"};}
else{data = {"value":"ON"};}
postData(data, "laser");}
if(touchingButton(touch, joystick_ballX, joystick_ballY, joystick_ballD){ joystickPressed = true;
joystick_xoffset = touch.x - joystick_ballX;
joystick_yoffset = touch.y - joystick_ballY;
console.log("joystick pressed");}}
function touchMoved(){
var touch = touches[0];
//drag interaction for joystick
let xOffset = touch.x - joystick_xoffset;
let yOffset = touch.y - joystick_yoffset;
if(joystickPressed){
//constrain joystick movement within x radius
if(xOffset < joystick_dragMinX){
xOffset = joystick_dragMinX;}
else if(xOffset > joystick_dragMaxX){
xOffset = joystick_dragMaxX;}
//constrain joystick movement within x radius
if(yOffset < joystick_dragMinY){
yOffset = joystick_dragMinY;}
else if(yOffset > joystick_dragMaxY)
{yOffset = joystick_dragMaxY;}
joystick_ballX = xOffset;
joystick_ballY = yOffset;
//translate into 0 - 130 range with intervals of 3
xServoAngle = map(joystick_ballX, joystick_dragMinX, joystick_dragMaxX, xServoMax, 0);
xServoAngle = Math.ceil(xServoAngle/3) * 3;
yServoAngle = map(joystick_ballY, joystick_dragMinY, joystick_dragMaxY, yServoMax, 0);
yServoAngle = Math.ceil(yServoAngle/3) * 3;}}
function touchEnded(){ joystickPressed = false;
//send values to servo
if(xServoAngle != xservo_value){
console.log("X Servo Angle: "+xServoAngle);
postData({"value":xServoAngle}, "xservo");}
if(yServoAngle != yservo_value){
console.log("Y Servo Angle: "+yServoAngle);
postData({"value":yServoAngle}, "yservo");}}
Next, we make the joystick movement touch responsive with built-in functions touchStarted, touchMoved, and touchEnded.
Network Communication
function getData(){
let feedurl = "https://io.adafruit.com/api/v2/YOUR USERNAME/YOUR FEED URL?x-aio-key="+IO_KEY;
httpGet(feedurl, false, function(response){
allfeeds = JSON.parse(response);
});}
function postData(data, feedKey){
let url = "https://io.adafruit.com/api/v2/YOUR USERNAME/feeds/"+feedKey+"/data?x-aio-key="+IO_KEY;
httpPost(url, data, function(data){}, function(response){
if(response == "Error: [object ReadableStream]"){
errorBool = true;
}
});
}
Last but certainly not least, we have the functions getData and postData to communicate with the cloud. getData sends request to the server and comes back with a string of all the values in the cat toy group I created on Adafruit. This data is converted into JSON and stored in the variable allfeeds.
postData sends the desired data as a request to whatever feed is passed into the function, in this case xServo or yServo. If it returns with an error, the variable errorBool will be set to true which indicates to the draw function to print the text "too many requests." Again, depending on the user's joystick movement this does happen occasionally.