C++でArduinoと通信

記念すべき関西大学 電気通信工学研究会のアドベントカレンダー1日目です!!
初日の今日は、先日行われた学園祭でC++でArduinoと通信した話を書こうと思います。

# 学園祭でした事
今回の学園祭での展示物のひとつとして、子供向けに3色LEDの発光パターンを自分で作って持って帰ろう!!(¥500) という擬似的なプログラミング体験をしてもらいました。GUI上のマウス操作で手元のLEDを見ながら色、順番、点灯時間を操作してもらいました。今回の話はプレビューを手元のLEDで光らせるという話です。

# 使用技術
GUIプログラムからRGB値をArduinoにシリアル通信で送信しています。なお言語はC/C++です。

# Arduinoとシリアル通信
今回は[こちら](https://monitorgazer.blogspot.com/2013/06/carduinoled.html)を参考にしました。
Arduinoとの通信の基本的な流れは以下の通りです。

# Arduinoとシリアル通信
今回は[こちら](https://monitorgazer.blogspot.com/2013/06/carduinoled.html)を参考にしました。
Arduinoとの通信の基本的な流れは以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2963586/b9fc3147-821c-16b0-c62a-9bf02b169158.png)

## 初期化
Arduinoと通信するにはCOMポートを使用します。まずはこのCOMポートを初期化しないといけません。

```C++:serial_init.cpp
HANDLE arduino = INVALID_HANDLE_VALUE;

/// @brief arduinoとの通信の初期化
/// @param com 使用するCOMポート
/// @return 初期化に成功したか
bool init(std::string com) {
	bool Ret;
	if (arduino != INVALID_HANDLE_VALUE) {
		CloseHandle(arduino);
	}
	//1.ポートをオープン
	arduino = CreateFile(com.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

	if (arduino == INVALID_HANDLE_VALUE) {
		std::cout << "PORT COULD NOT OPEN";

		return false;
	}

	//2.送受信バッファ初期化
	Ret = SetupComm(arduino, 1024, 1024);
	if (!Ret) {
		std::cout << "SET UP FAILED";
		return false;
	}
	Ret = PurgeComm(arduino, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);
	if (!Ret) {
		std::cout "CLEAR FAILED";
		return false;
	}

	//3.基本通信条件の設定
	DCB dcb;
	GetCommState(arduino, &dcb);
	dcb.DCBlength = sizeof(DCB);		//	DCBのサイズ：
	//データ
	dcb.BaudRate = 9600;				//	伝送速度：　ボーレートをbps単位で指定
	dcb.fBinary = TRUE;				 	//	バイナリモード：  通常TRUEに設定
	dcb.ByteSize = 8;					//	データサイズ  ：　通常8ビット
	dcb.fParity = NOPARITY;				//	パリティ有無種類：　パリティなしの場合はNOPARITYを指定
	//										奇数パリティの場合は ODDPARITY 　他
	dcb.StopBits = ONESTOPBIT;			//	ストップビットの数：　通常１ビット→ ONESTOPBIT;


	//ハードウェアフロー制御
	dcb.fOutxCtsFlow = FALSE;			//	CTSハードウェアフロー制御：CTS制御を使用しない場合はFLASEを指定
	//										CTS 制御をする場合はTRUEを指定してCTS信号を監視します。					
	dcb.fOutxDsrFlow = FALSE;	  		//	DSRハードウェアフロー制御：使用しない場合はFALSEを指定
	dcb.fDtrControl = DTR_CONTROL_DISABLE; // DTR有効/無効：　無効なら　DTR_CONTROL_DISABLE;ISABLE
	dcb.fRtsControl = RTS_CONTROL_DISABLE; // RTS制御：　RTS制御をしない場合はRTS_CONTROL_DISABLEを指定
	//　　　　　　　 							RTS制御をする場合はRTS_CONTROL_ENABLE　等を指定
	// ソフトウェアフロー制御
	dcb.fOutX = FALSE;				 	//	送信時XON/OFF制御の有無：　なし→FLALSE 
	dcb.fInX = FALSE;				  	//	受信時XON/XOFF制御の有無：なし→FALSE 
	dcb.fTXContinueOnXoff = TRUE;		//	受信バッファー満杯＆XOFF受信後の継続送信可否：送信可→TRUE
	dcb.XonLim = 512;				 	//	XONが送られるまでに格納できる最小バイト数：
	dcb.XoffLim = 512;				 	//	XOFFが送られるまでに格納できる最小バイト数：
	dcb.XonChar = 0x11;				 	//	送信時XON文字 ( 送信可：ビジィ解除 ) の指定：
	//　										一般に、XON文字として11H ( デバイス制御１：DC1 )がよく使われます
	dcb.XoffChar = 0x13;			 	//	XOFF文字（送信不可：ビジー通告）の指定：なし→FALSE
	//											一般に、XOFF文字として13H ( デバイス制御3：DC3 )がよく使われます


	//その他

	dcb.fNull = TRUE;				// NULLバイトの破棄： 破棄する→TRUE
	dcb.fAbortOnError = TRUE;		// エラー時の読み書き操作終了：　終了する→TRUE
	dcb.fErrorChar = FALSE;			// パリティエラー発生時のキャラクタ（ErrorChar）置換：　なし→FLALSE
	dcb.ErrorChar = 0x00;			// パリティエラー発生時の置換キャラクタ
	dcb.EofChar = 0x03;				// データ終了通知キャラクタ　：　一般に0x03(ETX)がよく使われます。
	dcb.EvtChar = 0x02;				// イベント通知キャラクタ ：　一般に0x02(STX)がよく使われます


	Ret = SetCommState(arduino, &dcb);
	if (!Ret) {
		std::cout << "SetCommState FAILED";
		return false;
	}

	//タイムアウト
	COMMTIMEOUTS timeout; 					//	COMMTIMEOUTS構造体の変数を宣言

	timeout.ReadIntervalTimeout = 500;		//	文字読込時の２も時間の全体待ち時間（msec）

	timeout.ReadTotalTimeoutMultiplier = 0; //	読込の１文字あたりの時間
	timeout.ReadTotalTimeoutConstant = 500; //	読込エラー検出用のタイムアウト時間
	//(受信トータルタイムアウト) = ReadTotalTimeoutMultiplier * (受信予定バイト数) + ReadTotalTimeoutConstant

	timeout.WriteTotalTimeoutMultiplier = 0;//	書き込み１文字あたりの待ち時間
	timeout.WriteTotalTimeoutConstant = 500;//	書き込みエラー検出用のタイムアウト時間
	//(送信トータルタイムアウト) = WriteTotalTimeoutMultiplier * (送信予定バイト数) + WriteTotalTimeoutConstant

	Ret = SetCommTimeouts(arduino, &timeout);//	SetCommTimeOut()関数にポートハンドルおよびCOMMTIMEOUTS構造体の
	//アドレスを代入します。

	if (Ret == FALSE) //失敗した場合
	{
		std::cout << "SetCommTimeouts FAILED";
		return false;
	}
	return true;
}

```

引数の`std::string com`は使用するCOMポートを渡します。例えば3番ポートを使用するのであれば`"COM3"`を渡します。(デバイスマネージャーの「ポート(COMとLPT)」から確認できます。)またグローバル空間の`arduino`変数を経由してarduinoと通信していきます。

## 送信
データは1バイトずつ送ります。

```C++:serial_send.cpp
/// @brief arduinoにデータを送る
/// @param data 送るデータ(BYTE)
/// @return 送れたか
bool send(BYTE data) {
	// 通信ができないときは何もしないで終わる
	if (arduino == INVALID_HANDLE_VALUE) {
		return false;
	}

	DWORD dwSendSize;	// 送信データサイズ
	COMSTAT Comstat;	// 通信デバイス情報構造体
	DWORD dwErrorMask;	// エラーコードを受け取る変数


	constexpr auto DataSize = sizeof(data); //データのサイズ取得
	define MAX = 64;
	while(true)	{
		ClearCommError(			//ポートの状態を取得してエラーをクリアする
			arduino,			//通信デバイスのハンドル：CreateFile()で取得したハンドルを指定
			&dwErrorMask,		//下記のエラーコードのポインタを取得→エラーの判別
								//	CD_BREAK（フレーミングエラー）、
								//	CE_RXPARITY（パリティエラー）、
								//	CE_RXOVER（入力バッファーオーバーフロー）他
			&Comstat			//COMSTAT構造体へのポインタを取得
								//	Comstat.cbOutQue 送信バッファーの中に残っているデータ数
		);

		if ((MAX - Comstat.cbOutQue) > DataSize && (Comstat.fCtsHold == FALSE)) {
			break;
		}
		std::cout << "stack over";

		switch (dwErrorMask) {
		case CE_BREAK: {
			std::cout << "break";
			break;
		}
		case CE_RXPARITY: {
			std::cout << "parity";
			break;
		}
		case CE_RXOVER: {
			std::cout << "over";
			break;
		}default: {
			std::cout << "unknown";
			break;
		}
		}
		return false;
	}

	// 1バイトデータを送信
	if (WriteFile(arduino, &data, sizeof(data), &dwSendSize, NULL) == 0) {
		std::cout << "SEND FAILED";
		return false;
	}
	return true;
}
```

特に解説することはありませんが、シリアル通信ではReadFile/WriteFile関数を使用し、ファイルハンドルに初期化したarduinoのハンドルをいれます。

## 受信
arduinoとGUIプログラムでは動作速度が違いますが、今回はRGBの3つの信号を順番通り送る必要があります。このままではデータの先頭がわからなくなります。なのでarduinoとGUIプログラムで同期をとる必要があります。そこで`send`関数で1バイトデータを送り、GUIプログラムは1バイト受信するまで待機するとった流れになります。
まずはwindowsからどれだけデータが送られてきたかを確認します

```C++:reciev.cpp
/// @brief データ受信する
/// @return 受信したデータ数
DWORD available() {
	DWORD dwErrors;
	COMSTAT ComStat;
	DWORD dwCount;

	// COMポートの状況取得
	ClearCommError(arduino, &dwErrors, &ComStat);
	switch (dwErrors) {
	case 0: {

		break;
	}
	case CE_BREAK: {
		std::cout << "break";
		return 0;
		break;
	}
	case CE_RXPARITY: {
		std::cout << "parity";
		return 0;
		break;
	}
	case CE_RXOVER: {
		std::cout << "over";
		return 0;
		break;
	}default: {
		std::cout << "unknown";
		return 0;
		break;
	}
	}

	// 未処理のデータ数
	dwCount = ComStat.cbInQue;

	return dwCount;
}
```

また先頭1バイトだけ読み取ります。

```C++:reciev.cpp
/// @brief データの読み取り
/// @return 受信したデータの先頭1バイト
int read() {
	int buf = -1;
	auto count = available();

	// データを受信していない場合は-1を返却
	if (count == 0) {
		return buf;
	}
	DWORD rc;

	// 先頭1バイトをbufに格納
	if (ReadFile(arduino, &buf, 1, &rc, NULL) == 0) {
		std::cout << "failed to recieve";
		std::cout << GetLastError();
	}
	return buf;
}
```

## 終了処理
最後にarduinoとの通信を遮断します。ここも特に解説することはありません。

# おわりに
突貫で書いた記事なのでいろいろ抜けがありそうですが、とりあえずカレンダー初日の記事です。なんかあればコメント、編集リクエストください。