Things Used In This Project
- 1 x Makeblock XY-Plotter Robot Kit V2.0 (No Electronics). This include all mechanical parts, two step motors (control position of pen), 1 servo motor (control pen up/down) and four touch sensors (detect the top, bottom, left and right border reach).
- 1 x Arduino Uno or Mega
- 1 x PHPoC Shield R2 or PHPoC WiFi Shield R2
- 2 x Stepper Motor Controller PES-2605
- Jumper wires
How It Works
When a finger touches to the drawing area in webpage, the XY coordinate of touching point is sent to Arduino. After scaling the coordinate, Arduino will move two step motors to locate the pen to that coordinate. During moving period, Arduino continuously send trajectory of the pen to web app, the web app draw the trajectory on the canvas.
Source Code
Arduino Code
This is arduino code, which run in infinite loop to:
- Receiving the command from webpage and do task according to the command.
- CMD_MOVE: move pen to a position by controlling two step motor
- CMD_PEN_UP: raise the pen by changing the angle of servo motor
- CMD_PEN_DOWN: lower the pen by changing the angle of servo motor
- Continuously read the current position of pen and send to webpage
Code:
#include <Phpoc.h> #include <PhpocExpansion.h> #include <Servo.h> #define MAX_X 55550 // unit is step #define MAX_Y 68780 // unit is step #define TOUCH_OFFSET 5000 // unit is step #define PEN_STATE_UP 0 #define PEN_STATE_DOWN 1 #define CMD_PEN_UP 0 #define CMD_PEN_DOWN 1 #define CMD_MOVE 2 #define STEP_MODE 32 #define SPEED_X_COEF ((long)40 * STEP_MODE) #define SPEED_Y_COEF ((long)40 * STEP_MODE) #define SPEED_X_MAX ((long)1500 * STEP_MODE) #define SPEED_Y_MAX ((long)1500 * STEP_MODE) #define ACCEL_X_MAX ((long)6000 * STEP_MODE) #define ACCEL_Y_MAX ((long)6000 * STEP_MODE) #define STEP_STATE_STOP 0 #define STEP_STATE_LOCK 1 #define RESOLUTION 500 #define MIN_UPDATE_INTERVAL 100 // in millisecond PhpocServer server(80); ExpansionStepper stepX(14); ExpansionStepper stepY(13); Servo servo; long preX = 0; long preY = 0; byte penState = PEN_STATE_UP; bool isUnlockedX = false; bool isUnlockedY = false; int forwardDirX = -1; /* direction of XY plotter when motor X move forward, depending on installization, It should be tested to determine the values*/ int forwardDirY = +1; /* direction of XY plotter when motor Y move up, depending on installization, It should be tested to determine the values*/ unsigned long lastUpdateMillis; void penUp() { servo.write(110); penState = PEN_STATE_UP; } void penDown() { servo.write(180); penState = PEN_STATE_DOWN; } void xyWait() { while(stepX.getState() > 1 || stepY.getState() > 1) ; } void xyInit() { penUp(); stepX.setMode(STEP_MODE); stepX.setVrefStop(4); stepX.setVrefDrive(15); stepX.setResonance(120, 250); stepX.setSpeed(20000); stepX.setAccel(50000); stepY.setMode(STEP_MODE); stepY.setVrefStop(4); stepY.setVrefDrive(15); stepY.setResonance(120, 250); stepY.setSpeed(20000); stepY.setAccel(50000); // move pen to (0, 0) stepX.stepGotoSW(0, -forwardDirX); stepY.stepGotoSW(0, -forwardDirY); xyWait(); stepX.setPosition(0); stepY.setPosition(0); // uncomment this block for the first run and change the value in line 6, 7 of this file and line 33 of index.php according to value in IDE console /* // check max steps stepX.stepGotoSW(1, forwardDirX); stepY.stepGotoSW(1, forwardDirY); xyWait(); // change these value in line 45 of index.php Serial.print(F("MAX_X:")); Serial.println(stepX.getPosition() * forwardDirX); Serial.print(F("MAX_Y:")); Serial.println(stepY.getPosition() * forwardDirY); */ xyGoto(TOUCH_OFFSET, TOUCH_OFFSET); xyWait(); stepX.setEioMode(0, EIO_MODE_LOCK); stepX.setEioMode(1, EIO_MODE_LOCK); stepY.setEioMode(0, EIO_MODE_LOCK); stepY.setEioMode(1, EIO_MODE_LOCK); } void xyGoto(long x, long y) { if(x < TOUCH_OFFSET) x = TOUCH_OFFSET; else if(x > (MAX_X - TOUCH_OFFSET)) x = MAX_X - TOUCH_OFFSET; if(y < TOUCH_OFFSET) y = TOUCH_OFFSET; else if(y > (MAX_Y - TOUCH_OFFSET)) y = MAX_Y - TOUCH_OFFSET; x *= forwardDirX; y *= forwardDirY; int32_t deltaX = x - stepX.getPosition(); int32_t deltaY = y - stepY.getPosition(); deltaX = abs(deltaX); deltaY = abs(deltaY); long speedX = SPEED_X_COEF * abs(deltaX); long speedY = SPEED_Y_COEF * abs(deltaY); if(speedX > speedY) { if(speedX > SPEED_X_MAX) speedX = SPEED_X_MAX; double ratio = deltaY / (double)deltaX; speedY = (long) (ratio * speedX); } else { if(speedY > SPEED_Y_MAX) speedY = SPEED_Y_MAX; double ratio = deltaX / (double)deltaY; speedX = (long) (ratio * speedY); } long accelX; long accelY; if(speedX < speedY) { accelY = ACCEL_Y_MAX; double ratio = accelY / (double)speedY; accelX = (long) (ratio * speedX); } else { accelX = ACCEL_X_MAX; double ratio = accelX / (double)speedX; accelY = (long) (ratio * speedY); } if(deltaX != 0) stepX.command(F("goto %ld %lu %lu"), x, speedX, accelX); if(deltaY != 0) stepY.command(F("goto %ld %lu %lu"), y, speedY, accelY); } void xyCheckUpdateToWeb() { bool isUpdate = false; unsigned long curMillis = millis(); if((curMillis - lastUpdateMillis) > MIN_UPDATE_INTERVAL) isUpdate = true; long curX = stepX.getPosition(); long curY = stepY.getPosition(); long deltaX = curX - preX; long deltaY = curY - preY; long dist = sqrt(pow(deltaX, 2) + pow(deltaY, 2)); if(dist > RESOLUTION) isUpdate = true; if(isUpdate == false || dist == 0) return false; lastUpdateMillis = curMillis; preX = curX; preY = curY; sendPositionToWeb(); // send current postion to display on web } void sendPositionToWeb() { char wbuf[20]; long x = stepX.getPosition() * forwardDirX; long y = stepY.getPosition() * forwardDirY; String data = String(F("[")) + x + String(F(",")) + y + String(F(",")) + penState + String(F("]\n")); data.toCharArray(wbuf, data.length() + 1); server.write(wbuf, data.length()); } void setup() { Serial.begin(9600); while(!Serial) ; Phpoc.begin(PF_LOG_SPI | PF_LOG_NET); server.beginWebSocket("xy_plotter"); Serial.print("WebSocket server address : "); Serial.println(Phpoc.localIP()); Expansion.begin(460800); servo.attach(8); /** NOTE: For the first run: * - uncomment the last block in xyInit() * - run Arduino code * - change the value in line 6, 7 of this file and line 33 of index.php according to value in IDE console. **/ xyInit(); lastUpdateMillis = millis(); } void loop() { // wait for a new client: PhpocClient client = server.available(); if(client) { String data = client.readLine(); if(data) { //Serial.println(data); byte separatorPos1 = data.indexOf(':'); byte separatorPos2 = data.lastIndexOf(':'); byte cmd = data.substring(0, separatorPos1).toInt(); long x = data.substring(separatorPos1 + 1, separatorPos2).toInt(); long y = data.substring(separatorPos2 + 1).toInt(); switch(cmd) { case CMD_PEN_DOWN: xyGoto(x, y); //xyWait(); while(stepX.getState() > 1 || stepY.getState() > 1) xyCheckUpdateToWeb(); penDown(); break; case CMD_PEN_UP: //xyWait(); while(stepX.getState() > 1 || stepY.getState() > 1) xyCheckUpdateToWeb(); penUp(); break; case CMD_MOVE: xyGoto(x, y); break; } } } xyCheckUpdateToWeb(); }
Web User Interface - remote_draw.php
- Providing the user interface
- Handling the user event and send command with coordinate to Arduino
- Receiving the trajectory from Arduino and draw it on webpage
remote_draw.php is a file that contains Web User Interface. It needs to be stored on PHPoC [WiFi] Shield. In order to upload the file to PHPoC [WiFi] Shield, please do the following steps:
- Copy the below code and save it into remote_draw.php file.
- Install PHPoC Debugger
- Connect PHPoC to PHPoC [WiFi] Shield via micro-USB cable according to this instruction.
Note that Arduino must be powered. - Upload remote_draw.php file to PHPoC [WiFi] Shield according to this instruction
PHP Code:
<!DOCTYPE html>
<html>
<head>
<title>Arduino - PHPoC Shield - XY Plotter</title>
<meta name="viewport" content="width=device-width, initial-scale=0.7">
<style>
body { text-align: center; background-color: #33C7F2; }
#canvas { margin-right: auto;
margin-left: auto;
position: relative;
background-color: #FFFFFF;
}
canvas {
position: absolute;
left: 0px;
top: 0px;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch; /* nice webkit native scroll */
}
#layer1 { z-index: 2; }
#layer2 { z-index: 1; }
#layer3 { z-index: 0; }
</style>
<script>
var PEN_STATE_UP = 0;
var PEN_STATE_DOWN = 1;
var CMD_PEN_UP = 0;
var CMD_PEN_DOWN = 1;
var CMD_MOVE = 2;
var RESOLUTION = 1000;
var MIN_UPDATE_INTERVAL = 100; // in millisecond
var MAX_X = 55550, MAX_Y = 68780;
var CANVAS_WIDTH = 0, CANVAS_HEIGHT = 0;
var ws = null;
var canvas = null;
var layer1 = null, layer2 = null, layer3 = null;
var ctx1 = null, ctx2 = null, ctx3 = null;
var touchState = 0;
var touchStepX = 0, touchStepY = 0; /* unit is step */
var touchPixelX = 0, touchPixelY = 0; /* unit is pixel */
var plotterX = 0, plotterY = 0;
var plotterPenState = PEN_STATE_UP;
var lastUpdateMillis;
var buffer = "";
function init() {
canvas = document.getElementById("canvas");
layer1 = document.getElementById("layer1"); /* for drawing current postion, which is received from XY plotter */
layer2 = document.getElementById("layer2"); /* for drawing trajatory of XY plotter, which is received from XY plotter*/
layer3 = document.getElementById("layer3"); /* for drawing the current touch postion, which is inputed by user on screen */
ctx1 = layer1.getContext("2d");
ctx2 = layer2.getContext("2d");
ctx3 = layer3.getContext("2d");
layer1.addEventListener("touchstart", mouse_down);
layer1.addEventListener("touchend", mouse_up);
layer1.addEventListener("touchmove", mouse_move);
layer1.addEventListener("mousedown", mouse_down);
layer1.addEventListener("mouseup", mouse_up);
layer1.addEventListener("mousemove", mouse_move);
canvasResize();
var d = new Date();
lastUpdateMillis = d.getTime();
}
function ws_onmessage(e_msg) {
buffer += e_msg.data;
var pos = buffer.indexOf('\n');
if(pos == -1)
return;
var data = buffer.substring(0, pos);
buffer = buffer.substring(pos + 1);
var arr = JSON.parse(data);
plotterX = arr[0];
plotterY = arr[1];
var newPlotterPenState = arr[2];
/* convert step unit to pixel unit */
plotterX = Math.round(plotterX * CANVAS_WIDTH / MAX_X);
plotterY = -Math.round(plotterY * CANVAS_HEIGHT / MAX_Y);
/* draw current postion of plotter*/
ctx1.clearRect(0, 0, CANVAS_WIDTH, -CANVAS_HEIGHT);
ctx1.beginPath();
ctx1.arc(plotterX, plotterY, 7, 0, 2*Math.PI);
ctx1.fill();
/* draw trajatory of plotter only when pen down */
if(plotterPenState == PEN_STATE_UP && newPlotterPenState == PEN_STATE_DOWN)
ctx2.beginPath();
if(newPlotterPenState == PEN_STATE_DOWN) {
ctx2.lineTo(plotterX, plotterY);
ctx2.stroke();
}
plotterPenState = newPlotterPenState;
}
function ws_onopen() {
document.getElementById("ws_state").innerHTML = "OPEN";
document.getElementById("wc_conn").innerHTML = "Disconnect";
}
function ws_onclose() {
document.getElementById("ws_state").innerHTML = "CLOSED";
document.getElementById("wc_conn").innerHTML = "Connect";
ws.onopen = null;
ws.onclose = null;
ws.onmessage = null;
ws = null;
}
function wc_onclick() {
if(ws == null) {
ws = new WebSocket("ws://<?echo _SERVER("HTTP_HOST")?>/xy_plotter", "text.phpoc");
document.getElementById("ws_state").innerHTML = "CONNECTING";
ws.onopen = ws_onopen;
ws.onclose = ws_onclose;
ws.onmessage = ws_onmessage;
}
else
ws.close();
}
function event_handler(event, type) {
// convert coordinate
if(event.targetTouches) {
if(event.targetTouches.length > 1)
return false;
touchPixelX = event.targetTouches[0].pageX - canvas.offsetLeft;
touchPixelY = event.targetTouches[0].pageY - canvas.offsetTop - CANVAS_HEIGHT;
} else {
touchPixelX = event.offsetX;
touchPixelY = event.offsetY - CANVAS_HEIGHT;
}
/* convert from pixel to step */
var newTouchStepX = Math.round(touchPixelX / CANVAS_WIDTH * MAX_X);
var newTouchStepY = Math.round((-touchPixelY) / CANVAS_HEIGHT * MAX_Y);
if(type == "MOVE") { /* check update condition to avoid sending too much data to plotter */
var isUpdate = false;
var d = new Date();
var curMillis = d.getTime();
if((curMillis - lastUpdateMillis) > MIN_UPDATE_INTERVAL) {
isUpdate = true;
}
var deltaX = newTouchStepX - touchStepX;
var deltaY = newTouchStepY - touchStepY;
var dist = Math.sqrt( Math.pow(deltaX, 2) + Math.pow(deltaY, 2) );
if(dist > RESOLUTION)
isUpdate = true;
if(isUpdate == false)
return false;
lastUpdateMillis = curMillis;
}
touchStepX = newTouchStepX;
touchStepY = newTouchStepY;
drawXYline();
return true;
}
function mouse_down() {
event.preventDefault();
event_handler(event, "DOWN");
sendToPlotter(CMD_PEN_DOWN, touchStepX, touchStepY);
touchState = 1;
}
function mouse_up() {
event.preventDefault();
touchState = 0;
sendToPlotter(CMD_PEN_UP, touchStepX, touchStepY);
}
function mouse_move() {
event.preventDefault();
if(touchState == 1) {
if(event_handler(event, "MOVE"))
sendToPlotter(CMD_MOVE, touchStepX, touchStepY);
}
}
function sendToPlotter(cmd, x, y) {
if(ws != null && ws.readyState == 1)
ws.send(cmd + ":" + touchStepX + ":" + touchStepY + "\r\n");
}
function canvasClear() {
ctx2.clearRect(0, 0, CANVAS_WIDTH, -CANVAS_HEIGHT); /* only clear trajatory */
}
function canvasResize() {
var width = Math.round(window.innerWidth*0.95);
var height = Math.round(window.innerHeight*0.95) - 100;
var temp_height = Math.round(width*MAX_Y/MAX_X);
if(temp_height <= height) {
CANVAS_WIDTH = width;
CANVAS_HEIGHT = temp_height;
} else {
CANVAS_WIDTH = height*MAX_X/MAX_Y;
CANVAS_HEIGHT = height;
}
canvas.style.width = CANVAS_WIDTH + "px";
canvas.style.height = CANVAS_HEIGHT + "px";
layer1.width = CANVAS_WIDTH;
layer1.height = CANVAS_HEIGHT;
layer2.width = CANVAS_WIDTH;
layer2.height = CANVAS_HEIGHT;
layer3.width = CANVAS_WIDTH;
layer3.height = CANVAS_HEIGHT;
ctx1.translate(0, CANVAS_HEIGHT);
ctx2.translate(0, CANVAS_HEIGHT);
ctx3.translate(0, CANVAS_HEIGHT);
ctx1.fillStyle = "#00979d";
ctx2.lineWidth = 3;
ctx2.strokeStyle = "black";
ctx3.strokeStyle = "00FFFF";
}
function drawXYline() {
ctx3.clearRect(0, 0, CANVAS_WIDTH, -CANVAS_HEIGHT);
ctx3.beginPath();
ctx3.moveTo(0, touchPixelY);
ctx3.lineTo(CANVAS_WIDTH, touchPixelY);
ctx3.moveTo(touchPixelX, 0);
ctx3.lineTo(touchPixelX, -CANVAS_HEIGHT);
ctx3.stroke();
}
window.onload = init;
</script>
</head>
<body onresize="canvasResize()">
<br>
<div id="canvas">
<canvas id="layer1"></canvas>
<canvas id="layer2"></canvas>
<canvas id="layer3"></canvas>
</div>
<p>WebSocket : <span id="ws_state">null</span></p>
<button id="wc_conn" type="button" onclick="wc_onclick();">Connect</button>
<button type="button" onclick="canvasClear();">Clear</button>
</body>
</html>
How To
- Config network information for PHPoC shield or PHPoC WiFi shield
- Install PHPoC Library
- Install PHPoC Expansion Library
- Compile and upload code to Arduino
- Upload web user interface to PHPoC [WiFi] shield
- Open Serial Monitor and copy IP address of PHPoC Shield
- Access Web User Interface via Web Browser: http://ip_address_of_shield/remote_draw.php
- Draw via Web