Arduino初心者がESP32でWebSocketラジコン作った話

Arduino歴3ヶ月の初心者が安価で高機能なマイコンESP32を使ってスマホから操作できるラジコンを作った時に躓いた点などを備忘録兼共有用に時系列順で書き残す
#経緯
とある所属しているロボコン団体のメンバー募集兼遊びとしてラジコンを作る企画が浮上した
そのときはESP32っていうwifi飛ばせるマイコンがあるらしいよという話だけを聞いのだが個人的にも興味が湧いたので企画に参加することにした
#初期の最終目標

- スマホで操作
- すぐ動かせる
- 電源は1つ 電池
- 簡単に組み立てられる
- 予算は一機¥3000

#ハード
一般的なホビー用DCモーターx2の二輪駆動のラジコン

チームにいた有識者はESPはノイズに弱いと繰り返して別のマイコンを使うか電源をモーターとESPで分けることを薦めてきた
ブラシ付きモーターは回る時に接点の切り替えからノイズが発生する
なお私はなんらかのフィルターをつけてるもしくは何かしらで絶縁して対処すればいいと考えていたのでその程度で引き返すことは考えていなかった

最初の目標はESPはUSBから電源を供給した状態で別電源を使ってモーターを回すこと

##「とりあえず動く」までに購入した部品
###ESP32-DevKitC-32E
主役

