[Windows 10版] Digispark でパソコンとリモコン両方でON/OFF操作ができるスイッチの作成

# **何の話？**
電子工作の中でも最もシンプルでお手軽なマイコンボードの一つ**「Digispark」**を使って
**「パソコンとリモコン両方でON/OFF操作ができるスイッチ」**を作りたいと思います。

[![ds.png](https://qiita-image-store.s3.amazonaws.com/0/65983/6fd719bb-db24-f591-9b12-62cfbd205535.png "紹介動画（YouTube）")](https://www.youtube.com/watch?v=24a4KMGl6qQ)
[紹介動画(YouTube)](https://www.youtube.com/watch?v=24a4KMGl6qQ)

**[Digispark](http://digistump.com/products/1)** : プログラムをチップに書き込んで、単体で動作させる事ができるハードウェア(※USB給電は必要です)。PCに接続すると、キーボード・マウスの様な入力デバイスとして認識させることも可能。今回はPCに常時接続する事になりますが、ハードウェア側の動作を担当してもらいます。
PCから抜いた状態でも、身近なUSB給電・しかも最低限の部品で「リモコン操作でON/OFFできるスイッチ」を作ることができます。

**[IR](http://digistump.com/wiki/digispark/tutorials/ir)** : Infrared,一般的なリモコンに用いられる赤外線通信です。指向性には欠けるが、単純な分ワイヤレス技術において初期認識からの反応速度は段違いに良いところが利点。

**[リレースイッチ](http://digistump.com/wiki/digispark/tutorials/relay)** : 電気信号でON/OFFができるスイッチ。物理スイッチなのでON時抵抗ゼロ。OFF時抵抗∞となります。大きな電圧にも耐えられますが、その分摩耗が促進されます。

※各種組み立ては単純なので省略しております。（はんだ付け初心者の私でも、ものの13分でできます。はんだごて加熱3分、作業10分）上記紹介動画の例や、リンクのドキュメントを参照してください。

# **着手前の疑問**
製作を始める時に、疑問に思った点を挙げます。本題が目当てであれば読み飛ばしてください。

- **Windows 10 でDigisparkは動くのか？（プログラム書き込み・USB通信）**
> 動作確認できました。
本体へのプログラム書き込みはもちろん、パソコンとのUSB通信（入力・出力）もDigiUSBを使えば、ドライバの細工をせずに自動認識します。（※但し、今後のWindowsUpdateやパソコンの構成によっては利用できない可能性は考えられます。）

- **プログラムの書き込みや、通信にはDigispark以外の特別なハードウェアが必要か？**
> 普通に書き込む分には特に必要ありません。
パソコンにUSB直差しで問題ありません。よくあることですが、ハブ経由だと調子が悪いかもしれません。書き込みは一旦Digisparkをパソコンから抜いてから差しなおす動作が必要なのですがそれを補助してくれるツールは存在します。

- **USB通信をするにはPC側でソフトウェア開発が必要っぽい？簡単にできないかな・・**
> 「パソコンからDigisparkへコマンドラインで信号を送信できれば十分。」「Digisparkから送信された信号を見たい。」「Digisparkから送信された信号を判断してバッチ処理に回せればそれでいい。」
という方であれば、有り物が用意されているのでそれを利用できます。

- **Digisparkの「シールド(拡張ボード)」2枚差しはできるのか？**
> 各シールド毎に割り振られているポート番号(0～5)が被らなければ可能です。確認は本家HPでお願いします。
今回試したもの（IR(受信機)シールド=2番・Relayシールド=5番・USB通信=3番4番）は被っていないので同時差しできました。しかしながらこれ以上はポートが塞がって無理です。
差し方は後述。

- **IR（赤外線リモコン信号）の受信ができるのはわかるが、コードの解析までできるのか？**
> Digisparkで試行してみた限り「受信した信号を固有のコードに変換できる」ところまではできたので、あとは、その変換されたコードを元に処理を割り当てができます。IRの動作はその他「信号が正しいかチェックする」等の工程があるのですが、それは省略しています。

# **レシピ**

- パソコン（Digisparkへプログラムを書き込む,USB通信する）

- [Digispark本体](http://www.elefine.jp/SHOP/Digispark.html)・[Digispark 赤外線受信シールドキット](http://www.elefine.jp/SHOP/DigisparkInfraredReceiverKit.html)・[Digispark リレーシールドキット](http://www.elefine.jp/SHOP/DigisparkRelayShieldKit.html)

- [足の長いピンソケット 6P](
https://www.switch-science.com/catalog/1416/)・ [足の長いピンソケット 3P](https://www.switch-science.com/catalog/2719/)

- [スイッチに繋げるコード（電子工作用）](http://amzn.to/2dsxPGE)、[はんだ、はんだごて](http://amzn.to/2dOdBXX)

(パソコン・道具以外は合わせて￥3,000程）


# **開発環境のインストール**
特別なことは行っていないので、既に済んでいる方は読み飛ばしてください。

- **ArduinoIDEのインストール**

 - https://www.arduino.cc/en/Main/Software
をダウンロード・実行し、あとは促されるまま…。(Installerの使用をおすすめします。)

- **ArduinoIDEの設定**
 - メニュー**「ファイル / 環境設定」** を開きます。

 - 設定タブの中にある**「追加ボードマネージャのURL」**に対してURLを貼り付け、
```
http://digistump.com/package_digistump_index.json
```
「OK」で適用します。

 - メニュー**「ツール / ボード:"～" / ボードマネージャ」** を開き、**タイプ**を**「提供された」**に選択すると表示される**「Digistump AVR Boards by Digistump」**を選択し**「インストール」**ボタンでインストールします。完了したらウィンドウを閉じます。

 - メニュー**「ツール / ボード:"～"」**は**「Digispark(Default 16.5mhz)」**を選択します。

- **ドライバのインストール**

 - https://github.com/digistump/DigistumpArduino/releases/download/1.6.7/Digistump.Drivers.zip
 - ZIP展開後のスクリプト**「install.bat」**を実行するとインストールされます。ドライバのインストール許可を求められますが問題なければインストールを進めます。

# **開発作業・動作の流れ**
開発環境が完成したら基盤への書き込みを行います。例として、LEDを点滅させるプログラムを実行させましょう。

- **プログラムを書き込んで動作チェック**

 1. メニュー**「ファイル / スケッチの例 / Digistump_Example / Start」**を選択すると、コードが現れます。
 2. **「→」(マイコンボードに書き込む)**のボタンをクリックするとコンパイルが開始します。
 3. **Running Digispark Uploader...Plug in device now... (will timeout in 60 seconds)** の表示が出ると、Digisparkをパソコンに差し込みます。
 4. 書き込みが行われ、完了の表示が出たら、USBの再認識が始まり、DigisparkのLEDの点滅が始まっています。←この時点で書き込んだプログラムを実行していることになります。
 5. 再度プログラムを書き込む際は、Digisparkをパソコンから抜いて、2.から同様の繰り返し操作となります。


# **IR/USB 2WAYスイッチのプログラム**
- 2番ピン=IRの受信, 5番ピン=リレースイッチへの出力となります。設定変更は一応できるようにしています。
- http://elm-chan.org/docs/ir_format.html の様に使用するリモコンには規格や個体差が有りますので、それに応じて「// I/O Timing Setting」欄の調整が必要となります。
(主に試行錯誤となります・・)
- その他「loop()」関数内の「ir」コードに応じた挙動はリモコン個別に変更が必要です。
- 同様にプログラム書き込みを行って動作確認できます。

```c:code.ino
// I/O Setting
#define IR        2    //IRのピン番号
#define RELAY     5    //リレーのピン番号
#define IR_PININ  PINB //ポート入力

// I/O Timing Setting (AEHA/NEC)
#define IR_TS_LC_OFF      4000    // リーダコードOFF間隔(AEHA:4000 NEC:8000)
#define IR_TS_LC_ON       3800    // リーダコードON間隔
#define IR_TS_LC_B        0       // リーダコード判定基準(AEHA:0 NEC:1)
#define IR_TS_BIT_ON      1000    // ビットデータON:1/0判定間隔
#define IR_TS_TO          32000   // タイムアウト

// App Define
#include <DigiUSB.h>
#define IRbit_ON()     (IR_PININ&_BV(IR)) //true:ON, false:OFF
#define IS_REPEAT      0
#define IS_ERROR       0xFFFFFFFF

bool r_is_on = false;

// ****** IR 制御 ******

// IR 間隔計測
//   1:ON間隔 0:OFF間隔
unsigned long ir_time(uint8_t io){
  unsigned long t = micros();
  while(!IRbit_ON() ^ io);
  return micros() -t;
}

// IR デコード
//   IS_REPEAT:リピート IS_ERROR:エラー/無視 他:データ
uint32_t IR_Decode() {
  uint32_t  dt = 0; //データ
  unsigned long t;  //信号長

  // ONの場合は無視（呼び出し元で割り込みする）
  if(IRbit_ON()) return IS_ERROR;

  // リーダーコードOFF間隔に満たない場合は無視
  if (ir_time(0) <= IR_TS_LC_OFF) return IS_ERROR;

  // リピートコード検出(NEC:">" AEHA:"<")
  if ((ir_time(1) < IR_TS_LC_ON) ^ IR_TS_LC_B) {
    // 0N間隔 がリピートコードの場合、ストップビットまで待って戻す。
    while(IRbit_ON());
    return IS_REPEAT;
  }

  // データを取得（32ビット分）
  for (uint8_t i = 0; i <32; i++) {
    // ビット開始待ち
    while(!IRbit_ON());
    // ON間隔検出
    t = ir_time(1);
    // タイムアウトはエラーを返す
    if (t>IR_TS_TO) return IS_ERROR;
    dt<<=1;
    dt |= (t>IR_TS_BIT_ON) ? 1:0;     
  }

  //ストップビット待ち
  while(IRbit_ON());
  return dt;
}

// ****** リレーコマンド ******

// ON
void r_on(){
  digitalWrite(RELAY, HIGH);
  r_is_on = true;
}

// OFF
void r_off(){
  digitalWrite(RELAY, LOW);
  r_is_on = false;
}

// 起動
void r_start(){
  r_on();
  delay(1000);
  r_off();
}

// 停止
void r_halt(){
  r_on();
  delay(6000);
  r_off();
}

// トグル
void r_toggle(){
  if(r_is_on){
      r_off();
  }else{
      r_on();
  }
}

// ****** Arduino制御 ******

void setup() {
  pinMode(IR, INPUT);
  pinMode(RELAY, OUTPUT);
  DigiUSB.begin();
}

void loop() {
  uint32_t ir = IR_Decode();
  if (ir && ir != IS_ERROR) {
    //IR 入力 (リピートコード無視)
    if(ir==669017340) r_start();
    else if(ir==663774450) r_halt();
    else if(ir==666526967) r_off();
    else if(ir==665871606) r_on();
    else if(ir==668624123) r_toggle();
    DigiUSB.println(ir);
    DigiUSB.delay(200);
  }else if (DigiUSB.available()) {
    //USB 入力
    char recv = DigiUSB.read();
    if(recv=='0') r_off();
    else if(recv=='1') r_on();
    else if(recv=='s') r_start();
    else if(recv=='h') r_halt();
  }
  DigiUSB.refresh();
}
```

# **USB通信ソフト**
パソコンとの入出力ですが、Digispark自体をキーボードやマウスのようなUSB機器として動作させる事ができる為、ドライバに接続する中間のソフトウェアの開発が自由にできるようになります。しかし複雑なことをしないのであれば、単なるバッチ処理だけでも問題なく作れます。

[DigisparkExamplePrograms](https://github.com/digistump/DigisparkExamplePrograms)
ソフトウェアは上記出来合いのものからダウンロード(ZIP Download)できます。ZIPを展開し、

```
(ZIPを展開したフォルダ)\DigisparkExamplePrograms-master\Python\DigiUSB\windows\
```
以下に既にビルドされたプログラムが配置されています。上記プログラム実行後、**「monitor.exe」**を起動すると、ターミナルのようなものが現れます。送受信の確認については、それでほぼ事足ります。
上の入力欄に
「1」を入力欄に入力後エンターで、リレースイッチがON。
「0」を入力欄に入力後エンターで、リレースイッチがOFF。
「s」を入力欄に入力後エンターで、1秒間ON。
「h」を入力欄に入力後エンターで、6秒間ON。
といった操作ができます。
逆に、リモコンからの受信は下の枠にてモニタリングされるようになっています。

今回はIRの制御に合わせたスクリプト(BAT+WSH)を用意しました。上記フォルダと同じ位置に以下の4ファイルを作成して、「digiusb_agent_start.js」を起動すると、Digispark側の挙動を常駐監視してくれます。

- **Digisparkからの通信を受けて実行されるBAT**

```bat:digiusb_agent_setting.bat
@echo off
:LOOP
for /f "usebackq tokens=*" %%i in (`receive.exe`) do @set CODE=%%i
if "%CODE%"=="" ( echo 0

rem ***ここにコードとコマンドを記載 ***
) else if "%CODE%"=="669148412" (
start notepad.exe

) else if "%CODE%"=="663643377" (
start explorer.exe

) else if "%CODE%"=="669869309" (
start mspaint.exe

rem ***********************************

) else ( echo CODE=%CODE% )
goto :LOOP
exit /b 0
```

- **上記BATを起動するスクリプト**

```js:digiusb_agent_start.js
var ws = new ActiveXObject( "WScript.Shell" );
var fso = new ActiveXObject("Scripting.FileSystemObject");
//--------------------------------------------------------------------------------

pid = pid("cmd.exe%digiusb_agent_setting.bat");
if(pid > 0){
  WScript.Echo("Digiusb Agent.js は PID:"+pid+"で起動中です。");
}else{
  ws.run("digiusb_agent_setting.bat",0,false);
  WScript.Echo("Digiusb Agent.js の起動を試行しました。");
}

//--------------------------------------------------------------------------------

//プロセスIDをチェックする。
function pid(procname) {
	var proc = new Enumerator(GetObject("WinMgmts:Root\\Cimv2").ExecQuery(
			"Select * From Win32_Process where CommandLine like '%" + procname
					+ "%'"));
	var cmdln = '';
	for (; !proc.atEnd(); proc.moveNext()) {
		var item = proc.item();
		if(item.CommandLine&&item.CommandLine.indexOf(fso.GetFileName(WScript.ScriptFullName))>=0) continue;
		return item.ProcessID;
	}
	return 0;
}
```

- **BAT常駐を監視・停止するスクリプト**

```js:digiusb_agent_stop.js
var ws = new ActiveXObject( "WScript.Shell" );
var fso = new ActiveXObject("Scripting.FileSystemObject");
//--------------------------------------------------------------------------------

pid = pid("cmd.exe%digiusb_agent_setting.bat");
if(pid > 0){
   ws.run("taskkill /F /PID "+pid,0);
}
ws.run("taskkill /F /IM receive.exe",0);
WScript.Echo("Digiusb Agent.jsの終了を試行しました。");


//--------------------------------------------------------------------------------

//プロセスIDをチェックする。
function pid(procname) {
	var proc = new Enumerator(GetObject("WinMgmts:Root\\Cimv2").ExecQuery(
			"Select * From Win32_Process where CommandLine like '%" + procname
					+ "%'"));
	var cmdln = '';
	for (; !proc.atEnd(); proc.moveNext()) {
		var item = proc.item();
		if(item.CommandLine&&item.CommandLine.indexOf(fso.GetFileName(WScript.ScriptFullName))>=0) continue;
		return item.ProcessID;
	}
	return 0;
}
```

- **BAT常駐をチェックするスクリプト**

```js:digiusb_agent_status.js
var ws = new ActiveXObject( "WScript.Shell" );
var fso = new ActiveXObject("Scripting.FileSystemObject");
//--------------------------------------------------------------------------------

pid = pid("cmd.exe%digiusb_agent_setting.bat");
if(pid > 0){
  WScript.Echo("Digiusb Agent.js は PID:"+pid+"で起動中です。");
}else{
  WScript.Echo("Digiusb Agent.js の起動を確認できませんでした。");
}

//--------------------------------------------------------------------------------

//プロセスIDをチェックする。
function pid(procname) {
	var proc = new Enumerator(GetObject("WinMgmts:Root\\Cimv2").ExecQuery(
			"Select * From Win32_Process where CommandLine like '%" + procname
					+ "%'"));
	var cmdln = '';
	for (; !proc.atEnd(); proc.moveNext()) {
		var item = proc.item();
		if(item.CommandLine&&item.CommandLine.indexOf(fso.GetFileName(WScript.ScriptFullName))>=0) continue;
		return 0;
}

```

# **Digisparkプログラミングの勘所**
「ソフト」なソフトウェア開発に慣れている人間が、今回Digisparkプログラミングして気になった点を挙げてみます。厄介なのが「コンパイルは通っても実行時エラーの場合、無反応」なところ。普通はデバッガ等「見える化」を施すことがベストですが、面倒がりは仮説検証の繰り返しをしていると思います。その中で私が躓いた点を列挙します。

- **pulseIn()はloop()中に複数回実行できない？**
> 他のArduino機種では問題ないかもですが、Digisparkの場合、コンパイルエラーではなく、USB接続時に落ちてしまうようでした。まあ・μ秒の世界でアナログ値フルで取得するには荷が重すぎるということでしょうか・・しかしながらIRの信号はアナログデータではなくビットデータなので、代わりに「PINB & _BV(2)」といった、ポートを直接見るような記述になっています。

- **関数定義は呼び出し前に書く**
> つい癖でルーチンを後ろに書いてしまいそうになりますが、その場合コンパイル時エラーは出ないけれどもUSB接続時に落ちてしまいます。

- **DigiUSBを利用する場合は、DigiUSB.refresh(),DigiUSB.delay()を都度挿入する**
> プログラムは順を追って一つのことしかできないので、待ち受けループ等の処理をさせると、DigiUSBとの通信が疎かになり、USB認識が途絶えたりする場合があります。ループ処理中に**DigiUSB.refresh()**等を実行すると、DigiUSBとの通信の為の割り込みができるようになります。
長い文字列を通信する場合は、**DigiUSB.delay()**等を使ってそれなりに待ちを入れないといけないようです。

# **個人的なあとがき**
今回製作に至ったそもそもの動機は他のパソコンの電源ON/OFF。「MagicPacket(WOL)使えばいいじゃん」とツッコまれそうですが、Windows10からシャットダウン時のフリーズが激しく動作が不安定だったので、電源自体を制御するに至りました。（がんばるベクトルが違う…）
今回はパソコンのON OFFでしたが、スイッチについては、様々な箇所に応用ができます。身近なUSB給電、しかも最低限の部品でリモコン操作スイッチを作れる点においては、有効活用できるかと思いますので参考になれば幸いです。