This project shows how to control devices from anywhere using web-based joystick on smart phone and PHPoC


Demo




Things Used In This ProjectWe can control everything with this project, but I took the servo motors as an example.



Wiring Diagram
  • Connect pin GND and VCC of two servo motors to GND and 5V of PHPoC, respectively.
  • Connect pin signals of two servo motors to pin ht0 and pin ht1 of PHPoC, respectively.

    Click image for larger version  Name:	PHPoC_Joystick.jpg Views:	1 Size:	105.9 KB ID:	757



Data Flow

Web browser ---> PHPoC

Web app on web browser will send the coordinate (after scaling) of touch or click event to PHPoC via WebSocket. When receiving the command, PHPoC controls two servo motors according to the data received from web browser

Source Code

Source code includes two files:

- index.php: This file is client side code. When receiving HTTP request from web browser, PHPoC interprets PHP script in this file, and then send the interpreted file to web browser. The interpreted file (contains HTML, CSS and JavaScript code) provides UI (User Interface), handling touch/click event from User and send data back to PHPoC via websocket.
<index.php>
PHP Code:
<!DOCTYPE html>
<html>
<head>
<title>PHPoC</title>
<meta name="viewport" content="width=device-width, initial-scale=0.7, maximum-scale=0.7">
<meta charset="utf-8">
<style>
body { text-align: center; font-size: width/2pt; }
h1 { font-weight: bold; font-size: width/2pt; }
h2 { font-weight: bold; font-size: width/2pt; }
button { font-weight: bold; font-size: width/2pt; }
</style>
<script>
var canvas_width = 500, canvas_height = 500;
var radius_base = 150;
var radius_handle = 72;
var radius_shaft = 120;
var range = canvas_width/2 - 10;
var step = 18;
var ws;
var joystick = {x:0, y:0};
var click_state = 0;

var ratio = 1;

function init()
{
    var width = window.innerWidth;
    var height = window.innerHeight;

    if(width < height)
        ratio = (width - 50) / canvas_width;
    else
        ratio = (height - 50) / canvas_width;

    canvas_width = Math.round(canvas_width*ratio);
    canvas_height = Math.round(canvas_height*ratio);
    radius_base = Math.round(radius_base*ratio);
    radius_handle = Math.round(radius_handle*ratio);
    radius_shaft = Math.round(radius_shaft*ratio);
    range = Math.round(range*ratio);
    step = Math.round(step*ratio);

    var canvas = document.getElementById("remote");
    //canvas.style.backgroundColor = "#999999";
    canvas.width = canvas_width;
    canvas.height = canvas_height;

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

    var ctx = canvas.getContext("2d");
    ctx.translate(canvas_width/2, canvas_height/2);
    ctx.shadowBlur = 20;
    ctx.shadowColor = "LightGray";
    ctx.lineCap="round";
    ctx.lineJoin="round";

    update_view();
}
function connect_onclick()
{
    if(ws == null)
    {
        var ws_host_addr = "<?echo _SERVER("HTTP_HOST")?>";
        if((navigator.platform.indexOf("Win") != -1) && (ws_host_addr.charAt(0) == "["))
        {
            // network resource identifier to UNC path name conversion
            ws_host_addr = ws_host_addr.replace(/[\[\]]/g, '');
            ws_host_addr = ws_host_addr.replace(/:/g, "-");
            ws_host_addr += ".ipv6-literal.net";
        }

        ws = new WebSocket("ws://" + ws_host_addr + "/web_joystick", "text.phpoc");
        document.getElementById("ws_state").innerHTML = "CONNECTING";
        ws.onopen = ws_onopen;
        ws.onclose = ws_onclose;
        ws.onmessage = ws_onmessage;
    }
    else
        ws.close();
}
function ws_onopen()
{
    document.getElementById("ws_state").innerHTML = "<font color='blue'>CONNECTED</font>";
    document.getElementById("bt_connect").innerHTML = "Disconnect";
    update_view();
}
function ws_onclose()
{
    document.getElementById("ws_state").innerHTML = "<font color='gray'>CLOSED</font>";
    document.getElementById("bt_connect").innerHTML = "Connect";
    ws.onopen = null;
    ws.onclose = null;
    ws.onmessage = null;
    ws = null;
    update_view();
}
function ws_onmessage(e_msg)
{
    e_msg = e_msg || window.event; // MessageEvent

}
function send_data()
{
    var x = joystick.x, y = joystick.y;
    var joystick_range = range - radius_handle;
    x = Math.round(x*100/joystick_range);
    y = Math.round(-(y*100/joystick_range));

    if(ws != null)
        ws.send(x + ":" + y + "\r\n");
}
function update_view()
{
    var x = joystick.x, y = joystick.y;

    var canvas = document.getElementById("remote");
    var ctx = canvas.getContext("2d");

    ctx.clearRect(-canvas_width/2, -canvas_height/2, canvas_width, canvas_height);

    ctx.lineWidth = 3;
    ctx.strokeStyle="gray";
    ctx.fillStyle = "LightGray";
    ctx.beginPath();
    ctx.arc(0, 0, range, 0, 2 * Math.PI);
    ctx.stroke();
    ctx.fill();

    ctx.strokeStyle="black";
    ctx.fillStyle = "hsl(0, 0%, 35%)";
    ctx.beginPath();
    ctx.arc(0, 0, radius_base, 0, 2 * Math.PI);
    ctx.stroke();
    ctx.fill();

    ctx.strokeStyle="red";

    var lineWidth = radius_shaft;
    var pre_x = pre_y = 0;
    var x_end = x/5;
    var y_end = y/5;
    var max_count  = (radius_shaft - 10)/step;
    var count = 1;

    while(lineWidth >= 10)
    {
        var cur_x = Math.round(count * x_end / max_count);
        var cur_y = Math.round(count * y_end / max_count);
        console.log(cur_x);
        ctx.lineWidth = lineWidth;
        ctx.beginPath();
        ctx.lineTo(pre_x, pre_y);
        ctx.lineTo(cur_x, cur_y);
        ctx.stroke();

        lineWidth -= step;
        pre_x = cur_x;
        pre_y = cur_y;
        count++;
    }

    var x_start = Math.round(x / 3);
    var y_start = Math.round(y / 3);
    lineWidth += step;

    ctx.beginPath();
    ctx.lineTo(pre_x, pre_y);
    ctx.lineTo(x_start, y_start);
    ctx.stroke();

    count = 1;
    pre_x = x_start;
    pre_y = y_start;

    while(lineWidth < radius_shaft)
    {
        var cur_x = Math.round(x_start + count * (x - x_start) / max_count);
        var cur_y = Math.round(y_start + count * (y - y_start) / max_count);
        ctx.lineWidth = lineWidth;
        ctx.beginPath();
        ctx.lineTo(pre_x, pre_y);
        ctx.lineTo(cur_x, cur_y);
        ctx.stroke();

        lineWidth += step;
        pre_x = cur_x;
        pre_y = cur_y;
        count++;
    }

    var grd = ctx.createRadialGradient(x, y, 0, x, y, radius_handle);
    for(var i = 85; i >= 50; i-=5)
        grd.addColorStop((85 - i)/35, "hsl(0, 100%, "+ i + "%)");

    ctx.fillStyle = grd;
    ctx.beginPath();
    ctx.arc(x, y, radius_handle, 0, 2 * Math.PI);
    ctx.fill();
}
function process_event(event)
{
    var pos_x, pos_y;
    if(event.offsetX)
    {
        pos_x = event.offsetX - canvas_width/2;
        pos_y = event.offsetY - canvas_height/2;
    }
    else if(event.layerX)
    {
        pos_x = event.layerX - canvas_width/2;
        pos_y = event.layerY - canvas_height/2;
    }
    else
    {
        pos_x = (Math.round(event.touches[0].pageX - event.touches[0].target.offsetLeft)) - canvas_width/2;
        pos_y = (Math.round(event.touches[0].pageY - event.touches[0].target.offsetTop)) - canvas_height/2;
    }

    return {x:pos_x, y:pos_y}
}
function mouse_down()
{
    if(ws == null)
        return;

    event.preventDefault();

    var pos = process_event(event);

    var delta_x = pos.x - joystick.x;
    var delta_y = pos.y - joystick.y;

    var dist = Math.sqrt(delta_x*delta_x + delta_y*delta_y);

    if(dist > radius_handle)
        return;

    click_state = 1;

    var radius = Math.sqrt(pos.x*pos.x + pos.y*pos.y);

    if(radius <(range - radius_handle))
    {
        joystick = pos;
        send_data();
        update_view();
    }
}
function mouse_up()
{
    event.preventDefault();
    click_state = 0;
}
function mouse_move()
{
    if(ws == null)
        return;

    event.preventDefault();

    if(!click_state)
        return;

    var pos = process_event(event);

    var radius = Math.sqrt(pos.x*pos.x + pos.y*pos.y);

    if(radius <(range - radius_handle))
    {
        joystick = pos;
        send_data();
        update_view();
    }
}
window.onload = init;
</script>
</head>

