今回は、ESP8266を使ってWebサーバを構成し、ブラウザ操作で LEGO Power Function IR Reciever を制御する赤外線信号を生成して LEGOを動かしてみたいと思います。
LEGO Power Functions IR Reciever はLEGO Power Function シリーズの電池ボックスと各種モータの間に中継することで赤外線リモコンでモータの回転速度を制御できるユニットです。
このIR Reciever の赤外線受光部、上部の黒いドーム状の部分に上面から赤外線LEDを対向させるように次のようなパーツをLEGOブロックに赤外LEDをホットボンドで固定して作成しました。
これをIR Reciever にかぶせて、確実に赤外線をIR Reciever が受信できるようにします。
赤外線LEDは今回は ESP8266 の 13ピンから220Ωの抵抗を介して ESP 13ピン → 220Ω → 赤外線LED → GND のように配線しています。
ブラウザからの制御を実現するためのソフトウェアに関しては色々調べてみたんですが、スマホのゲームなんかでよく使われる画面上に仮想のアナログスティックのようなものを表示しているものがありますが、あんな感じのをJavaScriptのライブラリを使って簡単に実現できるようなのがないかと探して、結局「enchant.js」というゲーム用のライブラリに行き着きました。この「enchant.js」はゲーム用のライブラリですが、自分が使いたかった仮想ジョイスティックが簡単に実現できるのと、ドキュメントが日本語で用意されているのがありがたがったので、こちらを利用させてもらうことにしました。
ブラウザ側JavaScriptプログラムとESP8266側Webサーバプログラム間の通信はWebSocketを使ってコネクションを張ることにします。WebSocketのライブラリはこちらを利用しました。
ESP8266にはSPIFFSというフラッシュメモリの一部をファイルを格納する領域として使える機能があるので、このSPIFFSにHTMLやJavaScriptファイルを保存して、ESP8266でWebサーバを実装します。と、いってもサンプルプログラムにほぼそのままWebサーバのサンプルなどがありますので、このあたりは比較的簡単に実現できます。
※SPIFFSの使い方についてはこちらとかを参考にしてください。
次にLEGOの赤外線リモコンですが、実はこの仕様は LEGO社が公開してくれていて、さらに 従来のArduinoでこのLEGOの赤外線リモコンを利用するためのライブラリをすでに作成してくれている方が何人かいます。そのなかで、今回はこちらのライブラリを利用させてもらうことにしました。従来のArduino用のライブラリですが、ライブラリ内で特殊なことは行っていないのでESP8266でも使えます。ただし、信号のタイミングをdelay()関数で作っているのでメインループで処理をぐるぐる回すとだんだん遅れというか処理が詰まってきます。他の処理(ループ)の何回に一回だけ呼ぶ、といった工夫をしてやる必要があります。
以下、ESP8266 for Arduino のスケッチです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <ESP8266WiFi.h> | |
#include <WiFiClient.h>i | |
#include <ESP8266WiFiMulti.h> | |
#include <ESP8266WebServer.h> | |
#include <ESP8266mDNS.h> | |
#include <FS.h> | |
#include <WebSocketsServer.h> | |
#include <legopowerfunctions.h> | |
#define DEBUG_PORT Serial | |
#define PFIR_PORT 13 | |
#define PFIR_CH CH1 | |
const char* ssid = "SSID"; | |
const char* password = "********"; | |
const char* host = "ESP8266"; | |
const long interval = 200; | |
char msg[20]; | |
long previousMills = 0; | |
int speedX = 0, speedY = 0; | |
ESP8266WiFiMulti WiFiMulti; | |
ESP8266WebServer server = ESP8266WebServer(80); | |
WebSocketsServer webSocket = WebSocketsServer(81); | |
LEGOPowerFunctions pf(PFIR_PORT); | |
void PFIR_setSpeed(int speedA, int speedB) | |
{ | |
int pwmA = 0, pwmB = 0; | |
if (speedA > 7) speedA = 7; | |
if (speedA < -7) speedA = -7; | |
if (speedB > 7) speedB = 7; | |
if (speedB < -7) speedB = -7; | |
if (speedA >= 0) | |
pwmA = speedA; | |
else | |
pwmA = 0x10 + speedA; | |
if (speedB >= 0) | |
pwmB = speedB; | |
else | |
pwmB = 0x10 + speedB; | |
pf.ComboPWM(pwmA, pwmB, PFIR_CH); | |
} | |
void returnOK() | |
{ | |
server.send(200, "text/plain", ""); | |
} | |
void returnFail(String msg) | |
{ | |
server.send(500, "text/plain", msg + "\r\n"); | |
} | |
bool loadFromSPIFFS(String path) | |
{ | |
String dataType = "text/plain"; | |
if (path.endsWith("/")) | |
path += "index.html"; | |
if (path.endsWith(".src")) path = path.substring(0, path.lastIndexOf(".")); | |
else if (path.endsWith(".html")) dataType = "text/html"; | |
else if (path.endsWith(".htm")) dataType = "text/html"; | |
else if (path.endsWith(".css")) dataType = "text/css"; | |
else if (path.endsWith(".js")) dataType = "application/javascript"; | |
else if (path.endsWith(".png")) dataType = "image/png"; | |
else if (path.endsWith(".gif")) dataType = "image/gif"; | |
else if (path.endsWith(".jpg")) dataType = "image/jpeg"; | |
else if (path.endsWith(".ico")) dataType = "image/x-icon"; | |
else if (path.endsWith(".xml")) dataType = "text/xml"; | |
else if (path.endsWith(".pdf")) dataType = "application/pdf"; | |
else if (path.endsWith(".zip")) dataType = "application/zip"; | |
File dataFile = SPIFFS.open(path.c_str(), "r"); | |
if (!dataFile) | |
return false; | |
if (server.hasArg("download")) | |
dataType = "application/octet-stream"; | |
if (server.streamFile(dataFile, dataType) != dataFile.size()) | |
{ | |
DEBUG_PORT.println("Sent less data than expected!"); | |
} | |
dataFile.close(); | |
return true; | |
} | |
void handleNotFound() | |
{ | |
if (loadFromSPIFFS(server.uri())) | |
return; | |
String message = "URI: "; | |
message += server.uri(); | |
message += "\nMethod: "; | |
message += (server.method() == HTTP_GET) ? "GET" : "POST"; | |
message += "\nArguments: "; | |
message += server.args(); | |
message += "\n"; | |
for (uint8_t i = 0; i<server.args(); i++) { | |
message += " NAME:" + server.argName(i) + "\n VALUE:" + server.arg(i) + "\n"; | |
} | |
server.send(404, "text/plain", message); | |
DEBUG_PORT.print(message); | |
} | |
void handleWebSocket(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { | |
switch (type) { | |
case WStype_DISCONNECTED: | |
DEBUG_PORT.printf("[%u] Disconnected!\n", num); | |
break; | |
case WStype_CONNECTED: { | |
IPAddress ip = webSocket.remoteIP(num); | |
DEBUG_PORT.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); | |
// send message to client | |
webSocket.sendTXT(num, "Connected"); | |
} | |
break; | |
case WStype_TEXT: | |
DEBUG_PORT.printf("[%u] get Text: %s\n", num, payload); | |
if (payload[0] == '#') { | |
uint8_t buf[3]; | |
buf[2] = 0x00; | |
buf[0] = payload[1]; | |
buf[1] = payload[2]; | |
int x = (int)strtol((const char*)&buf[0], NULL, 16); | |
buf[0] = payload[3]; | |
buf[1] = payload[4]; | |
int y = (int)strtol((const char*)&buf[0], NULL, 16); | |
buf[0] = payload[5]; | |
buf[1] = payload[6]; | |
int z = (int)strtol((const char*)&buf[0], NULL, 16); | |
DEBUG_PORT.print(x); | |
DEBUG_PORT.print(", "); | |
DEBUG_PORT.print(y); | |
DEBUG_PORT.print(", "); | |
DEBUG_PORT.print(z); | |
DEBUG_PORT.print(", "); | |
int speedA = x; | |
int speedB = y; | |
speedX = x; | |
speedY = y; | |
DEBUG_PORT.print(speedA); | |
DEBUG_PORT.print(", "); | |
DEBUG_PORT.print(speedB); | |
DEBUG_PORT.println(); | |
} | |
break; | |
case WStype_BIN: | |
DEBUG_PORT.printf("[%u] get binary length: %u\n", num, length); | |
break; | |
} | |
} | |
void setup() | |
{ | |
DEBUG_PORT.begin(115200); | |
DEBUG_PORT.setDebugOutput(true); | |
DEBUG_PORT.println(); | |
DEBUG_PORT.println("Program Started."); | |
for (int i = 0; i < 20; i++) | |
msg[i] = 0; | |
for (uint8_t t = 4; t > 0; t--) { | |
DEBUG_PORT.printf("[SETUP] BOOT WAIT %d...\n", t); | |
DEBUG_PORT.flush(); | |
delay(1000); | |
} | |
pf.ComboPWM(PWM_FLT, PWM_FLT, PFIR_CH); | |
SPIFFS.begin(); | |
Dir dir = SPIFFS.openDir("/"); | |
while (dir.next()) | |
{ | |
String filename = dir.fileName(); | |
size_t fileSize = dir.fileSize(); | |
DEBUG_PORT.printf("FS File: %s\n", filename.c_str()); | |
} | |
WiFiMulti.addAP(ssid, password); | |
while (WiFiMulti.run() != WL_CONNECTED) { | |
delay(100); | |
} | |
DEBUG_PORT.print("IP Address: "); | |
DEBUG_PORT.println(WiFi.localIP()); | |
webSocket.begin(); | |
webSocket.onEvent(handleWebSocket); | |
server.onNotFound(handleNotFound); | |
if (MDNS.begin(host)) | |
{ | |
DEBUG_PORT.println("MDNS responder started"); | |
DEBUG_PORT.print("You can now connect to http://"); | |
DEBUG_PORT.print(host); | |
DEBUG_PORT.println(".local"); | |
} | |
server.begin(); | |
DEBUG_PORT.println("HTTP server started"); | |
MDNS.addService("http", "tcp", 80); | |
MDNS.addService("ws", "tcp", 81); | |
} | |
void loop() | |
{ | |
unsigned long currentMillis = millis(); | |
webSocket.loop(); | |
server.handleClient(); | |
if (currentMillis - previousMills >= interval) { | |
previousMills = currentMillis; | |
PFIR_setSpeed(speedX, speedY); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<title>Browserbot</title> | |
<script src="./js/lib/enchant.js"></script> | |
<script src="./js/lib/ui.enchant.js"></script> | |
<script src="./js/main.js"></script> | |
</head> | |
<body> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enchant(); | |
var connection = new WebSocket('ws://' + location.hostname + ':81/', ['arduino']); | |
var recieved = '0'; | |
connection.onopen = function () { | |
//connection.send('Connect ' + new Data); | |
}; | |
connection.onerror = function () { | |
console.log('WebSocket Error ', error); | |
}; | |
connection.onmessage = function (e) { | |
console.log('Message: ' + e.data); | |
}; | |
window.onload = function () { | |
function sendPad(padX, padY) { | |
var x = parseInt(padX).toString(16); | |
var y = parseInt(padY).toString(16); | |
if (x.length < 2) { | |
x = '0' + x; | |
} | |
if (y.length < 2) { | |
y = '0' + y; | |
} | |
var xy = '#' + x + y + '00'; | |
connection.send(xy); | |
} | |
var game = new Core(320, 320); | |
game.fps = 30; | |
game.rootScene.backgroundColor = "#fff"; | |
game.onload = function () { | |
// アナログパッド | |
var pad = new APad(); | |
pad.moveTo(0, game.rootScene.height - 100); | |
game.rootScene.addChild(pad); | |
// フレーム更新イベント | |
var sense = 10; | |
game.rootScene.addEventListener('enterframe', function () { | |
// アナログパッド処理 | |
if (pad.isTouched) { | |
sendPad(pad.vx * sense, pad.vy * sense); | |
} | |
}); | |
}; | |
game.start(); | |
} |
0 件のコメント:
コメントを投稿