Arduinoを USB/HIDデバイス(仮想キーボード)として活用する(第五回）赤外線によるエアコン制御

##本記事について
　Arduinoを USB/HIDデバイスとして活用する第五回です。[第四回はここ](https://qiita.com/pronechia/items/9247f6ff507f5a7d04de)。今回はブラウザーと連携した仮想キーボードの実用例として、 赤外線によるエアコンの ON/OFFと温度調整制御を紹介します。


##第五回）「赤外線によるエアコンのON/OFFと温度調節制御」
　Arduinoの USB/HID を使って文字列を出力しブラウザーと連携して、エアコンのON/OFFと温度調整制御をおこないます。ブラウザー側で制御値を生成し、それを音を使った下り通信で Arduino に送ります。 Arduino では、エアコンの赤外線リモコンの仕組みで、エアコンをコントロールします。
　赤外線リコモンの送信データは、学習により Arduino 内部の仮想EEPROMに保管して利用するため、各種メーカに対応できます。
##機能概要
 はじめに、SW押下状態で起動し学習モードで動かします。学習モードでは、実際のリモコンデータを赤外線受信し Adafruit Trinket M0 内部の仮想EEPROMに保管します。このデータは制御モードで使用されます。
　通常起動では制御モードで動きます。制御モードでは、ブラウザーと連携し温度収集/HID出力/ブラウザーからの制御値をTone受信/受信データに従った赤外線出力　を行うことで、エアコンの電源ON/OFFを行います。この処理は１０秒間隔で繰り返されます。
##準備するもの
　PC (Windows10) ブラウザーは、Microsoft Edge,Chrome
　Adafruit Trinket M0 + 関連部品
　開発環境は  Windows版の Arduino IDE v1.8.9
　WebServer(ローカスPC上でも可） 
##全体の仕組み
 htmlをWebサーバーに置きます。WebサーバーはPC内部のローカルサーバーで構いません。PC上のブラウザーで指定ページが表示でき、Adafruit Trinket M0にはSW、BME280、IR送受信モジュールが接続されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/c8494a48-6dcd-cd8b-9b96-06dcbda73706.png)

WebServerは、WindowsPCのローカルにapacheを立てて確認しました。
##ハード構成
 Adafruit Trinket M0 周りのハード構成を示す。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/ab7c5fc8-0886-92b8-79a3-6130e6f0ba1e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/96d00d0e-92db-4deb-5163-a9f05ef65d2d.png)

##ソフトウエア
Adafruit Trinket M0プログラム構成

制約：仮想EEPROMの内容保持（プログラムを焼き直すと仮想EEPROMは初期化される）のため、学習モードと制御モードは同一プログラムで実現する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/2ca6eea6-3275-893d-c0d3-1fa58f45aeae.png)

```example0501.htm
<script type="text/javascript">
//音に関する主要部分のみ
//グローバルに定義
var audioCtx=new(window.AudioContext || window.webkitAudioContext)();
function mybeepOnce(pd){
 var oscillator=audioCtx.createOscillator();
 if(audioCtx.state=="suspended") audioCtx.resume();
 oscillator.connect(audioCtx.destination);
 oscillator.frequency.value = 1000; // 値はHz(ヘルツ)
 oscillator.start();
 oscillator.stop(audioCtx.currentTime + pd);
}
//
//ONbitを送りたい場合の呼び出し
 mybeepOnce(0.05);
//OFFbitを送りたい場合の呼び出し
 mybeepOnce(0.025);
</script>
```
ブラウザーの音に関する処理説明
1ビットの処理
　・Enter押してから少したつと、Trinket M0側に音が聞こえる。
　・ON:50msecの間音が鳴る。音が鳴り始めてから、35msec 後に再度、音がなっていたらONと判定する。
　・OFF: 25msecの間音がなる。音が鳴り始めてから、35msec 後に音がなっていなかったらOFFと判定する。

データは最大4ビット
　・ブラウザーに Inqu で問い合わせた後、
　・"1" + Tab + Enter  => M0 で音の着信待ち。ONO/FFを判定する。
　・"2" , "3" , "4" と繰り返し、4ビット分のON/OFFデータを受信する。


