This project shows how to control XY Plotter Robot via Web interface.

You can also see the same article in Hacster.io https://www.hackster.io/phpoc_man/xy...via-web-10a9d0


Demonstration




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 PHPoC blue (Provide web interface and control two step motors via PHPoC Motor Controller)
  • 2 x PHPoC Motor Controller (control to two step motors to move the pen )


How It Works

When a finger touches to the drawing area in webpage, the XY coordinate of touching point is sent to PHPoC. After scaling the coordinate, PHPoC will move two step motors to locate the pen to that coordinate. During moving period, PHPoC continuously send trajectory of the pen to web app, the web app draw the trajectory on the canvas.


Source Code

Main task (task0.php):
  • Receiving the command from webpage and do task according to 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


PHP Code:
<?php

include_once "/lib/sd_340.php";
include_once 
"/lib/sd_spc.php";
include_once 
"/lib/sn_tcp_ws.php";

define("PWM_PERIOD"20000); // (20ms)
define("WIDTH_MIN"600);
define("WIDTH_MAX"2450);

define("SID_X",            13);
define("SID_Y",            14);

define("MAX_X",         3421);
define("MAX_Y",         4296);
define("TOUCH_OFFSET",    0);

define("PEN_UP",        0);
define("PEN_DOWN",        1);

define("CMD_PEN_UP",    0);
define("CMD_PEN_DOWN",    1);
define("CMD_MOVE",        2);

define("SPEED_X_COEF",        20);
define("SPEED_Y_COEF",        20);
define("SPEED_X_OFFSET",    50);
define("SPEED_Y_OFFSET",    50);
define("SPEED_X_MAX",        5000);
define("SPEED_Y_MAX",        5000);

define("ACCEL_X_COEF",        1500);
define("ACCEL_Y_COEF",        1000);
define("ACCEL_X_OFFSET",    0);
define("ACCEL_Y_OFFSET",    0);
define("ACCEL_X_MAX",        20000);
define("ACCEL_Y_MAX",        20000);

define("STATE_X_LOCK",    1);
define("STATE_X_RUN",    2);
define("STATE_Y_LOCK",    4);
define("STATE_Y_RUN",    8);

function 
step_cmd($sid$cmd)
{
    
$resp spc_request($sid4$cmd);

    if(
$resp)
    {
        
$resp explode(","$resp);

        if((int)
$resp[0] != 200)
        {
            echo 
"step_cmd : '$cmd' request error "$resp[0], "\r\n";
            return 
false;
        }

        if(
count($resp) > 1)
            return 
$resp[1];
        else
            return 
"";
    }
    else
        return 
false;
}

function 
spc_check_did($sid$did)
{
    
$resp spc_request_csv($sid0"get did");

    if(
$resp === false)
    {
        echo 
"spc_check_did: sid$sid - device not found\r\n";
        return 
false;
    }

    if(
$resp[1] != "40012403")
    {
        echo 
"spc_check_did: unknown device "$resp[2], "\r\n";
        return 
false;
    }

    return 
true;
}

function 
pen_up()
{
    
$angle 110;

    
$width WIDTH_MIN + (int)round((WIDTH_MAX WIDTH_MIN) * $angle 180.0);

    if((
$width >= WIDTH_MIN) && ($width <= WIDTH_MAX))
        
ht_pwm_width(2$widthPWM_PERIOD);
}

function 
pen_down()
{
    
$angle 180;

    
$width WIDTH_MIN + (int)round((WIDTH_MAX WIDTH_MIN) * $angle 180.0);

    if((
$width >= WIDTH_MIN) && ($width <= WIDTH_MAX))
        
ht_pwm_width(2$widthPWM_PERIOD);
}

function 
xy_goto($x$y)
{

    if(
$x TOUCH_OFFSET)
        
$x TOUCH_OFFSET;
    if(
$x > (MAX_X TOUCH_OFFSET))
        
$x MAX_X TOUCH_OFFSET;

    if(
$y TOUCH_OFFSET)
        
$y TOUCH_OFFSET;
    if(
$y > (MAX_Y TOUCH_OFFSET))
        
$y MAX_Y TOUCH_OFFSET;

    
$x0 = (int)step_cmd(SID_X"get pos");
    
$y0 = (int)step_cmd(SID_Y"get pos");

    
$delta_x $x $x0;
    
$delta_y $y $y0;

    if(
$delta_x == && $delta_y == 0)
        return;

    
$speed_x SPEED_X_COEF abs($delta_x) + SPEED_X_OFFSET;
    
$speed_y SPEED_Y_COEF abs($delta_y) + SPEED_Y_OFFSET;

    if(
$speed_x SPEED_X_MAX)
        
$speed_x SPEED_X_MAX;

    if(
$speed_y SPEED_Y_MAX)
        
$speed_y SPEED_Y_MAX;

    
$accel_x ACCEL_X_COEF $speed_x ACCEL_X_OFFSET;
    
$accel_y ACCEL_Y_COEF $speed_y ACCEL_Y_OFFSET;

    if(
$accel_x ACCEL_X_MAX)
        
$accel_x ACCEL_X_MAX;

    if(
$accel_y ACCEL_Y_MAX)
        
$accel_y ACCEL_Y_MAX;

    if(
$delta_x == 0)
    {
        
step_cmd(SID_Y"goto $y $speed_y $accel_y");
    }
    else if(
$delta_y == 0)
    {
        
step_cmd(SID_X"goto $x $speed_x $accel_x");
    }
    else
    {
        
step_cmd(SID_X"goto $x $speed_x $accel_x");
        
step_cmd(SID_Y"goto $y $speed_y $accel_y");
    }
}

