【Python / C】PCの画面を無線で遠隔スクロールする機器をつくってみた

# あらすじ

　友達と漫才の練習する時、セリフを目の前のパソコンに表示してカンペとして利用していますが、
文字を大きく表示させるとスクロールなしに全セリフを見ることができません。
　そこで、遠隔でパソコンの画面を上下スクロールできるような装置を作ってみました。

# システム構成

手元にあるコントローラに取り付けられたスイッチを押すと、PCがスクロールするようなシステムを考えました。

システム全体の構成は、以下としました。

![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/06a649cc-a6d2-d0bf-5fde-db5a64d17897.png)
※ 矢印が一方通行になっているのは、送信情報もその方向にしかいかないからです。

上記システム全体図に関し、詳細を以下に示します。
（実用性もさることながら勉強の一環で作るという目的もあるため、色々な機器を使ってみようと意図して選定しました）

 1. 送信側
  - マイコン：ATtiny2313
  - マイコンの書き込みライタ：AVRISP mkⅡ
  - IDE：Atmel Studio 7
  - 無線モジュール：SBDBT
 
 2. 受信側
  - マイコン：Arduino Micro
  - IDE：Arduino IDE 1.8.11
  - 無線モジュール：rn42xvp-i/rm

 3. PC側
  - OS：Windows
  - 画面スクロール：Python3.7.3

　あとは、本記事では特に触れませんが、シリアル送受信を行うなど、通信のデバッグツールとして以下を使用しました。

 - USB/シリアル変換器：MR-USBSIR-F
 - ターミナル：TeraTerm

　また、Bluetoothによる無線モジュールは送信側(SBDBT)をスレーブ、受信側(rn42xvp-i/rm)をマスターとしました。
　理由として、SBDBTはデフォルト設定ではスレーブとなっており、マスターに変更するにはマイコン（PIC24FJ64GB004-I/PT）の書込みライタが必要なためです。一方、rn42xvp-i/rmはArduinoのシリアルモニタやTeraTermなどのターミナルからコマンドを送信するだけで設定を変更できるため、送信側のrn42xvp-i/rmの設定を変更し、マスタとしました。（設定の詳細は後述）

…と思ったけど、rn42xvp-i/rmはデフォルトでマスタとなっているようでした。しかしそれを抜きに考えても設定変更が簡単にできて便利なので、rn42xvp-i/rmをマスタとしました。

# 受信側の無線モジュール：rn42xvp-i/rm の設定

　前述の通り、送信側の無線モジュールはpicライタがないと設定が変更できないため、受信側のrn42xvp-i/rmをマスタとしました。rn42xvp-i/rmの設定をターミナルからのコマンド送信で事前に変更することで、Bluetoothモジュール同士の接続を可能にしました。

　以下、手順を示します。Arduino IDEのシリアルモニタからでも、TeraTermからの実施でもOKです。
Arduinoのシリアルモニタから実施する場合、以下を書き込んだ上で実施します。

```c:change_bt_settings.c
#include <SoftwareSerial.h>

#define BT_RX 8
#define BT_TX 7
#define BPS 115200 // rn42xvp-i/rmのデフォルトのボーレート

SoftwareSerial btSerial(BT_RX, BT_TX); // Bluetoothとやりとりするためのシリアル設定

void setup()
{
  Serial.begin(BPS); // PC - Arduino間シリアル通信の初期化
  btSerial.begin(BPS); // Arduino - Bluetooth間シリアル通信の初期化
}

void loop()
{
  // ArduinoからBluetoothへの送信処理
  if (Serial.available())
  {
    btSerial.print(char(Serial.read()));
//    btSerial.write(Serial.read()); // こちらでもOK]
  }

  // Bluetoothからの信号の受信処理
  if (btSerial.available())
  {
    Serial.print(char(btSerial.read()));
//    Serial.write(btSerial.read()); // こちらでもOK
  }
  delay(50);
}
```


また、実施には以下記事を参考にさせていただきました。
 ・http://workpiles.com/2014/04/rn42-bluetooth1/ 
 ・http://kosakai.world.coocan.jp/RN-42.html

以下、設定手順です。

|番号|コマンド|改行|応答|意味|
|---|---|---|---|---|
|1|$$$|なし|CMD|コマンドモード開始|
|2|SA,0|あり|AOK|接続のための認証を不要に変更|
|3|SU,96|あり|AOK|ボーレートを9600に変更|
|4|R,1|あり|Reboot!|モジュールを再起動（これで設定変更が反映される）|
※1 … 改行「あり」となっているのは、コマンドに記載されている文字列を送信後に改行やEnterを実施する必要あり
※2 … rn42xvp-i/rm のデフォルトのボーレート設定は115200bpsであり、送信側であるArduino（もしくはターミナルソフトの設定）も115200bpsとする必要があるので注意
※3 … コマンドには変にスペースとか入れると認識されないので注意


# 作ったもの（ハードウェア）

１．送信側（ATtiny2313）