- ESP32-DevKitC-32E ¥1230 [秋月電子](https://akizukidenshi.com/catalog/g/gM-15673/)

###ブレッドボード 301 (没)
チームの手元にあった
27列

- EIC-301 ¥200 [秋月電子](https://akizukidenshi.com/catalog/g/gP-00313/)

###モーター タミヤダブルギアボックス (没)
チームの手元にあった
調べたところ¥780
モーター電圧は3V以下

###二輪ロボットプラットフォーム (高すぎ没)
高すぎ

- 二輪ロボットプラットフォーム ¥1480 [秋月電子](https://akizukidenshi.com/catalog/g/gK-13651/)

###モータードライバー TB67H450 (没)
チームで最初に買いに行ったモータードライバーがこれ

- TB67H450 ¥320 [秋月電子](https://akizukidenshi.com/catalog/g/gK-14753/)

一般的な1回路のモータードライバーだ
選んだ理由はよく分かっていない

端子は8本
IN端子x2
OUT端子x2
モーター電源 VM
モーター電流設定 Vref
モーター電流検出 RS
GND

IN端子2本はESPに繋ぎ電流関連は使わないので適宜GND等に接続した
回転速度はIN端子から直接PWMで制御すれば良い

私はタミヤのギアボックスとESP32Eで試作した
電源を3Vに設定して使用

結果動かなかった
モータードライバーのIN端子を直接ESPの電源に繋げるなどして確かめたが動かない
故障等疑ってもう一つ買ってきたのだがここで原因に気づいた

**「・モータ電源電圧(VM)：4.5V~44V」**

完全に敗北した
そもそもこのドライバーではタミヤのギアボックスのモーターを直接制御することなどできなかったのである

チームの別のメンバーはこのドライバーで二輪ロボットプラットフォームのモーターを動かすことに成功していた
定格4.5VのモーターだったようでUSBの電源をそのまま流してやったところ元気に回ったそうだ

###モータードライバー DRV8832 (没)
モーター電圧範囲が3Vを含むようなモータードライバーを個人で探して見つけたのがこれ
- DRV8832 ¥230 [秋月電子](https://akizukidenshi.com/catalog/g/gK-06375/)
今度こそ行けるだろ&前のより¥90安いと作る前から勝手に舞い上がっていたわけだがこのドライバーが割と曲者だった

端子は10本
IN端子x2
OUT端子x2
モーター電源 VCC
モーター電圧設定 Vset
モーター電圧設定用定電圧出力 Vref
モーター電流設定 ISENSE
障害通知 FAULTn

IN端子があって何故モーターの電圧設定の端子があるのか
それはこのドライバーがIN端子からのPWM入力に対応していないからである
どうやら中にPWM生成のロジックが入っているようでVsetを参照してパルスを生成するとのこと
このVsetもPWMに対応していないと書いてあったがこれはESPのアナログ出力でなんとかできるだろう
テスターで確認したところVrefからは常に1.28Vほどが出ていた
これを抵抗等で分圧してVsetに入れるとその4倍の電圧がOUT端子から出てくるということだそう
乾電池直接繋げて即動かすと言う使い方には適していそうだが少なくとも今回の目的には合わない

かなり謎な仕様だなと思いつつ安いしこれでどうにかしたいところ
電流設定等いらない端子は適宜GND等にVsetをそのままVrefに繋げて
IN端子からESPのIO出力3.3Vをそのまま流したところモーターがぴくっと動いた
障害通知端子を確認したところGNDと通電していた

動いたのは大きかったが結局原因は分からず私のやる気は一度ここで尽きた

###モータードライバー DRV8835 (可)
その後のチームの集まりで買い出しに行ったときにメンバーがノリで手に取った一品
ホームページで確認したところどうやら2回路入でモーター電圧等も用途に合っている
変な仕様のドライバー2個使うより安いしとりあえず一個買ってみるかと買った理由もほぼノリ

- DRV8835 ¥450 [秋月電子](https://akizukidenshi.com/catalog/g/gK-09848/)

端子は12本
IN端子x2x2
OUT端子x2x2
ドライバー電源 Vcc
モーター電源 VM
モード設定 MODE
GND

モード設定というのはこのドライバーはDCモーターモードとステッピングモーターモードがあって
ステッピングモーターモードにすると位相制御が直でできるようだ

一番最初のドライバーで作った回路を少し変えてESPのプログラムもそのまま試したところなんとあっさり動いてしまった
二輪ロボットプラットフォームのモーターも動いた

###ブレッドボード 801
新しく購入したドライバーDIPであるためブレッドボードの中央の溝を跨ぐ形でしか配置できず手元の27列ブレッドボード1枚では足りない
同じ¥200で購入できる30列のブレッドボードがあったので購入した

- BB-801 ¥200 [秋月電子](https://akizukidenshi.com/catalog/g/gP-05294/)

###モーター ROB-13302
DRV8835を買いに行った足で隣の千石電商に寄ったところ
二輪ロボットプラットフォームのギアボックス付きモーターに似たものが2個セットで¥600で売っていた
タミヤのダブルギアボックスが¥780だったのでこれは安いということで購入した
定格4.5V

- ROB-13302 ¥600 [千石電商](https://www.sengoku.co.jp/mod/sgk_cart/detail.php?code=EEHD-54NX)

###電池BOX
- スイッチ付き電池BOX単3x3 ¥90 [秋月電子](https://akizukidenshi.com/catalog/g/gP-02666/)


###モータードライバー TC78H653FTG
これは私が秋月電子のページをぶらぶらしていたところ見つけたもの
DRV8835でもいいのだが2回路入りで驚きの¥200
即買った

- TC78H653FTG ¥200 [秋月電子](https://akizukidenshi.com/catalog/g/gK-14746/)

端子は16本
IN端子x2x2
OUT端子x2x2
モーター電源 VM x2
モード設定 LARGE MODE STBY
GND x3

ドライバの電源はモーター電源から供給される
LARGEは電流を多く消費するモーターを使う場合にIN,OUT2組を並列にして使う場合の設定
MODEはDRV8835と同じくステッピングモーターのためのもの
STBYはGNDに落とすことでドライバーをスリープ状態にすることができる

これだけ詰め込んで¥200である
今までのドライバーを買った金はなんだったのだろうか

モード設定の端子を適宜落とすなり上げるなりして
他はDRV8835からほぼ配線を変更することなく置き換えることができた

ただしこのドライバー基盤の上の方に大きめのチップコンデンサが載っていて
ESPにギリギリまで近づけて配置した場合ESPのアンテナにコンデンサが干渉してしまう
さらに端子の識別文字が裏面に印刷されていて一度ブレッドボードに刺すと非常に配線が分かりにくくなる
この二つの問題点は基盤に足をつける際に表裏返すことで解決した
ピンヘッダーのプラスチック部の高さがチップコンデンサよりも高くブレッドボードに刺さりきらないこともないうえ識別文字も見えるため配線がわかりやすい
ランド部はスルーホール加工のため表面よりもハンダ付けの難易度は多少上がるが十分に機能する

モータードライバーはこれで確定

##電源の統一
この時点でモーターのノイズが原因でESPが不安定になることは確認できなかったので電源の統一に取り組んだ

ESPの電源供給は3通り

- USB給電
- 5V端子から5+α~12V給電
- 3.3V端子からきっかり3.3V給電

USB給電は今回電池BOXを用意してしまったうえコストカットの面からもよろしくないので除外する

電池切れの定義が電池一本の電圧が1Vになった時点として電池BOXからの電圧は3.0~4.5V
5V給電にせよ3.3V給電にせよ電源を昇圧もしくは昇降圧しなければならない
5V端子については個人で確認したところ給電してもESPの電源LEDが光らず動作しているのかわからなかった
よって3.3V給電にすることにした

ESPの3.3Vでの消費電流が調べたところ様々な情報があったが平均したところ最高で500mAほどだった
選んだDCDCコンバーターがこちら

- XC9306使用3.3VDCDCコンバーター ¥350 [秋月電子](https://akizukidenshi.com/catalog/g/gK-16055/)

入力5Vで最大900mAまで供給できるらしい
ただこれは基盤に直に半田付けした際の数値でありピンヘッダーを使うと精々500mAというところ
要件は満たしているのでこれでなんとかなりそうだ

##部品確定
以上ESPとモーターは別電源でモーターを動かすところまでたどり着いた

- ESP32-DevKitC-32E ¥1230 [秋月電子](https://akizukidenshi.com/catalog/g/gM-15673/)
- モータードライバー TC78H653FTG ¥200 [秋月電子](https://akizukidenshi.com/catalog/g/gK-14746/)
- ギアボックス付きモーター ROB-13302 ¥600 [千石電商](https://www.sengoku.co.jp/mod/sgk_cart/detail.php?code=EEHD-54NX)
- ブレッドボード BB-801 ¥200 [秋月電子](https://akizukidenshi.com/catalog/g/gP-05294/)
- スイッチ付き電池BOX単3x3 ¥90 [秋月電子](https://akizukidenshi.com/catalog/g/gP-02666/)
- XC9306使用3.3VDCDCコンバーター ¥350 [秋月電子](https://akizukidenshi.com/catalog/g/gK-16055/)

計¥2670で収まった
ブレッドボードの配線はこんな感じ
左の電源ラインを4.5Vに接続すると動く
![E7B713EB-AB14-40C8-B1AB-D8DD518C7B7D.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479627/52ff29c2-f508-4907-9a8a-b3e27e7262c6.jpeg)


#ソフト
メンバーにもコードを書いていた人がいるようだが個人で完成まで漕ぎ着けたのでそれを説明する
##モーター速度制御
ledcWriteで普通にPWMする
##クライアントと通信
ESP32は無線インターフェイスとしてWifiとBluetoothを備えている
ESPとクライアントで通信する一般的な方法は2通り
Blynkというアプリを使う方法とESPとクライアントをWifiで接続してブラウザを利用して情報をやり取りする方法である
Blynkはアプリのインストールやトークンの取得などその場ですぐ使うことは難しいので今回はWifi接続を選択する
Wifi接続もまた2通り
ESPとクライアントを同じWifiに接続して通信する方法とESPをサーバーにして直接Wifiを飛ばしそれにクライアントが接続する方法(softAP)である
すぐ使えるという観点から後者の方が今回の目的に合っているので後者のsoftAPを選択する

下記リンクを参考にsoftAPとhttpRequestでラジコンを作った

http://mukujii.sakura.ne.jp/esp2.html

これでも遊んでいて十分楽しいのだがリアルタイムで制御ができない
折角だからもっと操作性の良いものを作りたい

##WebSocket
さてここからが本題である
WebSocketを使ってリアルタイム通信すればだいぶ良くなるのではないか

###ESP側
有志による優秀なライブラリがあるのでそれを使わせていただく

https://github.com/me-no-dev/ESPAsyncWebServer

Async Web Serverと名前がついているがWebSocketを扱うプラグインが同梱されている
まずはこれとその[依存ライブラリ(AsyncTCP)](https://github.com/me-no-dev/AsyncTCP)をインストールする

ライブラリの使用方法は[ドキュメント](https://me-no-dev.github.io/ESPAsyncWebServer/)に記載されている

WebSocketサンプル
コンソールからWebSocket飛ばすとつながる

```cpp
const IPAddress ip(192,168,1,1);
const IPAddress subnet(255,255,255,0);
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len){
	switch(type){
		case WS_EVT_CONNECT:
			Serial.printf("ws[%u] connect: %s\n", client->id(), client->remoteIP().toString().c_str());
			client->printf("id: %u\nip: %s\n", client->id(), client->remoteIP().toString().c_str());
			client->ping();
			break;
		case WS_EVT_DISCONNECT:
			Serial.printf("ws[%u] disconnect\n", client->id());
			break;
		case WS_EVT_ERROR:
			Serial.printf("ws[%u] error(%u): %s\n", client->id(), *((uint16_t*)arg), (char*)data);
			break;
		case WS_EVT_PONG:
			Serial.printf("ws[%u] pong\n", client->id());
			break;
		case WS_EVT_DATA:
			{
				AwsFrameInfo *info=(AwsFrameInfo*)arg;
				if(info->final && info->index==0 && info->len==len && info->opcode==WS_TEXT){
					data[len]=0;
					String str=(char*)data;
					Serial.printf("ws[%u] text-msg[%llu]: %s\n", client->id(), info->len, str.c_str());
					ws.printfAll("%s\n",str.c_str());
				}		
			}
			break;
	}
}
void setup(){
	Serial.begin(115200);
	delay(1000);

	WiFi.mode(WIFI_AP_STA);
	WiFi.softAP(ssid,pass);
	delay(100);
	WiFi.softAPConfig(ip,ip,subnet);
	Serial.printf("SSID: %s\nPASS: %s\nAPIP: %s\n",ssid,pass,WiFi.softAPIP().toString().c_str());

	ws.onEvent(onEvent);
	server.addHandler(&ws);
	server.begin();
	Serial.println("server started");
}
void loop(){
	ws.cleanupClients();
}
```

###クライアント用HTML
こっちは一般的なWebSocket

```html
<!DOCTYPE html>
<html lang="en" dir="ltr">
	<head>
		<meta charset="utf-8">
	</head>
	<body>
		<pre id="log">Connecting…</pre>
		<script>
			'use strict';
			let ws,ws_send=()=>console.log('uninitialized'),recieved={};
			const ws_init=()=>{
				console.log('ws_init');
				ws=new WebSocket(`ws://${window.location.hostname}/ws`);
				ws.onopen=e=>{
					log.textContent='Opened :)';console.log('opened');
					ws.send('Hello');
				};
				ws.onclose=e=>{
					log.textContent='Closed :(';console.log('closed');
					setTimeout(ws_init,2000);
				};
				ws.onmessage=e=>{
					log.textContent+=`${e.data}\n`;
				};
				ws.onerror=e=>{
					console.log(e);
				};
			};
			window.onload=ws_init;
		</script>
	</body>
</html>
```
ジョイスティック操作にした
[こちら](https://openrtm.org/openrtm/sites/default/files/6357/171108-04.pdf)を参考にさせていただいた

```js
/*
stick=HTMLElement
stick.children[0]=HTMLElement
stick_stat=pressed?true:false
*/

window.onpointermove=e=>{
	if(!stick_stat)return;
	const stick_style=stick.getBoundingClientRect();
	let pos=[
		(e.clientX-stick_style.left-stick_style.width*.5)||0,
		(e.clientY-stick_style.top-stick_style.height*.5)||0
	],l=Math.sqrt(pos[0]*pos[0]+pos[1]*pos[1]);
	pos=pos.map(x=>x*Math.min(stick_style.width*.5,l)/l);
	stick.children[0].style.transform=`translate(-50%,-50%)translate(${pos.join('px,')}px)`;
	if(timer)return;
	//https://openrtm.org/openrtm/sites/default/files/6357/171108-04.pdf
	pos=[Math.atan2(...pos)-Math.PI*.75,Math.min(1,l/stick_style.width*2)];
	ws_send([Math.cos(pos[0])*pos[1],Math.sin(pos[0])*pos[1]]);
	timer=setTimeout(()=>timer=0,100);
};
```


###その他
ESPのString.toInt()が負の数に対応していないらしく送信時はオフセットをつけた
文字列で送信しているため切り出しを楽にするため0paddingをした

#課題
websocketの実装を始めたのが8/9ジョイスティックまで完成したのが8/11なので動作確認があまりできていない
電池電圧が3.6Vあたりまで低下してくるとESPの電源供給が怪しくなってくる
ESPが発振してDCDCコンバーターが熱くなる
やはりコンバーターの許容電流が足りないのかもしれない

8/13追記
ESPの起動時の大食いが原因であることがわかった
電池と並列に100μFの電解コンデンサを挟んだところだいぶマシになった
補助電源としての役割の方が大きいので単純に容量が大きい方がいいとは思う
今手元に100μF以外ないので今後検証する
→470μFで安定した

#おわり
今回のプロジェクトでできたものはここに置いてある

https://github.com/McbeEringi/esp-rc

今後も改良を続けたい