Thing Used in This Project
- PHPoC Blue or Black
- PHPoC DC Motor Controller PES-2404
- DC Motor with Encoder
- Jumper wires
How It Works
When user access webpage of PHPoC Blue from a web browser on Smart Phone or PC, a WebSocket connection will be created between PHPoC Blue and web browser. WebSocket connection allows to real-time exchange data between web browser and PHPoC Blue without reloading webpage.
When user rotates the needle on webpage, the rotated angle will be send to PHPoC Blue. PHPoC Blue convert angle to the equivalent position (unit is pulse), and then use PID algorithm to rotate DC motor the equivalent position.
PID library for PHPoC is available here. It also contains auto-tuning function for both position and speed. You can also learn about the basic of DC motor
Angle to Position Calculation
Assumption:
- Incremental Encoder Resolution (counts per revolution): ENC_RESOLUTION
- Gear Ratio: GEAR_RATIO
Signal from Encoder will be decoded by PHPoC DC Motor Controller. PHPoC DC Motor Controller acts as Quadrature Decoder, which will quadruple (x4) the number of pulses per revolution.
We have:
- a revolution <=> 360 degree <=> ENC_RESOLUTION * GEAR_RATIO * 4
- angle <=> position
The motor I used have:
- ENC_RESOLUTION = 13
- GEAR_RATIO = 100
(see line 75 of task0.php)
Wiring Diagram
- Stack DC motor controller on PHPoC Blue or Black
- Connect DC motor to DC motor controller (see User Manual of DC motor Controller)
Source Code
task0.php
This is main task, which run in infinite loop. It needs to use PID library from this link https://www.hackster.io/phpoc_man/pi...c-motor-bc028f
PHP Code:
<?php
include_once "/lib/sd_340.php";
include_once "/lib/sd_spc.php";
include_once "/lib/sn_tcp_ws.php";
include_once "vc_pid.php";
$sid = 14;
$dc_id = 1;
$pwm_period = 10000; //10000 us
st_free_setup(0, "us");
ws_setup(0, "dc_motor_rotate", "csv.phpoc");
spc_reset();
spc_sync_baud(460800);
spc_request_dev($sid, "dc$dc_id pwm set period $pwm_period");
spc_request_dev($sid, "dc$dc_id enc set psr 64");
spc_request_dev($sid, "dc$dc_id pwm set decay slow");
spc_request_dev($sid, "dc$dc_id lpf set freq 5000");
/*----------- check enc pol-------------- */
$pwm = 0; $pos = 0;
while(!$pos)
{
$pwm += 10;
spc_request_dev($sid, "dc$dc_id pwm set width $pwm");
$pos = (int)spc_request_dev($sid, "dc$dc_id enc get pos");
usleep(1000);
}
if($pos < 0)
spc_request_dev($sid, "dc$dc_id enc set pol -");
spc_request_dev($sid, "dc$dc_id pwm set width 0");
/*-----------PID parameters-----------*/
$pid = array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
//$pid[INDEX_KP_GAIN] = 0; // manually set by user or automatically set by auto-tuning function
//$pid[INDEX_KI_GAIN] = 0; // manually set by user or automatically set by auto-tuning function
//$pid[INDEX_KD_GAIN] = 0; // manually set by user or automatically set by auto-tuning function
$pid[INDEX_INT_TERM] = 0; // initial value
$pid[INDEX_PRE_ERROR] = 0; // initial value
$pid[INDEX_INT_MAX] = 2000; // recommended vaule <= 100% of pwm width for position control
$pid[INDEX_INT_MIN] = -2000; // opposite of upper limit
$pid[INDEX_PRE_TIME] = st_free_get_count(0); //current time
$pid[INDEX_SAMPLE_TIME] = 10000; // in micro-second
$pid[INDEX_PID_TYPE] = PID_TYPE_POS; // position control
$pid[INDEX_PID_SCALE] = 5; // depending on motor, should test manually to have the best value
$pid[INDEX_PWM_PERIOD] = $pwm_period;
$setpoint = 0; //desired position
/*----------- PID auto-tuning-------------- */
dc_pid_tune_start();
while(!dc_pid_tune_loop($sid, $dc_id, $pid))
usleep(10000);
spc_request_dev($sid, "dc$dc_id enc set pos 0");
/*----------- loop -----------*/
while(1)
{
dc_pid_loop($sid, $dc_id, $setpoint, $pid);
$rwbuf = "";
if(ws_state(0) == TCP_CONNECTED)
{
$rlen = ws_read_line(0, $rwbuf);
if($rlen)
{
$target_angle = (float)$rwbuf;
$setpoint = (int)($target_angle / 360.0 * (13 * 100 * 4));
}
//$pos = (int)spc_request_dev($sid, "dc$dc_id enc get pos");
//echo "$pos $setpoint\r\n";
}
}
?>
Web User Interface
PHP Code:
<!DOCTYPE html>
<html>
<head>
<title>PHPoC / <?echo system("uname -i")?></title>
<meta name="viewport" content="width=device-width, initial-scale=0.7">
<style>
body { text-align: center; background-color: whiite;}
canvas {
background-color: white;
background: url(plate.png) no-repeat;
background-size: contain;}
}
</style>
<script>
var MIN_TOUCH_RADIUS = 20;
var MAX_TOUCH_RADIUS = 200;
var CANVAS_WIDTH, CANVAS_HEIGHT;
var PIVOT_X, PIVOT_Y;
var PLATE_WIDTH = 1010.0, PLATE_HEIGHT = 1010.0; // size of background image
var NEEDLE_WIDTH = 160, NEEDLE_HEIGHT = 744; // size of needle image
var RATIO;
var plate_angle = 0;
var needle_img = new Image();
var click_state = 0;
var last_angle_pos = 0;
var mouse_xyra = {x:0, y:0, r:0.0, a:0.0};
var ws;
needle_img.src = "needle.png";
function init()
{
CANVAS_WIDTH = window.innerWidth - 50;
CANVAS_HEIGHT = window.innerHeight - 50;
if(CANVAS_WIDTH < CANVAS_HEIGHT)
CANVAS_HEIGHT = CANVAS_WIDTH;
else
CANVAS_WIDTH = CANVAS_HEIGHT;
PIVOT_X = CANVAS_WIDTH/2;
PIVOT_Y = CANVAS_HEIGHT/2;
RATIO = CANVAS_WIDTH / PLATE_WIDTH;
NEEDLE_WIDTH = Math.round(NEEDLE_WIDTH * RATIO);
NEEDLE_HEIGHT = Math.round(NEEDLE_HEIGHT * RATIO);
//needle_img.width = NEEDLE_WIDTH;
//needle_img.height = NEEDLE_HEIGHT;
var dc_motor = document.getElementById("dc_motor");
dc_motor.width = CANVAS_WIDTH;
dc_motor.height = CANVAS_HEIGHT;
dc_motor.addEventListener("touchstart", mouse_down);
dc_motor.addEventListener("touchend", mouse_up);
dc_motor.addEventListener("touchmove", mouse_move);
dc_motor.addEventListener("mousedown", mouse_down);
dc_motor.addEventListener("mouseup", mouse_up);
dc_motor.addEventListener("mousemove", mouse_move);
var ctx = dc_motor.getContext("2d");
ctx.translate(PIVOT_X, PIVOT_Y);
rotate_plate(0);
ws = new WebSocket("ws://<?echo _SERVER("HTTP_HOST")?>/dc_motor_rotate", "csv.phpoc");
document.getElementById("ws_state").innerHTML = "CONNECTING";
ws.onopen = function(){ document.getElementById("ws_state").innerHTML = "OPEN" };
ws.onclose = function(){ document.getElementById("ws_state").innerHTML = "CLOSED"};
ws.onerror = function(){ alert("websocket error " + this.url) };
ws.onmessage = ws_onmessage;
}
function ws_onmessage(e_msg)
{
e_msg = e_msg || window.event; // MessageEvent
plate_angle = Number(e_msg.data);
rotate_plate(plate_angle);
//alert("msg : " + e_msg.data);
}
function rotate_plate(angle)
{
var dc_motor = document.getElementById("dc_motor");
var ctx = dc_motor.getContext("2d");
ctx.clearRect(-PIVOT_X, -PIVOT_Y, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.rotate(-angle / 180 * Math.PI);
ctx.drawImage(needle_img, 0, 0, needle_img.width, needle_img.height, -NEEDLE_WIDTH/2, -NEEDLE_HEIGHT/2, NEEDLE_WIDTH, NEEDLE_HEIGHT);
//ctx.drawImage(needle_img, -NEEDLE_WIDTH/2, -NEEDLE_HEIGHT/2);
ctx.rotate(angle / 180 * Math.PI);
debug = document.getElementById("debug");
debug.innerHTML = plate_angle.toFixed(1);
}
function check_update_xyra(event, mouse_xyra)
{
var x, y, r, a;
var min_r, max_r, width;
if(event.touches)
{
var touches = event.touches;
x = (touches[0].pageX - touches[0].target.offsetLeft) - PIVOT_X;
y = PIVOT_Y - (touches[0].pageY - touches[0].target.offsetTop);
}
else
{
x = event.offsetX - PIVOT_X;
y = PIVOT_Y - event.offsetY;
}
/* cartesian to polar coordinate conversion */
r = Math.sqrt(x * x + y * y);
a = Math.atan2(y, x);
mouse_xyra.x = x;
mouse_xyra.y = y;
mouse_xyra.r = r;
mouse_xyra.a = a;
if((r >= MIN_TOUCH_RADIUS) && (r <= MAX_TOUCH_RADIUS))
return true;
else
return false;
}
function mouse_down()
{
if(event.touches && (event.touches.length > 1))
click_state = event.touches.length;
if(click_state > 1)
return;
if(check_update_xyra(event, mouse_xyra))
{
click_state = 1;
last_angle_pos = mouse_xyra.a / Math.PI * 180.0;
}
}
function mouse_up()
{
click_state = 0;
}
function mouse_move()
{
var angle_pos, angle_offset;
if(event.touches && (event.touches.length > 1))
click_state = event.touches.length;
if(click_state > 1)
return;
if(!click_state)
return;
if(!check_update_xyra(event, mouse_xyra))
{
click_state = 0;
return;
}
angle_pos = mouse_xyra.a / Math.PI * 180.0;
if(angle_pos < 0.0)
angle_pos = angle_pos + 360.0;
angle_offset = angle_pos - last_angle_pos;
last_angle_pos = angle_pos;
if(angle_offset > 180.0)
angle_offset = -360.0 + angle_offset;
else
if(angle_offset < -180.0)
angle_offset = 360 + angle_offset;
plate_angle += angle_offset;
rotate_plate(plate_angle);
if(ws.readyState == 1)
ws.send(plate_angle.toFixed(4) + "\r\n");
event.preventDefault();
}
window.onload = init;
</script>
</head>
<body>
<h2>
Smart Expansion / DC Motor Rotate<br>
<br>
<canvas id="dc_motor"></canvas>
<p>
WebSocket : <span id="ws_state">null</span><br>
Angle : <span id="debug">0</span>
</p>
</h2>
</body>
</html>
Two images used in Web User Interface