何を土台に使用するか色々考えましたが、普通のプラスチック板やユニバーサルプレートは
加工し辛いため、比較的剛性があって加工も容易な5㎜厚の発泡スチレンボードを使用しました。
その発泡スチレンボードを適当な大きさに加工し、電池ボックスや基板などを取り付けました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/64e7c770-0468-802c-5265-caa75ca04a78.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/6734972a-5d53-3278-77df-6bf9c25005db.png)


２．受信側（Arduino Micro）

受信側はパソコンに直接接続することもあり、ブレッドボードで作成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/17673821-b93f-987b-aa73-f4d9e5abda0c.png)




# ソースファイル

１．送信側（ATtiny2313）

```c:send_data_to_arduino.c
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
 
#define OFF 0   // スイッチング用
#define ON  1   // 同上
#define READ  0 // ポートレジスタ設定用
#define WRITE 1 // 同上
#define CNTD  0 // スイッチ押しっぱなし時間の計測に使用
#define CNTU  1 // 同上
#define KRTIME 100000 // 推しっぱなしと認識されるためのスイッチ押しっぱなし時間


void AVR_init(void)
{
	//シリアル通信などの設定
	DDRD=(READ<<PD0)|(WRITE<<PD1)|(READ<<PD4)|(READ<<PD5)|(WRITE<<PD6);
}

void USART_init(void)
{
	unsigned int baud;
	baud=8000000/16/9600-1;          // ボーレートの計算(計算式は仕様書P.75下部に記載)
	UBRRH=(unsigned char)(baud>>8);  // ボーレート設定(上位byte)
	UBRRL=(unsigned char)baud;       // ボーレート設定(下位byte)
	UCSRC=(0<<USBS)|(3<<UCSZ0);      // 使用bit長を8bit、停止bitを2bitに設定
	UCSRB=(1<<RXEN)|(1<<TXEN);       // 送受信許可
}

void Utx(char *data) //データ送信
{
	int i;
	
	// 終端文字「\0」までループ
	for(i=0; data[i]!='\0'; i++)
	{
		while(!(UCSRA & (1<<UDRE)) );
		UDR=data[i];
	}
}

void INT_init(void)
{
	// INT0及び1(6及び7番ピン)の割込みを許可
	GIMSK=0b11000000; // 一般割り込み許可レジスタ
	MCUCR=0b00001111; // INT0と1共に外部割込みトリガをアップエッジで設定
// 	SREG=0b10000000;  // 多重割り込み禁止→デフォルトで禁止だった
}

ISR(INT0_vect) // ピン7の割込みハンドラ
{
	// チャタリング対策
	_delay_ms(50);
	EIFR=ON<<INTF0;

	Utx("UP");
	Utx("_");  // 終端文字
}

ISR(INT1_vect) // ピン6の割込みハンドラ
{
	// チャタリング対策
	_delay_ms(50);
 	EIFR = ON<<INTF1;

	Utx("DOWN");
	Utx("_");  // 終端文字
}

unsigned char Urx(void) //データ受信
{
	while(!(UCSRA & (1<<RXC)) );
	return UDR;
}

int main(void)
{
	// キーリピート(スイッチ押しっぱなし)時間の計測
	static unsigned long rptcnt[1] = {}; // intが2byteになったのでlongで宣言
	
	AVR_init();
	USART_init();
	INT_init();
	sei(); //全割込み許可命令
		
	PORTD=ON<<PD6; //常に5V出力(スイッチング用)

	for(;;)
	{
		// ボタン押しっぱなしへの対応処理
		if (bit_is_set(PIND, 4)) // PD4が1（ボタンを押している）の場合
		{
			if (rptcnt[CNTD] > KRTIME) // 押しっぱなしを検知
			{
				Utx("UP");
				Utx("_"); // 終端文字
				_delay_ms(100);
			}
			else // キーリピート時間を加算
			{
				rptcnt[CNTD]++;
			}
		}
		else if (bit_is_set(PIND, 5)) // PD5が1（ボタンを押している）の場合
		{			
			if (rptcnt[CNTU] > KRTIME) // 押しっぱなしを検知
			{
				Utx("DOWN");
				Utx("_"); // 終端文字
				_delay_ms(100);
			}
			else // 押しっぱなし時間を加算
			{
				rptcnt[CNTU]++;
			}
		}
		else // 押しっぱなし時間をゼロクリア
		{
			rptcnt[CNTD] = 0;
			rptcnt[CNTU] = 0;
			// memset(rptcnt, 0, sizeof(rptcnt)*10); //処理系に依存するので使わない
		}
	}
}
```

２．受信側（Arduino Micro）