function 
xy_state()
{
    
$ret_val 0;

    
$x_state = (int)step_cmd(SID_X"get state");
    
$y_state = (int)step_cmd(SID_Y"get state");

    if(
$x_state == 1// motor is locked
        
$ret_val |= STATE_X_LOCK;
    else if(
$x_state 1// motor is running
        
$ret_val |= STATE_X_RUN;

    if(
$y_state == 1// motor is locked
        
$ret_val |= STATE_Y_LOCK;
    else if(
$y_state 1// motor is running
        
$ret_val |= STATE_Y_RUN;

    return 
$ret_val;
}

function 
xy_wait()
{
    while(
xy_state() & (STATE_X_RUN STATE_Y_RUN))
        
usleep(1);
}

function 
xy_init()
{
    
$width WIDTH_MIN + (int)round((WIDTH_MAX WIDTH_MIN) * 110 180.0);
    
ht_pwm_setup(2$widthPWM_PERIOD);
    
pen_up();

    
spc_reset();
    
spc_sync_baud(460800);

    if(!
spc_check_did(SID_X"40012403"))
        return;
    if(!
spc_check_did(SID_Y"40012403"))
        return;

    
step_cmd(SID_X"set vref stop 3");
    
step_cmd(SID_X"set vref drive 14");
    
step_cmd(SID_X"set mode half");
    
step_cmd(SID_X"set rsnc 120 250");

    
step_cmd(SID_Y"set vref stop 3");
    
step_cmd(SID_Y"set vref drive 14");
    
step_cmd(SID_Y"set mode half");
    
step_cmd(SID_Y"set rsnc 120 250");

    
// move pen to (0, 0)
    
step_cmd(SID_X"goto -sw1 1600 20000");
    
step_cmd(SID_Y"goto -sw1 1600 20000");

    
xy_wait();
    
step_cmd(SID_X"reset");
    
step_cmd(SID_Y"reset");

    
xy_goto(TOUCH_OFFSETTOUCH_OFFSET);
    
xy_wait();

    
// uncomment this block for the first run an change the value in line 45 of index.php
    /*
    // check max steps
    step_cmd(SID_X, "goto +sw0 1600 20000");
    step_cmd(SID_Y, "goto +sw0 1600 20000");

    xy_wait();
    // change this value in line 45 of index.php: var MAX_X = 3421, MAX_Y = 4296;
    echo "Max X:", step_cmd(SID_X, "get pos"), "\r\n";
    echo "Max Y:", step_cmd(SID_Y, "get pos"), "\r\n";
    */
}

function 
send_position($pen_state)
{
    
$x = (int)step_cmd(SID_X"get pos");
    
$y = (int)step_cmd(SID_Y"get pos");
    
$wbuf "[$x$y$pen_state]";
    
ws_write(0$wbuf);
}

ws_setup(0"xy_plotter""csv.phpoc");
xy_init();

$pre_x 0;
$pre_y 0;
$cur_x 0;
$cur_y 0;
$rbuf "";
$pen_state CMD_PEN_UP;
$x_is_unlock false;
$y_is_unlock false;

while(
1)
{
    if(
ws_state(0) == TCP_CONNECTED)
    {
        
$rlen ws_read_line(0$rbuf);

        if(
$rlen)
        {
            
$data explode(" "$rbuf);
            
$cmd = (int)$data[0];

            switch(
$cmd)
            {
                case 
CMD_PEN_DOWN:
                    
$x = (int)$data[1];
                    
$y = (int)$data[2];
                    
xy_goto($x$y);
                    
xy_wait();

                    
pen_down();
                    
$pen_state PEN_DOWN;

                    
send_position($pen_state);

                    break;
                case 
CMD_PEN_UP:
                    
pen_up();
                    
$pen_state PEN_UP;

                    
send_position($pen_state);

                    break;
                case 
CMD_MOVE:
                    
$x = (int)$data[1];
                    
$y = (int)$data[2];
                    
xy_goto($x$y);
                    
//xy_wait();

                    
break;
            }
        }
    }

    
$cur_x = (int)step_cmd(SID_X"get pos");
    
$cur_y = (int)step_cmd(SID_Y"get pos");

    if(
abs($pre_x $cur_x) > 10 || abs($pre_y $cur_y) > 10)
    {
        
$pre_x $cur_x;
        
$pre_y $cur_y;
        
send_position($pen_state);
    }

    
$xy_state xy_state();

    if((
$xy_state STATE_X_LOCK) && ($x_is_unlock == false))
    {
        
step_cmd(SID_X"unlock");
        
$x_is_unlock true;
    }
    else if((
$xy_state STATE_X_RUN))
    {
        
step_cmd(SID_X"eio set 0 mode lock");
        
step_cmd(SID_X"eio set 1 mode lock");
        
$x_is_unlock false;
    }

    if((
$xy_state STATE_Y_LOCK) && ($y_is_unlock == false))
    {
        
step_cmd(SID_Y"unlock");
        
$y_is_unlock true;
    }
    else if((
$xy_state STATE_Y_RUN))
    {
        
step_cmd(SID_Y"eio set 0 mode lock");
        
step_cmd(SID_Y"eio set 1 mode lock");
        
$y_is_unlock false;
    }
}

?>



Web interface (index.php)
  • Providing the user interface
  • Handling the user event and send command with coordinate to PHPoC
  • Receiving the trajectory from PHPoC and draw it on webpage


PHP Code:
<!DOCTYPE html>
<html>
<head>
<title>PHPoC - XY Plotter</title>
<meta name="viewport" content="width=device-width, initial-scale=0.7">
<style>
body {
    text-align: center;
    background-color: #33C7F2;
}
#cvs_frame {
    margin-right: auto;
    margin-left: auto;
    position: relative;
}
.canvas {
    position: absolute;
    left: 0px;
    top: 0px;
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch; /* nice webkit native scroll */
}

#layer_1 {
    z-index: 2;
}
#layer_2 {
    z-index: 1;
}
#layer_3 {
    z-index: 0;
    background-color: #FFFFFF;
}

