ABOUT THIS PROJECT
A vehicle measures distance with an encoder on its wheel. It is remotely controlled and transmits the distance via Websocket.
THINGS USED IN THIS PROJECT
- Arduino Uno
- PHPoC WiFi shield for Arduino (P4S-347)
- DFRobot RB-Dfr-67
- Rechargeable Battery AA (3050mA) x 5
- DFRobot RB-Dfr-04
- DFRobot 2x2A DC Motor Shield for Arduino
STORY
When you measure distance between two point general way is to use a ruler. But you can use a lot of other ways: by laser, map, foot or walking meter.
The walking meter is very useful when you are measuring curved (not straight) distance. But it might be very tired because you should walk the entire distance.
Here is a movie which is measuring distance. You can see that the vehicle is being controlled by the web browser and distance is displayed in the web page in real-time.
This project is about a distance measuring vehicle. It is remotely controlled through the WiFi network via Websocket and user can get the distance data from the vehicle in real-time via the Websocket.
Now, I am going to explain how I have implemented the system.
1. Assembling a vehicle
At first, I have assembled a vehicle from the DFRobot.
http://www.robotshop.com/en/dfrobot-...-encoders.html
The vehicle kit was great because I could get everything from this kit. But when I assembled two encoders some parts were not fit so it was tough to fix.
2. Modifying a web page
I have implemented a web page by using a source code from hackster.io. It is a cool implementation of a joystick via Websocket.
https://www.hackster.io/iot_lover/ar...oystick-02ca54
I have added a function to display distance data from the vehicle.
And I have modified that it transmits control data to the vehicle only when joystick position is changed over predefined offset because there were a lot of data transmission with original web page.
3. Programming Arduino Uno
I have programmed one source code for the Arduino Uno and PHPoC Wifi Shield.
The PHPoC Wifi Shield supports Websocket so user can implement a real-time web-based system with Arduino Uno.
Data from the web browser through PHPoC Shield's Websocket are text number from 0 to 100. But the PWM data for the DC motor driver are from 0 to 255. So there is a conversion routine after reading the data from the Websocket.
And distance is transmitted every 10cm through the Websocket. Distance is determined by counting the encoder on the wheel and calculating it based on the wheel's diameter.
So I needed to count the encoder. But calculated distance is so large than I expected. I found that there are a lot of noise when I reading the encoder state from pin 2. So I programmed software filter. The algorithm is followed:
- Check the input port (digital port 2) ever 100us in ISR (Interrupt Service Routine) from the TimerOne library.
- If the read state is same to former state it increase a number and it decrease it when the state is not same.
- When the count is over to the predefined threshold it counts one.
SCHEMATICS
CODE
PHP Code:
<!DOCTYPE html>
<html>
<head>
<title>Arduino - PHPoC Shield</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 pos_old;
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
document.getElementById("distance").innerHTML = e_msg.data;
}
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);
pos_old = pos;
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;
if((Math.abs(pos_old.x - pos.x) > 5) || (Math.abs(pos_old.y - pos.y) > 5))
{
send_data();
pos_old = pos;
}
update_view();
}
}
function clear_distance()
{
if(ws != null) ws.send("c\r\n");
}
window.onload = init;
</script>
</head>
<body>
<p>
<h1>Arduino - Web-based Joystick</h1>
</p>
<canvas id="remote"></canvas>
<h2>
<p>
Distance: <span id="distance">null</span>
</p>
<h2>
<p>
<input type="button" value="Clear Distance" id="ClickMe" onclick="clear_distance();" />
</p>
<p>
WebSocket : <span id="ws_state">null</span>
</p>
<button id="bt_connect" type="button" onclick="connect_onclick();">Connect</button>
</h2>
</body>
</html>
Code:
#include "SPI.h" #include "Phpoc.h" #include "TimerOne.h" volatile long encoder1 = 0, encoder2 = 0; unsigned long encoder1_old = 0, encoder2_old = 0; unsigned long uptime_old = 0; //Arduino PWM Speed Control int EL = 5; int ML = 4; int ER = 6; int MR = 7; int amr, aml; float max_speed; bool state_old = 0; int filter_count = 0; int filter_threshold = 10; PhpocServer server(80); void setup() { Serial.begin(9600); while(!Serial) ; Phpoc.begin(PF_LOG_SPI | PF_LOG_NET); //Phpoc.begin(); server.beginWebSocket("web_joystick"); Serial.print("WebSocket server address : "); Serial.println(Phpoc.localIP()); pinMode(ML, OUTPUT); pinMode(MR, OUTPUT); digitalWrite(ML, LOW); digitalWrite(MR, HIGH); pinMode(2, INPUT); digitalWrite(2, HIGH); // pull-up encoder1 = 0; encoder2 = 0; encoder1_old = 0; encoder2_old = 0; //attachInterrupt(digitalPinToInterrupt(2), ISR_di1, FALLING); Timer1.initialize(100); // 0.1m second Timer1.attachInterrupt(ISR_Timer1); } void loop() { double distance1; String str; char ch[32]; // wait for a new client: PhpocClient client = server.available(); if (client) { String data = client.readLine(); if(data){ if(data.substring(0, 1) == "c") { Serial.println("Clearing encoder1...\r\n"); encoder1 = 0; encoder1_old = 0; server.write("0"); return; } int pos = data.indexOf(':'); long x = data.substring(0, pos).toInt(); long y = data.substring(pos+1).toInt(); if(y < 0) { analogWrite(EL, 0); analogWrite(ER, 0); return; } else { max_speed = sqrt((float)x*(float)x/10000+(float)y*(float)y/10000); max_speed *= 255; //Serial.print("Max Speed: "); //Serial.println(max_speed); //Serial.print("encoder1 = "); //Serial.println(encoder1); } if(x == 0) { aml = (int)max_speed; amr = (int)max_speed; } else if(x > 0) { aml = (int)max_speed; amr = (int)(max_speed*(100-abs(x))/100); } else if(x < 0) { aml = (int)(max_speed*(100-abs(x))/100); amr = (int)max_speed; } analogWrite(EL, aml); analogWrite(ER, amr); } } if(encoder1 >= (encoder1_old + 10)) { distance1 = (double)encoder1 * 0.0103; str = String(distance1, 2); str.toCharArray(ch, str.length()); server.write(ch); Serial.print(encoder1); Serial.print(":"); Serial.println(ch); encoder1_old = encoder1; } } void ISR_Timer1() { bool state = 0; state = digitalRead(2); if(state != state_old) { filter_count++; if(filter_count > filter_threshold) { state_old = state; encoder1++; } } if(state == state_old && filter_count > 0) { filter_count--; } } /* void ISR_di1() { unsigned long uptime_now; uptime_now = mills(); if(uptime encoder1++; } */