Arduinoを USB/HIDデバイス（仮想デバイス）として活用する第三回

##本記事について
　Arduinoを USB/HIDデバイスとして活用する第三回です。[第二回はここ](https://qiita.com/pronechia/items/0d3c78723d0c88176a2b)。マイコンとブラウザーの間の双方向通信処理を作ってみました。BME280で測定値のうち、ブラウザーからの指示に基づいた項目を Browserに定時間毎に数値表示するアプリを紹介します。


##第三回）「Toneを利用し、ArduinoとBrowserとの双方向リアルタイムモニター」
　前回までで、Arduinoから USB/HID を使って文字列を出力することができました。逆に Arduinoへの入力の例として、 Tone(音)を使ってみました。  Toneは "音が鳴るか鳴らないか” の 1ビット信号として使っています。
マイコンは前回同様、Arduino互換 Adafruit Trinket M0を使用します。
##機能概要
 TrinketM0で収集した測定値を文字列としてPCのブラウザーにHID/USB出力することによってブラウザーに表示する、リアルタイム・モニターを実現しました。測定値は温度／湿度などがあり、どれを表示するかはブラウザーから指定することができます。
・Setup処理
　・SWの押下を待って処理開始。まず音量をUPして音が聞こえる状態にする。
　・WebデータのURLをHID/USB出力し画面が表示された後、Loop処理に移行
・Loop処理
　・定時間隔毎、処理を繰り返し実施
　・ブラウザーからデータ種別を受信する
　・I2Cデバイス（BME280）から気温／湿度などの測定値を取集
　・定時間隔毎に指定データ種別の「ラベル文字列」「測定値文字列」「単位文字列」をHID/USB出力
##準備するもの
　PC (Windows10) ブラウザーは、Microsoft Edge,Chrome
　Adafruit Trinket M0
　開発環境は  Windows版の Arduino IDE v1.8.9
　WebServer(ローカスPC上でも可） 
##全体の仕組み
 htmlをWebサーバーに置いておく。内部のローカルサーバーでもよい。Adafruit Trinket M0には、テスト用のSWがあり、温度・湿度などを測定するBME280をI2C接続する。PCのイヤフォンジャックと Adafruit Trinket M0を接続する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/b9531e48-3e5f-855e-5e1f-9edfe8a550ff.png)
WebServerは、WindowsPCのローカルにapacheを立てて確認しました。
##ハード構成
 Adafruit Trinket M0 周りのハード構成を示す。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/917a1567-acab-d096-dffb-0cd0e5036991.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/67fdec25-8124-274a-3d5f-2ef977686804.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/5631d4e9-dd87-7a46-03d6-3e4b3cda7fe6.png)
動作確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/328b4e9e-e1d4-efe7-57f4-f295752ccab2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/9a9bcc46-8a32-4ffa-1776-90a847a6cc96.png)
##ソフトウエア
HTML内JavaSciptで音を鳴らす

```example0301.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と判定する。

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

```ruby:apl_usbhid_TrinketM0_03Wondows.ino
/*
 *公開第三回）双方向リアルタイムモニター
 */
// WindowsPC用
#define USBHOST_WINPC
//環境に合わせて内容を確認または修正すること　ーーここから -----------------------------
//①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/example0301.htm"; //localhost用
//③ハード設定
#define LED_PIN 13
#define SW_PIN  1
#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 <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;
//
//
//
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);
  myToneM.begin(TONE_PIN, 8);  //8bit

  Wire.begin();
  delay(100); //I2Cデバイスの初期化待ち
  mySensor.setI2CAddress(BME280DEVADDR);
  //BME280の初期化ができない場合、値ゼロで動かす
  if (mySensor.beginI2C() == false) g_I2CNormal = false;
  else                              g_I2CNormal = true;
  //
  sub_fw_Blink(LED_PIN, 3, 50); //動き始めたことを知らせる
  digitalWrite(LED_PIN, HIGH);  //明確に点灯
  //SWが押されるまで待つ 10msec x 100 = 1sec でエラーとする
  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;
    }
    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();  //タイマー起動
}
```
環境に合わせて、以下の内容を確認または修正してください
①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/example0301.htm"; //localhost用

③ハード設定が間違いないか確認
　LED_PIN 13
　SW_PIN  1
　TONE_PIN  4

```ruby:loop処理
//
//main 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;
  
  //待ち時間がゼロになるまで何もしない。
  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 = 20;
    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;
    myToneM.clear();
  } else if ((s_command >= 21) && (s_command <= 28)) {
    //制御情報を1ビットつづ受けとる。８ビット
    sub_out_kbd(s_command); //Inq no1 to no8
    if (s_command == 21) {
    }
    g_high_spead = 1; //問い合わせは待ち時間なしで動かす。本当はS_command==21のみでいい
    if (myToneM.readBit(s_command - 20)) {
      //正常
      s_command = s_command+1;   
      //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(); //受信した制御値を取り出す
    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 == 20) {
    if (s_first == 1) {
      g_pass = 50; //40msec*50= 2sec
      s_first = 0;
    } else {
      g_pass = 200; //40msec*200= 8sec
    }
  } else if ((s_command > 9) && (s_command < 12)) {
    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_03)から取り出すことができます。 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接続、イヤフォンジャック接続する。SW押下で動き出す

2.音量UPコマンドを数回出す。
3.Windowsキー  + ‘r’　でcmd窓を開き、URL文字列を入力する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/c0379633-ff4e-4aa4-41e4-8619529308f1.png)
4.音出しを繰り返し、安定して音が受信できることを確認する 
　・‘Start’ + Tab + Enterを入力する
　・Enter押下時に、Browser側で音を出す。M0側では音を拾う。
　・音が聞こえれば次へ。聞こえなければ繰り返す
5.定時間隔で以下を繰り返す
・下りデータ受信
　　 ‘Inqu’ + Tab + Enterを入力する
　　 ‘1’ + Tab + Enterを入力すると、送信データ値にしたがって 音を出す。
　　 M0側で音を拾い、 ON/ OFFを判定する
　　必要なビット数 (ここでは4回)繰り返す。データ種別が把握できる。
・上りデータ送信
　　 ‘Data’ + Tab + Enterを入力する
　　ラベル + Tab + 測定値 + Tab + 単位 + Tab + Enter　を入力する
　　（送信すべきデータ種別にしたがって、ラベル /測定値 /単位を送信する ）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592438/f69b9f03-11df-4891-dd27-2188e453944b.png)
##まとめ
 Arduinoとブラウザーの連携において、USB/HIDの上りデータとToneの下りデータを使うことで、双方向の通信基盤が確認できた。これにより、マイコン制御の基本的な仕組みは実現できると考えます。Toneを使った下り通信は、安定性や情報量が少ないなどの課題があるため、次回は 下りデータをToneから発展させることに挑戦します。ESP8266/ESP32のWiFiをうまいこと使えないかと検討中です。
##参考
音量調整ボタンについて、以下を参照にしました。
　「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などで探してみてください。