概要
テレビのリモコンが壊れたので同じやつを買おうとしたら意外と高かったので、Arduinoで自作してAndroid TVから制御できるようにしたら意外と便利だったっていう話です。
一つ一つの技術はかなり枯れているので、何をいまさら…と感じる方もいると思いますが、ハードウェアよりソフトウェアの知識に偏っていた自分的には結構学びがあったのでメモしました。
使ったもの
- Nexus Player
- Arduino Uno Rev3
- USBケーブル(A-Bケーブル)
- 秋月に行ったらArduinoとセットで売ってた
- http://akizukidenshi.com/catalog/g/gM-07385/
- USB変換(microBオス - Aメス)
- ハブになってるやつを持ってます。個人的にはAndroid TVに必須のアイテム。
- 赤外線LED
- ブレッドボード(なくてもいい)
- ジャンプワイヤー×2(なくてもいい)
Arduino
https://www.arduino.cc/
Arduinoは触ったことあるけど大体忘れた…という感じです。
とりあえず、Arduino-IRremoteというライブラリを使用。
https://github.com/z3t0/Arduino-IRremote
そしたらデフォルトで存在する(?)RobotIRremoteとバッティングして動かなかったので、RobotIRremoteを削除。
電子回路の構成としては、3番ピンとGNDを、それぞれ赤外線LEDのアノードとカソードに接続しました。
そのままだとちょっと届かなかったので、ジャンプワイヤーとブレッドボードを使いました。
(Arduinoをあんまり理解していないので、3番ピンである理由がArduino-IRremoteの仕様なのかArduinoの仕様なのか分からず、別のピンに変更可能なのかも分らなかったので、素直にそのままにしました )
(あと、本当は抵抗を挟んだほうがいいかもしれないです。今回は家にあった詳細不明の赤外線LEDを使ったので、適切な抵抗が分からず、そのまま接続しています )
自分のテレビはSHARPのAQUOSなので、以下のような感じで、Serialから受け取った値をそのまま信号として発信するようにしました。
#include <IRremote.h>
IRsend irsend;
void setup() {
Serial.begin(9600);
}
void loop() {
int input;
if (Serial.available() >= 2) {
input = Serial.read() << 8;
input |= Serial.read();
irsend.sendSharpRaw(input, 15);
}
}
これで電源のオンオフとかができたのでひとまずOK
Android TV
上で作った装置はテレビの前に放置して、常時起動しているAndroid TVからArduinoに命令を送るようにすることで、家中どこからでも(あるいは外からでも)信号を送れるようにしたいと思いました。
(だったら最初からRaspberry Pi買えばいいじゃんみたいな話もあると思いますが、今回はAndroidを触りたかったので… )
Nexus Playerは通常のAndroid端末と違い、USB端子とは別で電源を供給するので、Arduinoとつないだままでも電源が確保できるという点もGoodです。
Arduinoとシリアル通信
最初usb-serial-for-androidっていうのを使いました。
https://github.com/mik3y/usb-serial-for-android
しかしなぜかうまく動かず、結局FTDriverっていうのを参考に、自分で作成しました。
https://github.com/ksksue/FTDriver
まず、対象となるArduinoのvendorIdとproductIdを取得します。これにより、別のUSB機器と区別することができます。
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
HashMap<String, UsbDevice> map = manager.getDeviceList();
for (HashMap.Entry<String, UsbDevice> entry : map.entrySet()) {
int vendorId = entry.getValue().getVendorId();
int productId = entry.getValue().getProductId();
// ...
}
上で述べたライブラリの場合、動作可能なvendorIdとproductIdのセットがあらかじめ設定されているので、それに含まれているボードなら動くみたいです。今回は、個人的なものなので、手元にあるArduinoで動けばいいやということで、以下のようにべた書きしました
if (vendorId == 0x2A03) {
device = entry.getValue();
break;
}
このようにしてdeviceが取得出来たら、初回のみ動的パーミッションが必要なので、必ず書きます。
(最初これでハマっていた)
if (!manager.hasPermission(device)) {
manager.requestPermission(device, PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0));
finish();
return;
}
ここまでできたら、あとは上で述べたFTDriverを使って、通信をします。
(ただ、FTDriverはもうメンテされていないらしく、複数端末をつなげたりするとバグるので、FTDIDriverという名前で作り直しました)
FTDIDriver serial = new FTDIDriver(manager);
serial.begin(FTDIDriver.BAUD9600);
あとは、SHARPに対応したコードを送ります。
参考:http://lirc.sourceforge.net/remotes/sharp/GA538WJSA
public static final int KEY_POWER = 0x41A2;
public static byte[] getBytes(int sharpCode) {
byte[] b = new byte[2];
b[0] = (byte) (sharpCode / 256);
b[1] = (byte) (sharpCode % 256);
return b;
}
serial.write(getBytes(KEY_POWER), 2);
これでボタンを押したらチャンネルを変えたりできるようになりました
Jettyで常駐
http://www.eclipse.org/jetty/
ここまでだと、Android TVの画面を見ないと操作できないので、実用に耐えかねます。
そこで、Jettyを用いることで、httpサーバを立てて、RESTなAPIで操作できるようになれば、PCやスマホから操作できて便利です。
(余談ですが、個人的にはhttpで指令を受けるインタフェースは、Android TVにおいてかなり主要になってくるのではないかと考えています)
dependencies {
// ...
compile 'org.eclipse.jetty:jetty-server:9.2.17.v20160517'
compile 'com.android.support:multidex:1.0.1'
}
Jettyを入れるとメソッド数の限界を超えるので、MultiDexも使います。
次にJettyのHandlerを実装して、特定のコマンドが来たらArduinoに送信、それ以外はassetsにあるファイルを返すようにします。
public class JettyHandler extends AbstractHandler {
private final Context context;
private final FTDIDriver serial;
public JettyHandler(Context context, FTDIDriver serial) {
this.context = context;
this.serial = serial;
}
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
if (target.equals("/")) target = "/index.html";
if (target.equals("/command")) {
Uri uri = Uri.parse("scheme://host/?" + request.getQueryString());
String command = uri.getQueryParameter("c");
try {
int code = Integer.parseInt(command, 16);
serial.write(getBytes(code), 2); // ここでArduinoに送信しています
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
baseRequest.setHandled(true);
response.getWriter().println("{\"result\":\"ok\"}");
} catch (NumberFormatException e) {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
baseRequest.setHandled(true);
response.getWriter().println("{\"result\":\"bad request\"}");
}
} else { // コマンド以外はassetsのファイルを返します
try {
InputStream is = context.getAssets().open(target.substring(1));
response.setContentType(getContentType(target));
response.setStatus(HttpServletResponse.SC_OK);
baseRequest.setHandled(true);
OutputStream os = response.getOutputStream();
rw(is, os);
} catch (IOException e) {
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
baseRequest.setHandled(true);
response.getWriter().println("<h1>Not Found</h1>");
}
}
}
private static void rw(InputStream in, OutputStream out) {
try {
while (true) {
int ch = in.read();
if (ch < 0) break;
out.write(ch);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static String getContentType(String fileName) {
if (fileName.endsWith(".txt")) {
return "text/plain;charset=utf-8";
} else if (fileName.endsWith(".html")) {
return "text/html;charset=utf-8";
} else if (fileName.endsWith(".xml")) {
return "text/xml;charset=utf-8";
} else if (fileName.endsWith(".js")) {
return "text/javascript;charset=utf-8";
} else if (fileName.endsWith(".css")) {
return "text/css;charset=utf-8";
} else if (fileName.endsWith(".gif")) {
return "image/gif";
} else if (fileName.endsWith(".jpg")) {
return "image/jpeg";
} else if (fileName.endsWith(".png")) {
return "image/png";
} else if (fileName.endsWith(".pdf")) {
return "application/pdf";
} else if (fileName.endsWith(".json")) {
return "application/json;charset=utf-8";
} else {
return "*/*";
}
}
}
ここまで出来たら、serverを起動します。(別スレッドで)
server = new Server(8080);
server.setHandler(new JettyHandler(MainActivity.this, serial));
try {
server.start();
server.join();
} catch (Exception e) {
e.printStackTrace();
}
そしたら、index.htmlをassets内に用意し、コマンドを呼び出します。
<input type="button" value="電源" class="command" code="41A2" />
$(function() {
$(".command").click(function() {
$.ajax({
url: "command?c=" + $(this).attr("code"),
cache : false
}).done(function(data) {
navigator.vibrate(10);
}).fail(function(data) {
alert("error: " + data['result']);
});
});
});
これで、Nexus PlayerのIPアドレスの8080ポートにブラウザでアクセスすれば、PCやスマホからテレビをコントロールできるようになりました
さいごに
今回は、Android TVなので(?)テレビを操作しましたが、実際にはエアコンや照明なども操作できると思います。枯れた技術ではありますが、割と安くて簡単にできるので、余っているAndroid端末があればオススメします (電源の問題はありますが )
何か補足やアドバイスがあればコメントしていただけると嬉しいです