2016年7月9日土曜日

ESP8266でLEGO Power Functions IR を動かしてみる

 今回のがらくたは 例のESP8266にUSBコネクタIFやブート(書き込み)スイッチ等を使いやすく実装した スイッチサイエンスさんの ESPr Developer 開発ボードと LEGO Power Function シリーズの赤外線リモコン受信モジュール LEGO 8884 Power Function IR Reciever です。

 今回は、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 のスケッチです。

#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);
}
}
view raw LEGO_PFIR.ino hosted with ❤ by GitHub
 以下がHTML。適宜、enchant.jsのライブラリやイメージファイル、次のmain.jsプログラムを配置する必要があります。

<!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>
view raw index.html hosted with ❤ by GitHub
 以下が JavaScript。

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();
}
view raw main.js hosted with ❤ by GitHub

0 件のコメント:

コメントを投稿