</style>
<script>
var PEN_UP = 0;
var PEN_DOWN = 1;
var CMD_PEN_UP = 0;
var CMD_PEN_DOWN = 1;
var CMD_MOVE = 2;
var RESOLUTION = 20;

var MAX_X = 3421, MAX_Y = 4296;

var ws = null;
var layer_1 = null, layer_2 = null, layer_3 = null;
var cvs_frame = null;
var ctx1 = null, ctx2 = null, ctx3 = null;

var canvas_width = 0, canvas_height = 0;
var x = 0, y = 0;
var touch_x = 0; touch_y = 0;
var cvs_pos_x = 0, cvs_pos_y = 0;
var pre_cvs_pos_x = 0, pre_cvs_pos_y = 0;
var pre_pen_state = PEN_UP;

function init()
{
    cvs_frame = document.getElementById("cvs_frame");

    layer_1 = document.getElementById("layer_1");
    layer_2 = document.getElementById("layer_2");
    layer_3 = document.getElementById("layer_3");

    var body = document.getElementsByTagName("BODY")[0];
    body.addEventListener("touchstart", disable_zoom);
    body.addEventListener("touchend", disable_zoom);
    body.addEventListener("touchmove", disable_zoom);
    body.addEventListener("mousedown", disable_zoom);
    body.addEventListener("mouseup", disable_zoom);

    layer_1.addEventListener("touchstart", mouse_down);
    layer_1.addEventListener("touchend", mouse_up);
    layer_1.addEventListener("touchmove", mouse_move);
    layer_1.addEventListener("mousedown", mouse_down);
    layer_1.addEventListener("mouseup", mouse_up);
    layer_1.addEventListener("mousemove", mouse_move);

    ctx1 = layer_1.getContext("2d");
    ctx1.translate(0, canvas_height);

    ctx2 = layer_2.getContext("2d");
    ctx2.translate(0, canvas_height);

    ctx3 = layer_3.getContext("2d");
    ctx3.translate(0, canvas_height);

    canvas_resize();
}
function canvas_clear()
{
    ctx1.clearRect(0, 0, canvas_width, -canvas_height);
    ctx2.clearRect(0, 0, canvas_width, -canvas_height);
    ctx3.clearRect(0, 0, canvas_width, -canvas_height);
}
function event_handler(event, type)
{
    var pre_x = x, pre_y = y;
    // convert coordinate
    if(event.targetTouches)
    {
        var targetTouches = event.targetTouches;

        touch_x = targetTouches[0].pageX - cvs_frame.offsetLeft;
        touch_y = targetTouches[0].pageY - cvs_frame.offsetTop - canvas_height;
    }
    else
    {
        touch_x = event.offsetX;
        touch_y = event.offsetY - canvas_height;
    }

    var temp_x = Math.round(touch_x / canvas_width * MAX_X);
    var temp_y = Math.round((-touch_y) / canvas_height * MAX_Y);

    if(type == "MOVE")
    {
        var delta_x = temp_x - pre_x;
        var delta_y = temp_y - pre_y;
        var dist = Math.sqrt( Math.pow(delta_x, 2) + Math.pow(delta_y, 2) );

        if(dist < RESOLUTION)
            return false;
    }

    x = temp_x;
    y = temp_y;

    return true;
}
function ws_onmessage(e_msg)
{
    var arr = JSON.parse(e_msg.data);
    var pos_x            = arr[0];
    var pos_y            = arr[1];
    var cur_pen_state    = arr[2];

    pre_cvs_pos_x = cvs_pos_x;
    pre_cvs_pos_y = cvs_pos_y;

    cvs_pos_x = Math.round(pos_x * canvas_width / MAX_X);
    cvs_pos_y = -Math.round(pos_y * canvas_height / MAX_Y);

    ctx1.clearRect(0, 0, canvas_width, -canvas_height);
    ctx1.beginPath();
    ctx1.arc(cvs_pos_x, cvs_pos_y, 7, 0, 2*Math.PI);
    ctx1.fill();

    if(pre_pen_state == PEN_UP && cur_pen_state == PEN_DOWN)
        ctx2.beginPath();

    if(cur_pen_state == PEN_DOWN)
    {
        ctx2.lineTo(cvs_pos_x, cvs_pos_y);
        ctx2.stroke();
    }

    pre_pen_state = cur_pen_state;
}
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", "csv.phpoc");
        document.getElementById("ws_state").innerHTML = "CONNECTING";

        ws.onopen = ws_onopen;
        ws.onclose = ws_onclose;
        ws.onmessage = ws_onmessage;  
    }
    else
        ws.close();
}
function mouse_down()
{
    if(event.targetTouches)
    {
        event.preventDefault();
        if(event.targetTouches.length > 1)
            return;
    }

    event_handler(event, "DOWN");

    if(ws == null || ws.readyState != 1)
        return;

    ws.send(CMD_PEN_DOWN + " " + x + " " + y + "\r\n");

    draw_xy_line();
}
function mouse_up()
{
    if(event.targetTouches)
    {
        event.preventDefault();
    }

    if(ws == null || ws.readyState != 1)
        return;

    ws.send(CMD_PEN_UP + "\r\n");
}
function mouse_move()
{
    if(event.targetTouches)
    {
        event.preventDefault();
        if(event.targetTouches.length > 1)
            return;
    }

    if(!event_handler(event, "MOVE"))
        return;

    if(ws == null || ws.readyState != 1)
        return;

    ws.send(CMD_MOVE + " " + x + " " + y + "\r\n");

    draw_xy_line();
}
function disable_zoom()
{
    //event.preventDefault();
}
function canvas_resize()
{
    var width = Math.round(window.innerWidth*0.95);
    var height = Math.round(window.innerHeight*0.95) - 50;

    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;
    }

    cvs_frame.style.width = canvas_width + "px";
    cvs_frame.style.height = canvas_height + "px";
    cvs_frame.style.top = 0 + "px";
    cvs_frame.style.left = 100 + "px";

    layer_1.width = canvas_width;
    layer_1.height = canvas_height;
    ctx1.translate(0, canvas_height);
    ctx1.lineWidth = 5;
    ctx1.fillStyle = "green";

    layer_2.width = canvas_width;
    layer_2.height = canvas_height;
    ctx2.translate(0, canvas_height);
    ctx2.lineWidth = 3;
    ctx2.strokeStyle = "black";

    layer_3.width = canvas_width;
    layer_3.height = canvas_height;
    ctx3.translate(0, canvas_height);
    ctx3.strokeStyle = "00FFFF";
}
function draw_xy_line()
{
    ctx3.clearRect(0, 0, canvas_width, -canvas_height);

    ctx3.beginPath();
    ctx3.moveTo(0, touch_y);
    ctx3.lineTo(canvas_width, touch_y);
    ctx3.stroke();

    ctx3.beginPath();
    ctx3.moveTo(touch_x, 0);
    ctx3.lineTo(touch_x, -canvas_height);
    ctx3.stroke();
}
window.onload = init;
</script>
</head>

<body onresize="canvas_resize()">
<br>
<div id="cvs_frame">
    <canvas id="layer_1" class="canvas"></canvas>
    <canvas id="layer_2" class="canvas"></canvas>
    <canvas id="layer_3" class="canvas"></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="canvas_clear();">Clear</button>
</body>
</html>