M5StickCで赤外線リモコンを作ります。
赤外線を受信してエンコードしたり、エンコードしたデータを送信したりします。
Arduinoのライブラリを活用します。
割と定番のようで、多数の有志の方々が、同様の記事を書かれていまして、それらを参考にさせていただきました。
また、ブラウザから、エンコードした信号を複数保存したり、保存した信号のうち1つを選択して送信したり、複数の信号をまとめて送信したりします。
ちなみに、テレビのリモコンだけでなく、エアコンのリモコンのようにリモコン信号が長すぎても大丈夫です。
全体構成
構成する各要素について説明しておきます。
M5Stick-C
今回の主題であるM5Stick-Cです。CPUとしてESP32を搭載しています。
最初から、赤外線送信機を搭載しています。
赤外線受信の方は、M5StackのUnit IRを使いました。M5StickCには、Grove端子がついているので、これがちょうどよかったです。
M5Stack : Unit IR
https://docs.m5stack.com/#/en/unit/ir
あと、やってみてわかったのですが、M5Stick-Cに搭載されている赤外線送信機の通信距離は非常に短いです。環境によって違うのかも知れませんが、数十cmしか飛びませんでした。
そこで、以下の赤外線送信機を使うようにしました。
Grove - Inframed Emitter
http://wiki.seeedstudio.com/Grove-Infrared_Emitter/
ちなみに、M5Stick-CのGrove端子は赤外線受信機で使ってしまっているので、M5Stick-Cの反対側の2.54mmピン端子を使いました。
また、赤外線送受信の操作は、外部からのGET呼び出しで行うようにしています。
Arduinoで動作するaRestという簡易なRESTサーバを使わせていただきました。
marcoschwartz/aREST
https://github.com/marcoschwartz/aREST
Webページ
WebブラウザからM5Stick-Cに対して、赤外線送信や受信を要求するためのWebページです。
エンコード化された赤外線信号は、ブラウザのHTML5の機能を使ってローカルストレージに保存しておきます。
また、M5Stick-Cに対する赤外線送信要求や赤外線受信要求は、ブラウザのJavascriptから直接要求するようにしました。
(とりあえず、今回は、手っ取り早く動かすことを優先しましたが、次回はWebサーバを立ち上げて、サーバ側で赤外線信号を覚えておくようにする予定です)
M5Stick-Cの構成
使うのは以下の通りです。
Arduino IDEのボードマネージャからインストール
・M5Stick-C
ライブラリマネージャからインストール
・IRremoteESP8266
・aRest
利用するGPIO
・LED(デジタル出力) GPIO_NUM_10
・赤外線受信(デジタル入力) GPIO_NUM_33
・赤外線送信(デジタル出力) GPIO_NUM_26 または GPIO_NUM_9
赤外線受信(デジタル入力)のGPIO_NUM_33 は、M5Stick-CのGrove端子に割り当たっているピンです。
赤外線送信(デジタル出力)は2つの選択肢があります。M5Stick-C内蔵の赤外線送信機であれば9を、2.54mmピン端子を使う場合は26を選択します。
ここでちょっとずるいことをしています。
aRESTを改造しています。
aRESTは、GET呼び出しを待ち受けることができるのですが、戻り値をnumber型しか返せません。
いろんな値を返したいので、String型を返すようにしています。
ちょっと強引でしたが、これしか思いつきませんでした。
以下のファイルを改造します。
[ユーザのDocuments]\Arduino\libraries\aREST\aREST.h
$ diff aREST.h aREST-original.h
1490c1490
< String result = functions[value](arguments);
---
> int result = functions[value](arguments);
1495c1495
< addStringToBuffer(result.c_str(), true);
---
> addToBuffer(result, true);
1578c1578
< void function(char * function_name, String (*f)(String)){
---
> void function(char * function_name, int (*f)(String)){
2015c2015
< String (*functions[NUMBER_FUNCTIONS])(String);
---
> int (*functions[NUMBER_FUNCTIONS])(String);
Arduinoのソースコードです。
#include <IRrecv.h>
#include <IRsend.h>
#include <IRac.h>
#include <IRutils.h>
#include <WiFi.h>
#include <WiFiServer.h>
#include <aREST.h>
#include <M5StickC.h>
// Create aREST instance
aREST rest = aREST();
#define LED_PIN GPIO_NUM_10
#define IR_RECV_PIN GPIO_NUM_33
//#define IR_SEND_PIN GPIO_NUM_9
#define IR_SEND_PIN GPIO_NUM_26
IRrecv irrecv(IR_RECV_PIN, 1024, 50, true);
decode_results results;
IRsend irsend(IR_SEND_PIN);
#define IR_MAX_SEND_DATA 300
uint16_t send_data[IR_MAX_SEND_DATA];
uint16_t send_len = 0;
unsigned long recvStartTime;
unsigned long recvDuration = 0;
// WiFi parameters
const char* ssid = "【WiFiのSSID】";
const char* password = "【WiFiのパスワード】";
WiFiServer server(80);
void print_screen(String message, int font_size = 2){
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 0);
M5.Lcd.setTextSize(font_size);
M5.Lcd.println(message);
}
void print_screen_next(String message){
M5.Lcd.println(message);
}
void setup()
{
M5.begin();
M5.IMU.Init();
M5.Axp.ScreenBreath(9);
M5.Lcd.setRotation(3);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.println("[M5StickC]");
delay(500);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
irsend.begin();
// Start Serial
Serial.begin(9600);
// Init variables and expose them to REST API
// Function to be exposed
rest.function("led",ledControl);
rest.function("start", irStart);
rest.function("stop", irStop);
rest.function("send", irSend);
rest.function("get", irGet);
// Give name & ID to the device (ID should be 6 characters long)
rest.set_id("0001");
rest.set_name("esp32");
// Connect to WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
// Start the server
server.begin();
Serial.println("Server started");
// Print the IP address
Serial.println(WiFi.localIP());
M5.Lcd.println(WiFi.localIP());
}
void loop() {
M5.update();
if ( M5.BtnA.wasReleased() ) {
// M5Stick-Cのボタンが押下されたら、直近のリモコン信号を送信
Serial.println("BtnA.released");
print_screen("BtnA.released start", 2);
if( send_len > 0 )
irsend.sendRaw((uint16_t*)send_data, send_len, 38);
print_screen_next("BtnA.released end");
}
if (irrecv.decode(&results)) {
// リモコン信号の受信が受信された
irrecv.resume(); // Receive the next value
if( results.rawlen <= IR_MAX_SEND_DATA ){
ir_stop();
// 受信したリモコン信号をバッファに格納
uint16_t * result = resultToRawArray(&results);
send_len = getCorrectedRawLength(&results);
for( int i = 0 ; i < send_len ; i++ )
send_data[i] = result[i];
delete[] result;
Serial.println("IR received");
print_screen_next("IR received");
}else{
Serial.println("IR size over");
print_screen_next("IR size over");
}
Serial.print(resultToHumanReadableBasic(&results));
String description = IRAcUtils::resultAcToString(&results);
if (description.length())
Serial.println("Mesg Desc.: " + description);
// Serial.println(resultToTimingInfo(&results));
Serial.println(resultToSourceCode(&results));
delay(100);
}
if( recvDuration > 0 ){
// リモコン受信待ち時間タイムアウト
unsigned long elapsed = millis() - recvStartTime;
if( elapsed >= recvDuration ){
Serial.println("Expired");
ir_stop();
}
}
// Handle REST calls
WiFiClient client = server.available();
if (client) {
// GET呼び出しを検知
for( int i = 0 ; i < 10000; i += 10 ){
if(client.available()){
// GET呼び出しのコールバック呼び出し
rest.handle(client);
return;
}
delay(10);
}
// まれにGET呼び出し受付に失敗するようです。
Serial.println("timeout");
}
}
// Custom function accessible by the API
String ledControl(String command) {
Serial.println("ledControl called");
// Get state from command
int state = command.toInt();
digitalWrite(LED_PIN, state);
return "OK";
}
String irSend(String command) {
Serial.println("irSend called");
send_len = hexstr2array(command, send_data, IR_MAX_SEND_DATA);
if( send_len > 0 ){
print_screen("IR send", 2);
Serial.println(send_data[0]);
Serial.println("...");
Serial.println(send_data[send_len - 1]);
irsend.sendRaw((uint16_t*)send_data, send_len, 38);
return "OK";
}else{
return "NG";
}
}
void ir_start(unsigned long duration){
if( duration == 0 ){
ir_stop();
}else{
if( recvDuration == 0 ){
irrecv.enableIRIn();
}
send_len = 0;
recvStartTime = millis();
recvDuration = duration;
print_screen("IR scan start", 2);
}
}
void ir_stop(void){
if( recvDuration > 0 ){
pinMode(IR_RECV_PIN, OUTPUT);
recvDuration = 0;
print_screen_next("IR scan stopped");
}
}
String irStart(String command){
Serial.println("irStart called");
unsigned long duration = command.toInt();
ir_start(duration);
return "OK";
}
String irStop(String command){
Serial.println("irStop called");
ir_stop();
return "OK";
}
String irGet(String command){
Serial.println("irGet called");
if( send_len > 0 ){
String hexstr = array2hexstr(send_data, send_len);
return hexstr;
}else{
return "NG";
}
}
int char2int(char c){
if( c >= '0' && c <= '9' )
return c - '0';
if( c >= 'a' && c <= 'f' )
return c - 'a' + 10;
if( c >= 'A' && c <= 'F' )
return c- 'A' + 10;
return 0;
}
char int2char(int i){
if( i >= 0 && i <= 9 )
return '0' + i;
if( i >= 10 && i <= 15 )
return 'a' + (i - 10);
return '0';
}
int hexstr2array(String str, uint16_t *array, int maxlen){
int len = str.length();
if( (len % 4) != 0 )
return -1;
if( len / 4 > maxlen )
return -1;
for( int i = 0 ; i < len ; i += 4 ){
uint16_t value = char2int(str.charAt(i)) << 12;
value += char2int(str.charAt(i + 1)) << 8;
value += char2int(str.charAt(i + 2)) << 4;
value += char2int(str.charAt(i + 3));
array[i / 4] = value;
}
return len / 4;
}
String array2hexstr(uint16_t *array, int len){
String str = "";
for( int i = 0 ; i < len * 4 ; i++ )
str.concat("0");
for( int i = 0 ; i < len ; i++ ){
str.setCharAt(i * 4, int2char((array[i] >> 12) & 0x0f));
str.setCharAt(i * 4 + 1, int2char((array[i] >> 8) & 0x0f));
str.setCharAt(i * 4 + 2, int2char((array[i] >> 4) & 0x0f));
str.setCharAt(i * 4 + 3, int2char((array[i]) & 0x0f));
}
return str;
}
以下の部分は環境に合わせて変更してください。
const char* ssid = "【WiFiのSSID】";
const char* password = "【WiFiのパスワード】";
GPIOはここで指定しています。
#define LED_PIN GPIO_NUM_10
#define IR_RECV_PIN GPIO_NUM_33
//#define IR_SEND_PIN GPIO_NUM_9
#define IR_SEND_PIN GPIO_NUM_26
以下のところで、GET呼び出しを受け付けるエンドポイントを指定しています。
GET呼び出しを受け付けると、指定されたコールバック関数が呼ばれます。
rest.function("led",ledControl);
rest.function("start", irStart);
rest.function("stop", irStop);
rest.function("send", irSend);
rest.function("get", irGet);
ledが簡単ですね。
String ledControl(String command) {
Serial.println("ledControl called");
// Get state from command
int state = command.toInt();
digitalWrite(LED_PIN, state);
return "OK";
}
GET呼び出し時のパラメータが、commandに入ってきます。
その値をint型にパースして、その値をLEDのGPIOに設定しています。
GET呼び出しエンドポイントの仕様を整理しておきます。
led
M5StickCに内蔵されたLEDをOn/Offします。
params:"0":点灯、"1":消灯
return_value:"OK"(成功時)
start
M5StickCに接続した赤外線受信機で、リモコン信号の受信を待ち受けます。
params:受信待ち時間の10進数文字列
return_value:"OK"(成功時)
end
赤外線受信待ち受けを停止します。
return_value:"OK"(成功時)
get
受信したリモコン信号を取得します。
return_value:エンコード化されたリモコン信号の16進数文字列(成功時)
send
M5Stick-Cに接続したまたは内蔵されている赤外線送信機で、リモコン信号を送信します。
params:エンコード化されたリモコン信号の16進数文字列
return_value:"OK"(成功時)
paramsはこんな風に指定します。
例:LED点灯
http://XXX.XXX.XXX.XXX/led?params=0
XXX.XXX.XXX.XXX には、M5Stick-Cに割り当てられたIPアドレスを指定してください。
ledでのreturn_valueの例です。
{"return_value": "OK", "id": "0001", "name": "esp32", "hardware": "esp32", "connected": true}
getのretun_valueやsendのparamsでの16進数文字列は、2バイトのunsigned shortをBig Endianで並べたものです。
例: [1,2,3]の場合、"000100020003" となります。
動作確認
起動させると以下のようにコンソールに表示されます。
..
WiFi connected
Server started
XXX.XXX.XXX.XXX
http://XXX.XXX.XXX.XXX/start?params=10000
と呼び出すと、以下のように表示されます。
irStart called
すかさず、手持ちのリモコンから信号を送信すると以下のように表示されます。
IR received
Encoding : UNKNOWN
Code : E88A71EA (58 bits)
uint16_t rawData[115] = {3298, 1596, 446, 414, 370, 444, 368, 1228, 422, 390, 418, 1204, 396, 470, 290, 442, 444, 418, 292, 1280, 418, 1230, 392, 416, 422, 392, 418, 392, 392, 1230, 422, 1202, 418, 416, 368, 420, 418, 470, 294, 440, 420, 442, 290, 442, 450, 390, 392, 418, 292, 520, 410, 430, 364, 442, 200, 614, 366, 444, 292, 1278, 416, 424, 418, 418, 368, 416, 262, 548, 398, 440, 292, 520, 366, 418, 446, 1176, 394, 472, 292, 464, 394, 442, 368, 600, 196, 1272, 292, 518, 368, 444, 366, 444, 366, 418 , 416, 500, 290, 416, 392, 1230, 394, 418, 420, 1202, 418, 1232, 288, 1306, 394, 1230, 392, 1228, 39 6, 1228, 394}; // UNKNOWN E88A71EA
ちょうど手元にあった富士通製のエアコンのリモコンを受信してみたときのものです。
上記の例では、長さは115でした。最大の受信サイズは以下で指定していますので、もし足りなければ変更してください。
#define IR_MAX_SEND_DATA 300
次に以下を呼び出します。
すると、以下が返ってきます。
{"return_value": "0ce2063c01be019e017201bc017004cc01a6018601a204b4018c01d6012201ba01bc01a20124050001a204ce018801a001a6018801a20188018804ce01a604b201a201a0017001a401a201d6012601b801a401ba012201ba01c20186018801a201240208019a01ae016c01ba00c80266016e01bc012404fe01a001a801a201a2017001a001060224018e01b801240208016e01a201be0498018a01d8012401d0018a01ba0170025800c404f801240206017001bc016e01bc016e01a201a001f4012201a0018804ce018a01a201a404b201a204d00120051a018a04ce018804cc018c04cc018a", "id": "0001", "name": "esp32", "hardware": "esp32", "connected": true}
return_valueのところが、エンコード化されたリモコン信号です。
これを以下のparamsに指定して呼び出します。
http://XXX.XXX.XXX.XXX/send?params=ZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
これで、取得したエンコード化されたリモコン信号をM5Stick-Cに接続した赤外線送信機から送信されます。
繰り返しになりますが、M5Stick-Cに内蔵の赤外線送信機の距離は短い(数十cm)ので注意してください。
Webページ
以下のようなページです。
ソースコードです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
<!-- Latest compiled and minified JavaScript -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
<title>M5Stick-Cリモコン</title>
<script src="js/methods_utils.js"></script>
<script src="js/vue_utils.js"></script>
<!--
<script src="dist/js/vconsole.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
-->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="top" class="container">
<h1>M5Stick-Cリモコン</h1>
<label>m5stickc url</label> <input class="form-control" v-model="base_url"><br>
<button class="btn btn-default" v-on:click="ir_start()">リモコン受信開始</button><br>
<button class="btn btn-default" v-on:click="ir_get()">リモコン信号取得</button><br>
<table class="table table-striped">
<thead>
<tr><th>選択</th><th>名前</th><th>削除</th></tr>
</thead>
<tbody>
<tr v-for="(value, index) in send_data_list">
<td><button class="btn btn-default" v-on:click="data_select(index)">選択</button></td>
<td>{{value.name}}</td>
<td><button class="btn btn-default" v-on:click="data_delete(index)">削除</button></td>
</tr>
</tbody>
</table>
<label>リモコン信号</label>
<textarea class="form-control" rows="10" v-model="send_data" readonly></textarea>
<button class="btn btn-default" v-on:click="ir_send()">リモコン信号送信</button><br>
<div class="modal fade" id="progress">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{progress_title}}</h4>
</div>
<div class="modal-body">
<center><progress max="100" /></center>
</div>
</div>
</div>
</div>
</div>
<script src="js/start.js"></script>
</body>
'use strict';
//var vConsole = new VConsole();
const base_url = "http://XXX.XXX.XXX.XXX";
var vue_options = {
el: "#top",
data: {
progress_title: '',
send_data: '',
send_data_list: [],
base_url: base_url,
},
computed: {
},
methods: {
ir_start: function(){
return do_get(this.base_url + "/start", { params: "5000" } )
.then(json =>{
console.log(json);
})
.catch(error =>{
alert(error);
});
},
ir_get: function(){
return do_get(this.base_url + "/get", {} )
.then(json =>{
console.log(json);
if( json.return_value != "NG" ){
var ret = prompt('名前を決めてください。');
if( ret ){
this.send_data_list.push({ name: ret, data: json.return_value });
this.send_data = json.return_value;
localStorage.setItem('send_data_list', JSON.stringify(this.send_data_list));
}
}else{
alert('取得できませんでした。');
}
})
.catch(error =>{
alert(error);
});
},
ir_send: function(){
return do_get(this.base_url + "/send", { params: this.send_data} )
.then(json =>{
console.log(json);
})
.catch(error =>{
alert(error);
});
},
data_select: function(index){
this.send_data = this.send_data_list[index].data;
},
data_delete: function(index){
this.send_data_list.splice(index, 1);
this.send_data_list = JSON.parse(JSON.stringify(this.send_data_list));
localStorage.setItem('send_data_list', JSON.stringify(this.send_data_list));
},
},
created: function(){
},
mounted: function(){
proc_load();
var list = localStorage.getItem('send_data_list');
if( list )
this.send_data_list = JSON.parse(list);
}
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );
function do_get(url, qs) {
var params = new URLSearchParams(qs);
var url2 = new URL(url);
url2.search = params;
return fetch(url2.toString(), {
method: 'GET',
})
.then((response) => {
if (!response.ok)
throw 'status is not 200';
return response.json();
});
}
base_url のところは、M5Stick-Cに割り当てられたIPアドレスを指定して下さい。
以上