```ruby:apl_usbhid_TrinketM0_05Wondows.ino
/*
 *公開第五回）赤外線によるエアコンのON/OFFと温度調整制御 2020/08/05
 */
#define USBHOST_WINPC

//環境に合わせて内容を確認または修正すること　ーーここから -----------------------------
//①BME280 I2Cアドレスの設定　（0x76 or 0x77 のどちらか）
//  ブレッドボードは 0x76, MBE280のグローブ端子接続は 0x76
#define BME280DEVADDR 0x76
//#define BME280DEVADDR 0x77
//②Webserverに合わせて、ＵＲＬを記述すること
//String g_url_string = "http'//192.168.xxx.xxx/xxxxxxxx.htm"; //内部WebServer用
String g_url_string = "http'//localhost/example0501.htm"; //localhost用
//③ハード設定
#define LED_PIN 13
#define SW_PIN  1
#define IR_IN   3
#define IR_OUT  4
#define TONE_PIN  3
//#define TONE_PIN  4
//環境に合わせて内容を確認または修正すること　ーーここまで -----------------------------
#define WCS_DELAY_T1 150 //T1 ブラウザー応答時間150
#define WCS_DELAY_GAMEN 3000  //URLたたいてから画面が表示されるまで待つ
#define WCS_BITREAD_MAXCOUNT 5  //bit read のリトライ回数上限
#define WCS_SWWAIT_MAXCOUNT 30  //sw押下待ちのリトライ回数上限 30秒
#include "types.h"
#include <Wire.h>
#include <ZeroTimer.h>
#include <SparkFunBME280.h>
#include "lib_keyboard.h"
#include "ToneManager.h"
#include "VolumeControl.h"

BME280 mySensor;
ToneManager myToneM;
VolumeControl myVolumeC;

int16_t g_pass;  //HID出力したら後の待ち時間を制御する
boolean g_I2CNormal;
volatile int g_i; //timerカウント
volatile boolean g_high_spead;
int g_dataSyubetu = 1; //1-4。初期値は温度
int g_retry_count;
int g_mode;
```
環境に合わせて、以下の内容を確認または修正してください
①BME280 I2Cアドレスの設定　（0x76 or 0x77 のどちらか）
//#define BME280DEVADDR 0x76
//#define BME280DEVADDR 0x77
②Webserverに合わせて、ＵＲＬを記述すること
//String g_url_string = "http'//192.168.xxx.xxx/xxxxxxxx.htm"; //内部WebServer用
String g_url_string = "http'//localhost/example0501.htm"; //localhost用
③ハード設定が間違いないか確認
//#define LED_PIN 13
//#define SW_PIN  1
//#define IR_IN   3
//#define IR_OUT  4
//#define TONE_PIN  3

setup では、SW押下を待って、学習モード　または　制御モードを切り替える。

```ruby:setup処理
void setup(){  
  g_pass = 1;
  g_i = 0;
  g_high_spead = 0;
  g_retry_count = 0;
  pinMode(LED_PIN,OUTPUT);
  pinMode(SW_PIN, INPUT_PULLUP);
  delay(5000);
  //LEDが点滅している間にSWを押せば学習モードに入る
  sub_fw_Blink(LED_PIN, 10, 50); //10回ｘ50msecON/OFF
  //SW=ONの時、学習モードに入る。その後、LEDが消灯していれば学習モードと分かる
  if (!sub_fw_isSWON(SW_PIN)) {
    pinMode(IR_IN, INPUT);
    pinMode(IR_OUT, OUTPUT);
    Serial.begin(115200);
    delay(200);
    Serial.println("sub_fw_isSWON ON LearningMOde");
    //定時間隔で無限ループ
    while(1) sub_learningMode(); 
    //定時間隔で無限ループ
  }
  //
  //ここからは制御モード
  // LEDが数回、高速点滅後、低速で点滅してる間にSWを押すと先に進む
  Wire.begin();
  delay(100); //I2Cデバイスの初期化待ち
  mySensor.setI2CAddress(BME280DEVADDR);
  //BME280の初期化ができない場合、値ゼロで動かす
  if (mySensor.beginI2C() == false) g_I2CNormal = false;
  else                              g_I2CNormal = true;
  pinMode(TONE_PIN, INPUT);
  myToneM.begin(TONE_PIN, 8);  //8bit
  sub_fw_Blink(LED_PIN, 3, 50); //動き始めたことを知らせる
  //
  //PC側の準備ができるまで、USB/HIDを出力しない
  //SWが押されるまで待つ 10msec x 30 x 100 = 30秒 でエラーとする
  boolean sw_pushed = false;
  for (int j = 0; j< (WCS_SWWAIT_MAXCOUNT * 100); j++) {  
    if (sub_fw_SWcheck(SW_PIN) == 1) {
      sw_pushed = true;
      break;
    }
    if ((j % 100) == 0) {
      sub_fw_Blink(LED_PIN, 1, 5);  //5msecx2=10msec
    } else {
      delay(10);
    }
  }
  if (sw_pushed == false )
    while(1) sub_fw_Blink(LED_PIN, 10, 50);  //LED点滅し続ける
  
  // 音量UPのため、HIDデバイスを定義する
  myVolumeC.begin();
#if defined USBHOST_WINPC
  myVolumeC.volumeUP(1);  //windows
#elif defined USBHOST_MAC
  myVolumeC.volumeUP(2);  //MAC
#else
  // 何もしない。後で音が拾えない可能性がある
#endif

  //
  delay(1000);  //USB/HID再定義のための待ち時間。
#if defined USBHOST_WINPC
  sub_kbd_begin(1);  //Windows用に初期化
#elif defined USBHOST_MAC
  sub_kbd_begin(2);  //Mac用に初期化
#else
  // 何もしない。後でKEYBOARDが動作しない可能性大
#endif
  delay(100); //HIDデバイスの初期化待ち
  //
  sub_fw_timerset();  //タイマー起動
}
```