```c:relay_data_to_PC.c
#include <SoftwareSerial.h>

#define BT_RX    8 // Arudino-Bluetooth間のシリアル通信に使用
#define BT_TX    7 // 同上
#define BAUD  9600 // シリアル通信のボーレート
#define OUT_5V   2 // ピン番号
#define IN_5V    3 // 同上
#define LED_OUT  4 // 同上
#define BUFF_MAX 5 // シリアル受信用バッファの容量（単位：文字）

// Arudino-Bluetooth間のソフトウェアシリアル設定
SoftwareSerial btSerial(BT_RX, BT_TX);

void setup()
{
  Serial.begin(BAUD);
  btSerial.begin(BAUD);
  pinMode(OUT_5V, OUTPUT); //スイッチングに使用
  pinMode(IN_5V,  INPUT);  //同上
}

void init_bt() //Bluetoothモジュールとの接続
{
  btSerial.print("$$$"); // 設定モードに遷移
  delay(1000);
  btSerial.print("C,00198600035E\n"); // 他のBluetoothモジュールに接続
  delay(5000);
  btSerial.print("---\n"); // 接続に失敗した場合はコマンドモードを終了（成功時は無視される）
}

void loop()
{
  char buff[BUFF_MAX]={'\0'};
  static int cnt = 0;
  
  digitalWrite(OUT_5V, HIGH); // 常に5V出力（スイッチング用）

  // スイッチを押したらBluetooth接続
  if (digitalRead(IN_5V))
  {
    init_bt();
  }

  // Bluetoothモジュールへの送信処理（シリアルモニタからの送信時に使用）
  if (Serial.available())
  {
    btSerial.print(char(Serial.read()));
    delay(100);
  }

  // Bluetoothモジュールからの受信処理
  if (btSerial.available())
  {
    // 受信した値をPC側に送信
    Serial.print(btSerial.readStringUntil('_')); //readStringだと遅いのでUntilを使用（引数は終端文字） 
    delay(100);
  }
}
```

３．PC側（これのみPython）

```python:scroll_screen_bt.py

import serial
import re
import pyautogui as pgui


def ctrl_screen():  # 0.08sec間隔で処理を実施
    with serial.Serial('COM8', 9600, timeout=0.08, stopbits=serial.STOPBITS_TWO) as ser:

        while True:
            val = ser.readline()
            val = val.decode() # bytes型をstr型（unicode文字列）に変換

            if val == 'UP':
                pgui.typewrite(['up'])  # 画面を上にスクロール
            elif val == 'DOWN':
                pgui.typewrite(['down'])  # 画面を下にスクロール


if __name__ == "__main__":

    ctrl_screen()
```


# 回路
以下、本システムの回路図を示します。
作図にはdraw.ioを使用しました。
初めて回路図を書きましたが、フリーでしかも書きやすく、便利！

１．送信側（ATtiny2313）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/649420cf-9034-f4b1-6b76-aafdbaf1d17e.png)

２．受信側（Arduino Micro）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/d23efbd4-d49c-f1c0-d5cb-8ec0e9eebf1d.png)


# 使い方


１．PC側で、Pythonのスクリプトを起動します。

２．マスタ・スレーブ両方の電源を入れます。（マスタはPCに繋ぐだけ）

３．マスタ側のスイッチを押し、Bluetoothモジュール同士で接続します。
　　接続に成功したら、点滅状態のLEDが点灯状態になります。これで準備完了
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/86cde516-dd16-cb6c-1932-20808392ea6d.png)

４．スレーブ側でスイッチを押し、PCのスクリーンを上下にスクロールさせます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/07b07f85-ad1d-f79e-1c9b-55e317e1f836.png)


# 実施する上での注意点
　ATtiny2313の設定で水晶発振器を外部のものを使用するように変更すると、当然外部の水晶発振器をつけないとマイコンが動かなくなるどころか書き込みすらできなくなります。内部発振器で十分でしょ！と思って発振器をつけていない状態で外部発振器を使用する設定にしたら最後、外付け発振器をつけないと何もできなくなるので注意が必要（私です）

また、Arduinoでソフトウェアシリアルのライブラリを使用する際は、使用できるピンに制約があります。詳しくは下記↓
https://garretlab.web.fc2.com/arduino_reference/libraries/standard_libraries/SoftwareSerial/


# 参考にさせていただいた情報

本記事のここまでに記載したサイト様以外にも、下記サイト様にお世話になりました。
ありがとうございました。

|内容|リンク先|
|---|---|
|ATtiny2313の基本的な使い方|http://lumenbolk.com/?p=19|
|「import serial」で生じたエラーの解決策|https://teratail.com/questions/101843|
|PC(Python)とarudino間のシリアル通信|https://qiita.com/Acqua_Alta/items/9f19afddc6db1e4d4286|
|Arudinoにおけるkbhit()の使用可否|https://forum.arduino.cc/index.php?topic=294636.0|
|Arudinoにおけるkeyイベントの検知方法|https://www.quora.com/How-do-I-detect-a-keyboard-event-in-an-Arduino-IDE-program|
|Pythonによるキー操作方法|https://www.lisz-works.com/entry/pyautogui-key|
|Arudinoによるキー操作方法|https://garretlab.web.fc2.com/arduino_reference/language/functions/usb/keyboard/write.html|
|Arduinoのシリアル通信における文字列の受信方法|https://ch.nicovideo.jp/yugata/blomaga/ar1177727|

# 最後に

ここはこうしたほういいんだよなあ…とか、ここ間違っているぞオラ！等あれば、お手数ですがご指摘いただけますと涙を流して喜びます。