ArduinoとPHPoC WiFiシールドで作るWeb操作XYプロッター(ステッピングモーター制御)
必要な部品と材料
| 部品 | 数量 |
|---|---|
| Arduino UNO | 1 |
| PHPoC WiFi Shield 2 for Arduino | 1 |
| PHPoC Stepper Motor Controller | 2 |
| Makeblock XY Plotterキット | 1 |
プロジェクト概要
このプロジェクトでは、WebブラウザからXYプロッターをリアルタイムで操作できるシステムを構築します。指でWebページ上の描画エリアをタッチすると、その座標がArduinoに送信され、2つのステッピングモーターがペンを該当位置に移動させます。移動中はArduinoからWebアプリへ軌跡データが連続送信され、キャンバス上に描画されます。
動作原理
Webページ上で指が描画エリアに触れると、タッチポイントのXY座標がArduinoへ送信されます。座標をスケーリングした後、Arduinoは2つのステッピングモーターを制御してペンをその座標へ移動させます。移動中、Arduinoはペンの軌跡を継続的にWebアプリへ送信し、Webアプリ側でキャンバス上に描画します。
ソースコード構成
ソースコードは「Arduino側コード」と「Webユーザーインターフェース(PHP)」の2部構成です。
Arduinoコード
無限ループ内で以下の処理を実行します:
- Webページからのコマンド受信とタスク実行
CMD_MOVE:2つのステッピングモーターでペンを指定位置へ移動CMD_PEN_UP:サーボモーター角度変更でペンを上げるCMD_PEN_DOWN:サーボモーター角度変更でペンを下げる- ペンの現在位置を継続的に読み取り、Webページへ送信
Webユーザーインターフェース(remote_draw.php)
- ユーザーインターフェースの提供
- ユーザーイベント処理と座標付きコマンドのArduinoへの送信
- Arduinoからの軌跡データ受信とWebページ上での描画
remote_draw.phpファイルをPHPoC WiFiシールドにアップロードする必要があります。手順:
- コードをコピーして
remote_draw.phpとして保存 - PHPoC Debuggerをインストール
- マイクロUSBケーブルでPHPoCをPHPoC WiFiシールドに接続(Arduinoへの給電必須)
- PHPoC Debuggerで
remote_draw.phpをシールドへアップロード
セットアップ手順
- PHPoCシールドのネットワーク設定
- PHPoCライブラリのインストール
- PHPoC Expansionライブラリのインストール
- Arduinoへコードをコンパイル・アップロード
- WebユーザーインターフェースをPHPoC WiFiシールドへアップロード
- シリアルモニタを開き、PHPoCシールドのIPアドレスを確認
- Webブラウザで
https://<シールドのIPアドレス>/remote_draw.phpにアクセス - Web上で描画開始
コード
Arduinoコード
#include <Phpoc.h>
#include <PhpocExpansion.h>
#include <Servo.h>
#define MAX_X 55550
#define MAX_Y 68780
#define TOUCH_OFFSET 5000
#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
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;
int forwardDirY = +1;
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);
stepX.stepGotoSW(0, -forwardDirX);
stepY.stepGotoSW(0, -forwardDirY);
xyWait();
stepX.setPosition(0);
stepY.setPosition(0);
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 * deltaX;
long speedY = SPEED_Y_COEF * 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;
lastUpdateMillis = curMillis;
preX = curX;
preY = curY;
sendPositionToWeb();
}
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 address : ");
Serial.println(Phpoc.localIP());
Expansion.begin(460800);
servo.attach(8);
xyInit();
lastUpdateMillis = millis();
}
void loop() {
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ユーザーインターフェース(PHP/HTML/JavaScript)
<!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: 0 auto; position: relative; background-color: #FFFFFF; }
canvas { position: absolute; left: 0; top: 0; }
#layer1 { z-index: 2; }
#layer2 { z-index: 1; }
#layer3 { z-index: 0; }
</style>
<script>
var PEN_STATE_UP = 0, PEN_STATE_DOWN = 1;
var CMD_PEN_UP = 0, CMD_PEN_DOWN = 1, CMD_MOVE = 2;
var RESOLUTION = 1000, MIN_UPDATE_INTERVAL = 100;
var MAX_X = 55550, MAX_Y = 68780;
var CANVAS_WIDTH = 0, CANVAS_HEIGHT = 0;
var ws = null;
var canvas = null, layer1 = null, layer2 = null, layer3 = null;
var ctx1 = null, ctx2 = null, ctx3 = null;
var touchState = 0, touchStepX = 0, touchStepY = 0;
var touchPixelX = 0, touchPixelY = 0;
var plotterX = 0, plotterY = 0, plotterPenState = PEN_STATE_UP;
var lastUpdateMillis, buffer = "";
function init() {
canvas = document.getElementById("canvas");
layer1 = document.getElementById("layer1");
layer2 = document.getElementById("layer2");
layer3 = document.getElementById("layer3");
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();
lastUpdateMillis = new Date().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];
plotterX = Math.round(plotterX * CANVAS_WIDTH / MAX_X);
plotterY = -Math.round(plotterY * CANVAS_HEIGHT / MAX_Y);
ctx1.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx1.beginPath();
ctx1.arc(plotterX, plotterY, 7, 0, 2*Math.PI);
ctx1.fill();
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 = ws.onclose = ws.onmessage = null;
ws = null;
}
function wc_onclick() {
if(ws == null) {
ws = new WebSocket("ws:///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) {
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;
}
var newTouchStepX = Math.round(touchPixelX / CANVAS_WIDTH * MAX_X);
var newTouchStepY = Math.round((-touchPixelY) / CANVAS_HEIGHT * MAX_Y);
if(type == "MOVE") {
var isUpdate = false;
var curMillis = new Date().getTime();
if((curMillis - lastUpdateMillis) > MIN_UPDATE_INTERVAL) isUpdate = true;
var deltaX = newTouchStepX - touchStepX;
var deltaY = newTouchStepY - touchStepY;
var dist = Math.sqrt(deltaX*deltaX + deltaY*deltaY);
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); }
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 = layer2.width = layer3.width = CANVAS_WIDTH;
layer1.height = layer2.height = 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>
製造プロセス
- ArduinoだけでDTMFデコーダーを構築 – 外付けIC不要
- Web制御DMX照明システム:Arduino Megaマスターコントローラー
- Arduino Unoベースの人間検知レスキューロボット:センサー統合ステップバイステップ
- DIY Arduino USBゲームコントローラー - 高性能ゲームパッドを自作
- SERENA:Arduino Mega 2560とTFT LCDタッチスクリーン搭載カスタムアラームシステム
- Android加速度センサーで操作するラジコン車両:Raspberry Pi・Arduino・モーターシールド連携
- Arduino UNOで作る完全自律ライントラッカーロボット ─ 入門に最適な自作キット
- FirmataとXbox OneコントローラーでArduinoローバーをシームレス操作
- Arduino UNO + WiFiシールドで温度をWebリアルタイム表示 — シリアル出力だけでグラフ化
- ArduinoをWebパターンロックで保護 - 簡単セットアップとリモート制御
- Arduino Mega 2560搭載 総合トレーニングキット – センサー、モーター、カスタムPCB完備