制御モードの時は、loop 内で処理を繰り返す。

```ruby:loop処理
void loop(){
  // 40msec毎に処理を行う
  if (sub_fw_event(2)) sub_proc();
}
//
//USB/HID処理
//
void sub_proc() {
  static byte s_command = 0; //処理振り分け
  static byte s_first = 0;
  int j;
  int w_ch;
  
  //待ち時間がゼロになるまで何もしない。
  if (g_pass-- >= 0 ) return;
  //処理振り分け
  if (s_command == 0) { 
    s_command = 1;
    s_first = 1;
    sub_initurl(); //一回だけ実施
  } else if (s_command == 1) { 
    sub_out_kbd(1);  //start
    if (sub_check_tone()) s_command = 10;
    else s_command = 90;
  } else if (s_command == 10) {
    sub_out_kbd(10); //Data
    if (sub_check_tone()) s_command = 11;
    else s_command = 90;
  } else if (s_command == 11) {
    sub_out_kbd(11); //センサー値UP
    if (sub_check_tone()) s_command = 20;
    else s_command = 90;
  } else if (s_command == 20) {
    sub_out_kbd(20); //Inqu
    if (sub_check_tone()) s_command = 21;
    else s_command = 90;
    g_retry_count = 0; //次回のために初期化  
    myToneM.clear();
  } else if ((s_command >= 21) && (s_command <= 28)) {
    //制御情報を1ビットつづ受けとる。８ビット
    sub_out_kbd(s_command); //Inq no1 to no8
    g_high_spead = 1; //問い合わせは待ち時間なしで動かす。本当はS_command==21のみでいい
    if (myToneM.readBit(s_command - 20)) {
      //正常
      s_command = s_command+1;
      g_retry_count = 0; //次回のために初期化  
      //8bitは無駄なので、4bitで次へ
      if (s_command == 25 ) s_command = 29; 
    } else {
      //異常時は同じビットを再送要求する
      if (g_retry_count++ >= WCS_BITREAD_MAXCOUNT) s_command = 90; //リトライオーバーで異常へ
    }
  } else if (s_command == 29) { 
    g_high_spead = 0; //問い合わせは待ち時間なしで動かすモードを終了する
    //制御情報を1ビットつづ受けとる。８ビット
    sub_out_kbd(29); //Inqe end
    g_dataSyubetu = myToneM.getToneVal(); //受信した制御値を取り出す
    //IR出力
    w_ch = 0;
    if (g_dataSyubetu == 3 ) w_ch = 1; //ON
    else if (g_dataSyubetu == 6 ) w_ch = 2; //OFF
    else if (g_dataSyubetu == 9 ) w_ch = 1; //ON
    else if (g_dataSyubetu == 10 ) w_ch = 2; //OFF
    if (w_ch == 1 || w_ch == 2 ) {
      sub_qCommand(w_ch);    
      //IR出力しことをLEDで知らせる
      sub_fw_Blink(LED_PIN, 10, 50); //10回ｘ50msecON/OFF 計1秒
    }
    s_command = 10; //次のセンサーデータUPへ、
  } else if (s_command == 90) {
    //フォーカスがおかしくて音が拾えない場合にここにくる
    sub_out_kbd(90); //フォーカスをコマンドフィールドへ
    s_first = 1;
    s_command = 1;   //startコマンドから再開する     
  } else {
    //その他はエラー
    sub_fw_Blink(LED_PIN, 10, 40);
  }
  
  if (s_command == 1) {
    g_pass = 50; //40msec*50= 2sec
  } else if (s_command == 10) {
    if (s_first == 1) {
      g_pass = 50; //40msec*50= 2sec
      s_first = 0;
    } else {
      g_pass = 200; //40msec*200= 8sec
    }
  } else if ((s_command > 10) && (s_command <= 20)) {
    g_pass = 25; //40msec*25= 1sec
  } else if ((s_command > 20) && (s_command < 30)) {
    g_pass = 1; //high speed
  } else if (s_command == 90) {
    g_pass = 25; //40msec*25= 1sec
  } else {
    //フォーカスおかしいときも、一旦ここにきて待つ
    g_pass = 200; //40msec*200= 8sec
  }
}
（以下、省略。ソースコードを参照ください）
```
　ソースコードは、[GitHub](https://github.com/pronechia/apl_usbhid_TrinketM0_05)から取り出すことができます。 Windows用とMac用にわかれています。各フォルダー内のすべてのファイルを同一フォルダに保存して、Arduino IDE でコンパイルしてください。
注意）
・BME280のI2Cアドレスが間違った場合は、測定値ゼロとします。
・ツール／ボードの設定は、 Adafruit Trinket M0 です。
・サンプルのHTMLファイルは、htmlフォルダにあります。
##実行
 準備
・WebServerが使えて、PCと接続できていること。
　WindowsPCのapacheを立てることが簡単かと思います。（下記参考欄）
・既定のブラウザーは、Microsoft Edge または、Chromeになっていること。
・WindowsPCの入力モードは、英数字である必要があります。日本語モードになっていないことを確認してください。

1.PCに Adafruit Trinket M0 をUSB接続、イヤフォンジャック接続する。電源ON後、概ね５秒以内にSWを押下すると学習モードで動きます。LEDが数回点滅している時点でSWを押していると入りやすいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/11af12a5-4ce3-3a90-86c7-39b1a1537a38.png)
学習モードの初期画面。LEDが消灯していることでも、学習モードとわかる

