Plotter XY Controlado via Web com Arduino e Motores de Passo
Componentes e Materiais
| Arduino UNO | × | 1 |
| PHPoC WiFi Shield 2 para Arduino | × | 1 |
| Controlador de Motor de Passo PHPoC | × | 2 |
| Plotter XY Makeblock | × | 1 |
Sobre o Projeto
Demonstração
Se você é novo no Arduino, pode começar com:
Como Funciona
Quando o usuário toca na área de desenho na página web, as coordenadas XY do ponto de toque são enviadas ao Arduino. Após escalar as coordenadas, o Arduino move dois motores de passo para posicionar a caneta naquela coordenada. Durante o movimento, o Arduino envia continuamente a trajetória da caneta para a aplicação web, que desenha a trajetória no canvas.
Código Fonte
O código fonte inclui duas partes: código Arduino e código da Interface Web do Usuário.
Código Arduino
Este é o código Arduino, que roda em loop infinito para:
- Receber comandos da página web e executar tarefas de acordo com o comando.
- CMD_MOVE: mover a caneta para uma posição controlando dois motores de passo.
- CMD_PEN_UP: levantar a caneta alterando o ângulo do motor servo.
- CMD_PEN_DOWN: baixar a caneta alterando o ângulo do motor servo.
- Ler continuamente a posição atual da caneta e enviar para a página web.
Interface Web do Usuário
Veja o código na seção Código.
- Fornecer a interface do usuário.
- Tratar eventos do usuário e enviar comandos com coordenadas ao Arduino.
- Receber a trajetória do Arduino e desenhá-la na página web.
O arquivo remote_draw.php contém a Interface Web do Usuário. Ele precisa ser armazenado no PHPoC [WiFi] Shield. Para fazer upload do arquivo para o PHPoC [WiFi] Shield, siga estes passos:
- Copie o código abaixo e salve como
remote_draw.php. - Instale o PHPoC Debugger.
- Conecte o PHPoC ao PHPoC [WiFi] Shield via cabo micro-USB conforme esta instrução. O Arduino deve estar alimentado.
- Faça upload do arquivo
remote_draw.phppara o PHPoC [WiFi] Shield conforme esta instrução.
Como Executar
- Configure as informações de rede para o PHPoC shield ou PHPoC WiFi shield.
- Instale a Biblioteca PHPoC.
- Instale a Biblioteca de Expansão PHPoC.
- Compile e faça upload do código para o Arduino.
- Faça upload da interface web para o PHPoC [WiFi] shield.
- Abra o Monitor Serial e copie o endereço IP do PHPoC Shield.
- Acesse a Interface Web via Navegador:
https://ip_address_of_shield/remote_draw.php - Desenhe via Web.
Para comprar componentes eletrônicos, você pode encomendá-los em utsource.net.
O Melhor Kit Inicial Arduino para Iniciantes
Veja O Melhor Kit Arduino para Iniciantes.
Referências de Funções
- Serial.begin()
- Serial.println()
- delay()
- millis()
- for loop
- while loop
- if else
- loop()
- setup()
- String.toInt()
- String.substring()
- String.indexOf()
- String.remove()
- String.equals()
Código
Código Arduino
#include <Phpoc.h>
#include <PhpocExpansion.h>
#include <Servo.h>
#define MAX_X 55550 // unidade: passo
#define MAX_Y 68780 // unidade: passo
#define TOUCH_OFFSET 5000 // unidade: passo
#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 // em milissegundo
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; // direção do plotter XY quando motor X move para frente, depende da instalação, deve ser testado para determinar os valores
int forwardDirY = +1; // direção do plotter XY quando motor Y move para cima, depende da instalação, deve ser testado para determinar os valores
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);
// mover caneta para (0, 0)
stepX.stepGotoSW(0, -forwardDirX);
stepY.stepGotoSW(0, -forwardDirY);
xyWait();
stepX.setPosition(0);
stepY.setPosition(0);
// descomente este bloco para a primeira execução e altere os valores nas linhas 6, 7 deste arquivo e linha 33 do index.php de acordo com o valor no console da IDE
/*
// verificar passos máximos
stepX.stepGotoSW(1, forwardDirX);
stepY.stepGotoSW(1, forwardDirY);
xyWait();
// altere estes valores na linha 45 do 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(); // enviar posição atual para exibir na 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("Endereço do servidor WebSocket: ");
Serial.println(Phpoc.localIP());
Expansion.begin(460800);
servo.attach(8);
/**
* NOTA: Para a primeira execução:
* - descomente o último bloco em xyInit()
* - execute o código Arduino
* - altere os valores nas linhas 6, 7 deste arquivo e linha 33 do index.php de acordo com o valor no console da IDE.
**/
xyInit();
lastUpdateMillis = millis();
}
void loop() {
// aguardar novo cliente:
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();
}
Interface Web do Usuário (PHP)
<!DOCTYPE html>
<html>
<head>
<title>Arduino - PHPoC Shield - Plotter XY</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; }
#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; // em milissegundo
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; /* unidade: passo */
var touchPixelX = 0, touchPixelY = 0; /* unidade: 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"); /* para desenhar posição atual, recebida do plotter XY */
layer2 = document.getElementById("layer2"); /* para desenhar trajetória do plotter XY, recebida do plotter XY */
layer3 = document.getElementById("layer3"); /* para desenhar posição atual do toque, inserida pelo usuário na tela */
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];
/* converter unidade de passo para unidade de pixel */
plotterX = Math.round(plotterX * CANVAS_WIDTH / MAX_X);
plotterY = -Math.round(plotterY * CANVAS_HEIGHT / MAX_Y);
/* desenhar posição atual do plotter */
ctx1.clearRect(0, 0, CANVAS_WIDTH, -CANVAS_HEIGHT);
ctx1.beginPath();
ctx1.arc(plotterX, plotterY, 7, 0, 2*Math.PI);
ctx1.fill();
/* desenhar trajetória do plotter apenas quando caneta baixada */
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) {
// converter coordenada
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;
}
/* converter de pixel para passo */
var newTouchStepX = Math.round(touchPixelX / CANVAS_WIDTH * MAX_X);
var newTouchStepY = Math.round((-touchPixelY) / CANVAS_HEIGHT * MAX_Y);
if(type == "MOVE") {
/* verificar condição de atualização para evitar envio excessivo de dados ao 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); /* limpar apenas trajetória */
}
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>
Processo de manufatura
- Construa um Decodificador DTMF Usando Apenas um Arduino – Sem CIs Externos
- Sistema de Iluminação DMX Controlado via Web – Controlador Mestre Arduino
- Robô de Detecção Humana Baseado em Arduino Uno: Integração de Sensores Passo a Passo
- Controle USB para Jogos DIY com Arduino – Construa Seu Próprio Gamepad de Alto Desempenho
- SERENA: Sistema de Alarme Personalizado Arduino Mega 2560 com Touchscreen TFT LCD
- Veículo Remoto Controlado por Android com Raspberry Pi e Motor Shield
- Criar Robô Seguidor de Linha Autônomo com Arduino UNO
- Controle um Rover Arduino com Firmata e Gamepad Xbox One
- Arduino UNO com Shield WiFi: Exibição de Temperatura em Tempo Real na Web via Serial
- Proteja seu Arduino com Desbloqueio por Padrão Web – Configuração Fácil e Controle Remoto