<body>

<p>
<h1>PHPoC - Web-based Joystick</h1>
</p>

<canvas id="remote"></canvas>

<h2>
<p>
WebSocket : <span id="ws_state">null</span>
</p>
<button id="bt_connect" type="button" onclick="connect_onclick();">Connect</button>
</h2>

</body>
</html>



- task0.php: This file is server side code. It is run in infinite loop to receive and handle data from web browser, control servo motors.
PHP Code:
<?php

if(_SERVER("REQUEST_METHOD"))
    exit; 
// avoid php execution via http request

include "/lib/sd_340.php";
include 
"/lib/sn_tcp_ws.php";

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

ht_pwm_setup(0, (WIDTH_MIN WIDTH_MAX) / 2PWM_PERIOD"us");
ht_pwm_setup(1, (WIDTH_MIN WIDTH_MAX) / 2PWM_PERIOD"us");
ws_setup(0"web_joystick""text.phpoc");

$rbuf "";

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

        if(
$rlen)
        {
            
$data explode(" ",$rbuf);
            
$x = (int) $data[0];
            
$y = (int) $data[1];

            echo 
"x:$x, y:$y\r\n";

            
// scale  from [-100; 100] to [0; 180]
            
$angle_x = ($x 100) * 180 /200;
            
$angle_y = ($y 100) * 180 /200;

            
$width_x WIDTH_MIN + (int)round((WIDTH_MAX WIDTH_MIN) * $angle_x 180.0);
            
$width_y WIDTH_MIN + (int)round((WIDTH_MAX WIDTH_MIN) * $angle_y 180.0);

            
ht_pwm_width(0$width_xPWM_PERIOD);
            
ht_pwm_width(1$width_yPWM_PERIOD);

        }
    }
}

?>



Similar Project but different hardware platform

This project https://www.hackster.io/iot_lover/ar...oystick-02ca54 does the same works but it used other hardware platform