2.学習モードで動作確認
　赤外線の送受信が正しく行われているか動作確認します。
・ r コマンドでIDEの送信ボタンを押します。５秒以内に、赤外線受信機に向けてエアコンのリモコンの電源ON/OFFボタンを押してください。受信した結果がシリアルモニターに表示されます。電源ONとOFFの２種類のデータをテキストエディタ等に保存してください。エアコンがONの時にボタンを押すとOFF信号、エアコンがOFFの時がON信号です。
　注意）エアコン信号は、数値が３００〜６００個並びます。数個の場合は受信エラーが考えられますので、再度、取り直してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/637b509b-289d-cb44-4215-dc27067bd434.png)

・ sコマンド（rコマンドで受信した　s, で始まる文字列をそのままコピペ）で送信ボタンを押します。赤外線送信により実際のエアコンが動作することを確認してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/8a2f806a-266d-832d-8bce-20740ca396e8.png)


わかったこと）　多くの家電製品（テレビ、照明器具等）の赤外線リモコンは、押されたボタンの信号を送信しているのに対して、エアコンの赤外線リモコンは、”現在の設定情報すべて（概ねリモコンの液晶画面に表示されている情報）”を送信しています。よって、忠実な送信データの再現が難しいことと送信データ量がかなり大きくなる特徴があります。


3.学習モードで信号を保管する
　リモコンの電源ON,OFF信号をM0に学習させます。p1,q1 が電源ON、p2,q2 が電源OFFです。p1コマンド用の文字列を編集します。rコマンドで受信した電源ON用の s, で始まる文字列の s, を p1, に変更してコピーしてください。
変更前の例）s,432,437,428,437,435,479,390,430,25318,3469,1735,430,1333,400,445,421,438,,,,,,
変更後の例）p1,432,437,428,437,435,479,390,430,25318,3469,1735,430,1333,400,445,421,438,,,,,,

p1 コマンド（上記でコピーした文字列をIDEにペーストして）でIDEの送信ボタンを押します。送った文字列が仮想EEPROMに保存されます。電源OFF信号を学習させる場合は p2 コマンドです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/65038270-4bee-93a9-83f2-a707316ea722.png)

　エアコンの前に移動して、LEDをエアコン受信窓に向けます。q1 コマンドでIDEの送信ボタンを押します。仮想EEPRONの該当信号が赤外線送信されます。これにより実際のエアコンが動作することを確認してください。電源OFF信号を動作させる場合は  q2 コマンドです。

