Arduino Web-gesteuerter XY-Plotter mit Schrittmotor-Controller
Komponenten und Materialien
| Arduino UNO | × | 1 |
| PHPoC WiFi Shield 2 für Arduino | × | 1 |
| PHPoC Schrittmotor-Controller | × | 2 |
| Makeblock XY-Plotter | × | 1 |
Über dieses Projekt
Demonstration
Wenn Sie neu bei Arduino sind, können Sie mit folgenden Ressourcen starten:
- Arduino-Tutorials für Einsteiger
- Alles über Motoren für Arduino
Funktionsweise
Wenn ein Finger den Zeichenbereich auf der Webseite berührt, werden die XY-Koordinaten des Berührungspunkts an den Arduino gesendet. Nach der Skalierung der Koordinaten bewegt der Arduino zwei Schrittmotoren, um den Stift an diese Position zu bringen. Während der Bewegung sendet der Arduino kontinuierlich die Stift-Trajektorie an die Web-App, die diese auf dem Canvas darstellt.
Quellcode
Der Quellcode besteht aus zwei Teilen: Arduino-Code und Web-Benutzeroberflächen-Code.
Arduino-Code
Dieser Arduino-Code läuft in einer Endlosschleife und:
- Empfängt Befehle von der Webseite und führt entsprechende Aufgaben aus
CMD_MOVE: Bewegt den Stift an eine Position durch Steuerung zweier SchrittmotorenCMD_PEN_UP: Hebt den Stift durch Ändern des Servowinkels anCMD_PEN_DOWN: Senkt den Stift durch Ändern des Servowinkels ab- Liest kontinuierlich die aktuelle Stiftposition und sendet sie an die Webseite
Web-Benutzeroberfläche
Der Code befindet sich im Code-Bereich.
- Stellt die Benutzeroberfläche bereit
- Behandelt Benutzerereignisse und sendet Befehle mit Koordinaten an den Arduino
- Empfängt die Trajektorie vom Arduino und zeichnet sie auf der Webseite
remote_draw.php ist eine Datei, die die Web-Benutzeroberfläche enthält. Sie muss auf dem PHPoC [WiFi] Shield gespeichert werden. Zum Hochladen der Datei auf das PHPoC [WiFi] Shield führen Sie folgende Schritte aus:
- Kopieren Sie den untenstehenden Code und speichern Sie ihn als
remote_draw.php - Installieren Sie den PHPoC Debugger
- Verbinden Sie PHPoC über ein Micro-USB-Kabel mit dem PHPoC [WiFi] Shield gemäß dieser Anleitung. Beachten Sie, dass der Arduino mit Strom versorgt sein muss.
- Laden Sie die
remote_draw.php-Datei gemäß dieser Anleitung auf das PHPoC [WiFi] Shield hoch
Vorgehensweise
- Netzwerkinformationen für das PHPoC Shield oder PHPoC WiFi Shield konfigurieren
- PHPoC Library installieren
- PHPoC Expansion Library installieren
- Code kompilieren und auf den Arduino hochladen
- Web-Benutzeroberfläche auf das PHPoC [WiFi] Shield hochladen
- Seriellen Monitor öffnen und IP-Adresse des PHPoC Shields kopieren
- Web-Benutzeroberfläche über Webbrowser aufrufen:
https://<ip_adresse_des_shields>/remote_draw.php - Über das Web zeichnen
Um elektronische Komponenten zu kaufen, können Sie diese bei utsource.net bestellen.
Die besten Arduino-Starterkits für Einsteiger
Siehe die besten Arduino-Kits für Anfänger
Funktionsreferenzen
Serial.begin()Serial.println()delay()millis()for-Schleifewhile-Schleifeif elseloop()setup()String.toInt()String.substring()String.indexOf()String.remove()String.equals()
Code
Arduino-Code
#include <Phpoc.h>
#include <PhpocExpansion.h>
#include <Servo.h>
#define MAX_X 55550 // Einheit: Schritte
#define MAX_Y 68780 // Einheit: Schritte
#define TOUCH_OFFSET 5000 // Einheit: Schritte
#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 // in Millisekunden
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; // Richtung des XY-Plotters bei Vorwärtsbewegung Motor X, abhängig von der Installation
int forwardDirY = +1; // Richtung des XY-Plotters bei Aufwärtsbewegung Motor Y, abhängig von der Installation
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);
// Stift zur Position (0, 0) bewegen
stepX.stepGotoSW(0, -forwardDirX);
stepY.stepGotoSW(0, -forwardDirY);
xyWait();
stepX.setPosition(0);
stepY.setPosition(0);
// Für den ersten Lauf: Block auskommentieren und Werte in Zeile 6, 7 dieser Datei und Zeile 33 von index.php gemäß IDE-Konsole anpassen
/*
// Maximale Schritte prüfen
stepX.stepGotoSW(1, forwardDirX);
stepY.stepGotoSW(1, forwardDirY);
xyWait();
// Diese Werte in Zeile 45 von index.php ändern
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, 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(); // Aktuelle Position an Web senden
}
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("WebSocket-Server-Adresse: ");
Serial.println(Phpoc.localIP());
Expansion.begin(460800);
servo.attach(8);
/** HINWEIS: Für den ersten Lauf:
* - Letzten Block in xyInit() auskommentieren
* - Arduino-Code ausführen
* - Werte in Zeile 6, 7 dieser Datei und Zeile 33 von index.php gemäß IDE-Konsole anpassen.
**/
xyInit();
lastUpdateMillis = millis();
}
void loop() {
// Auf neuen Client warten:
PhpocClient client = server.available();
if(client) {
String data = client.readLine();
if(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);
while(stepX.getState() > 1 || stepY.getState() > 1) xyCheckUpdateToWeb();
penDown();
break;
case CMD_PEN_UP:
while(stepX.getState() > 1 || stepY.getState() > 1) xyCheckUpdateToWeb();
penUp();
break;
case CMD_MOVE:
xyGoto(x, y);
break;
}
}
}
xyCheckUpdateToWeb();
}
Web-Benutzeroberfläche (PHP)
<!DOCTYPE html>
<html>
<head>
<title>Arduino - PHPoC Shield - XY Plotter</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; // in Millisekunden
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; // Einheit: Schritte
var touchPixelX = 0, touchPixelY = 0; // Einheit: 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"); // Für aktuelle Position vom XY-Plotter
layer2 = document.getElementById("layer2"); // Für Trajektorie des XY-Plotters
layer3 = document.getElementById("layer3"); // Für aktuelle Touch-Position des Benutzers
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];
// Schritt-Einheit in Pixel-Einheit umrechnen
plotterX = Math.round(plotterX * CANVAS_WIDTH / MAX_X);
plotterY = -Math.round(plotterY * CANVAS_HEIGHT / MAX_Y);
// Aktuelle Position des Plotters zeichnen
ctx1.clearRect(0, 0, CANVAS_WIDTH, -CANVAS_HEIGHT);
ctx1.beginPath();
ctx1.arc(plotterX, plotterY, 7, 0, 2*Math.PI);
ctx1.fill();
// Trajektorie des Plotters nur bei gesenktem Stift zeichnen
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) {
// Koordinaten umrechnen
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;
}
// Von Pixel in Schritte umrechnen
var newTouchStepX = Math.round(touchPixelX / CANVAS_WIDTH * MAX_X);
var newTouchStepY = Math.round((-touchPixelY) / CANVAS_HEIGHT * MAX_Y);
if(type == "MOVE") {
// Aktualisierungsbedingung prüfen, um zu vermeiden, dass zu viele Daten an den Plotter gesendet werden
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); // Nur Trajektorie löschen
}
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>
Herstellungsprozess
- DTMF-Decoder mit Arduino bauen – ganz ohne zusätzlichen IC
- Web-gesteuertes DMX-Lichtsystem – Arduino Master Controller
- Arduino-Uno-Roboter zur Personenerkennung – Sensorintegration Schritt für Schritt
- DIY Arduino USB Gaming Controller – Bauen Sie Ihren eigenen High-Performance Gamepad
- SERENA: Eigenes Arduino Mega 2560 Alarmsystem mit TFT-LCD-Touchscreen
- Android-gesteuertes Roboterfahrzeug mit Raspberry Pi Motor Shield
- Autonomer Linienfolger-Roboter mit Arduino UNO
- Arduino Rover per Firmata & Xbox One Gamepad steuern
- Arduino UNO & WiFi Shield: Echtzeit-Temperaturanzeige im Web via Serial
- Arduino per Web-Pattern-Entsperrung sichern – Einfache Einrichtung & Fernsteuerung