これで学習が終わりました。Adafruit Trinket M0　の電源をOFFしても学習内容は保持されます。ただ、プログラムを焼き直した場合は、学習内容がクリアーされるため、学習をやり直してください。



4.制御モードで動かす
 学習モードで動いている時は、一度電源をOFFします。電源ON後、概ね５秒で制御モードで動きます。LEDがゆっくり点滅すれば制御モードです。SWを押すと処理を始めます。突然USB/HID出力されることを防ぐため、LEDがゆっくり点滅している間（約30秒間）にSWを押すことで処理を開始するようにしています。

5.制御モードでWebページを開く
　Adafruit Trinket M0 のUSB/HID から音量アップキーとURL文字列が出力されます（Windowsキー  + ‘r’　でcmd窓を開き、URL文字列を入力する）。これでブラウザーが立ち上がり、トップページが表示されます。　
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/f7e80ec2-cc2b-3d22-2b57-617792a4164c.png)
トップページです。水色のフィールドはシステムが使用します。白色のフィールドは人が入力する項目です。

6.Startコマンド
　トップページ表示後、Startコマンドで確認音が出力されます。
　・USB/HIDで　‘Start’ + Tab + Enterが入力される
　・Enter押下時に、Browser側で音を出す。M0側では音を拾う。
　・音が聞こえれば次へ。聞こえなければ繰り返す
Adafruit Trinket M0 で音が受信できたということは、正しく画面が表示されて音量設定も問題ないと判断できます。これで、次に進むことができます。音が受信できない場合は、LEDが高速点滅しURL入力が繰り返されます。正しく音が拾えるように、音量やイヤホンジャックの接続などを確認してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/39cbf6a5-ceef-9662-6cce-223ea589d846.png)

7.定時間隔で以下を繰り返し、エアコンON/OFFと温度調節制御を実行します。

・上りデータ送信
　　　Dataコマンドは、Adafruit Trinket M0 で測定した室温をブラウザーに送ります。ブラウザー側では、室温を受け取ると同時に、現在時刻が表示されます。
　　・USB/HIDで　‘Data’ + Tab + Enterが入力される
　　・USB/HIDで　室温 + Tab + Enter　が入力される
　　・Browser側で現在時刻を求め表示する。
・下りデータ受信
　　・USB/HIDで　‘Inqu’ + Tab + Enterが入力される
　　・Browser側でエアコンON/OFFと温度調節制御が必要かどうか判断し、制御値を求める。
　　　 (制御値は、 3:時刻による電源ON  6:時刻による電源OFF  9:温度による電源ON  10:温度による電源OFF）
　　・USB/HIDで　‘1’ + Tab + Enterが入力されると、送信データ値にしたがって 音を出す。
　　 M0側で音を拾い、 ON/OFFを判定する
　　必要なビット数 (ここでは4回)繰り返す。エアコンON/OFF制御値が把握できる。M0で受信した値に応じて、必要であれば　学習モードで仮想EEPROMに保持した電源ON信号／電源OFF信号を赤外線出力します。実際にエアコンが動作することを確認してください。
　コマンドは１０秒間隔で繰り返されます。その間にブラウザー側でA/C ON時刻、A/C OFF時刻、設定温度　ON/OFF設定切替ボタン　を入力することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/4c15d81e-a540-f22a-966f-0f65ec595926.png)

##まとめ
 仮想キーボードの実用例として、エアコンのリモコンとしAdafruit Trinket M0を使用し、人に代わって自動でボタン操作をすることが確認できました。IOT製品の一般的な構成である、センサ/アクチュエータ/マイコンを用いたシステムとして構成できたことは、一つの通過点をクリアーできたと考えています。

##参考
赤外線リモコンの通信フォーマットについては、以下を参考にしました。
　http://elm-chan.org/docs/ir_format.html

音量調整ボタンについて、以下を参照にしました。
　「ArduinoでUSB接続のPC音量調整ボタンを作る方法」
　https://qiita.com/kwbt/items/0c9f930a236ca989e402

WindowsPCにapacheを立てる場合の例は、以下を参考にしました。
　https://www.adminweb.jp/apache/install/

Trinket M０の開発環境については、以下を参考にしました。
　https://www.denshi.club/cookbook/arduino/trinketm0/trinket-m01arduino-ide.html

２つのライブラリが必要です。以下を参考にしました。
　・ZeroTimer.h
　　　https://ehbtj.com/electronics/arduino-zero-timer-interrupt-library/
　・SparkFunBME280.h
　　　BME280のライブラリーは豊富にでているので、GitHubなどで探してみてください。