2022年1月25日火曜日

2D LiDAR LDS-006を買ってみた。

 Aliexpressを見ていたらロボット掃除機のLiDARが10ドルで売ってたのでつい買ってしまった。送料いれて15ドルだった。

シールからはEcovacs RoboticsのLDS-006っていうセンサーだということがわかる。
レーザーToFセンサーによる回転式のLiDAR。仕様は不明…
一応台座ごとプチプチに包まれて届いたので届いた。台座が付いてるのでそのまま平面部に置ける。
センサー部を回すのはベルト駆動のDCモーター。ベルトなのでまあまあ静か。ちなみにセンサー部めっちゃ回してるけど、センサー部の電力はコイルで非接触で給電してる模様。
通信は回転中心に赤外線LEDとそれの受光部があってセンサーから台座には一方通行でシリアル通信してるみたい。
接続はPHコネクタっぽい。
分解画像とピンアサインはLDS-006 Lidarを参照した。
赤が5V、緑がRX、青がTX、黒がGNDらしい。
とりあえずPHコネクタがピッタリ入った。ピッチが2.0mmなのでピンヘッダとかそのままさせないのよねぇ。UARTのレベルは3.3VらしいのでCP2102のUSBのドングルを使用した。5V電源もそちらからとった。

少し調べてみると中国語の情報だけどこのLDS-006を解析している人がいたので参考にさせてもらった。
Laser-Radar-LDS-006-Drive-Test
色んなサイトを見ているとモーターのコントロールを別にやらないとダメそうなんだけど試しにstartlds$コマンドを送ってみたら回ってデータも取れてそう。
回り始めると22バイトずつのデータがめっちゃ流れてくる。おそらく4°ずつのデータかな。
ただ、距離は4個分入っているので分解能としては1°って言うことでいいのかなぁ?
ちなみに止めるときはstoplds$らしい。
CamsenseX1用に表示プログラムを作った人が公開してくださっていたのでこれをLDS-006のプロトコルに対応するようにテキトーに改造して試してみた。
とりあえず取れてそう。センサー本体を回すと平面部の角度が変化する。
距離はどのぐらい飛ぶんだろうなぁ
これを作ってから気がついたんだけど、通信プロトコルがNeato XV11 LIDARのfirmware v2.4とv2.6のプロトコルにそっくりである。もしかしてまんまそのままだったりして?
LDS-006の解析結果との違いは距離の2bytesのうちの上位2bit分がエラーflagとして使われているぐらいなもんだし。
掃除機の部品で角度の分解能がいまいちとはいえ、LiDARも安く手に入るようになったなぁ
カスタムファーム開発してる人もいるっぽいんだけど分解能とか改善できるのかね。

追記:LDS-006のプロトコルが実はXV11と一緒なんじゃないかというところが気になって後日距離データの上位2bit分を解析してみた。やっぱりXV11と同様、距離データは14bitでHigh byteの上位2bit分が0以外になるとLow byteがエラーコード?になるようで16bit目がHighになると距離のLow byteは0x88とか0x99固定になってた。
XV11用に開発されたツールとかも使えたりするんじゃないかな。

2022年1月22日土曜日

ESP32でバーチャルスティックを使ってみた。

 スマホゲームとかで画面上に指を置くとアナログのジョイスティックみたいに使えるバーチャルスティックをESP32で使ってみた。ESP32でWebサーバーを立てて、ブラウザ上のバーチャルジョイスティックからラジコンを操作してみた。


ブラウザ上でJavascriptによってバーチャルスティックを実現できるライブラリは古くから存在しているようで、今回はvirtualjoystick.jsを使わせてもらった。

このライブラリでは画面をタッチするとバーチャルスティックが出て座標が取得できる。このライブラリからESP32本体にデータを送るためにはWebSocketを使ってみた。

virtualjoystick.jsから出てきた座標データをESP32のWebSocketサーバーにJavascriptで投げる感じ。

<html>
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		
		<style>
		body {
			overflow	: hidden;
			padding		: 0;
			margin		: 0;
			background-color: #BBB;
			overscroll-behavior-y: none;
		}
		#info {
			position	: absolute;
			top		: 0px;
			width		: 100%;
			padding		: 5px;
			text-align	: center;
		}
		#info a {
			color		: #66F;
			text-decoration	: none;
		}
		#info a:hover {
			text-decoration	: underline;
		}
		#container {
			width		: 100%;
			height		: 100%;
			overflow	: hidden;
			padding		: 0;
			margin		: 0;
			-webkit-user-select	: none;
			-moz-user-select	: none;
		}
		</style>

	</head>
	<body>
		<div id="container"></div>
		<div id="info">
			画面をタッチしてスライド
			<br/>
			<span id="result"></span>
			<br/>
			<span id="consoleArea"></span>
		</div> 
		<div id="consoleArea">sa</div>
<script src="./virtualjoystick.js"></script>
<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  window.addEventListener('load', onLoad);
  function initWebSocket() {
    console.log('Trying to open a WebSocket connection...');
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage; // <-- add this line
  }
  function onOpen(event) {
    console.log('Connection opened');
  }
  function onClose(event) {
    console.log('Connection closed');
    setTimeout(initWebSocket, 2000);
  }
  function onMessage(event) {
    var consoleArea = document.getElementById("consoleArea");
    consoleArea.innerHTML = event.data;
  }
  function onLoad(event) {
    initWebSocket(); 
  }
  function toggle(){
    websocket.send('toggle');
  }
			console.log("touchscreen is", VirtualJoystick.touchScreenAvailable() ? "available" : "not available");
	
			var joystick	= new VirtualJoystick({
				container	: document.getElementById('container'),
				mouseSupport	: true,
				limitStickTravel	: true,
			});
			joystick.addEventListener('touchStart', function(){
				console.log('down')
			})
			joystick.addEventListener('touchEnd', function(){
				console.log('up')
			})
			var prevX = 0;
			var prevY = 0;
			var newX = 0;
			var newY = 0;
			setInterval(function(){
				var outputEl	= document.getElementById('result');
				newX = Math.round(joystick.deltaX());
				newY = Math.round(joystick.deltaY()) * -1;
				outputEl.innerHTML	= '<b>Position:</b> '
					+ ' X:'+newX
					+ ' Y:'+newY;
				if ( newX != prevX || newY != prevY ){
				    websocket.send("x:"+newX+",y:"+newY);
				}
				prevX = newX;
				prevY = newY;
			}, 1/30 * 1000);
</script>
</body>
</html>

ESP32のWebサーバーで表示させるHTMLファイルはこんな感じ。エラー処理適当だけどとりあえずESP32のWebSocketサーバーにバーチャルスティックの座標データを送りつける感じ。

void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
  AwsFrameInfo *info = (AwsFrameInfo*)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
    data[len] = 0;
    /*if (strcmp((char*)data, "toggle") == 0) {
      ledState = !ledState;
      notifyClients();
    }
    */
    //Serial.println((char*)data);
    uint8_t ypos = 0;
    char strx[4];
    char stry[4];
    if(data[0] == 'x'){
      for (int i=2; i <= len; i++){
        if(data[i] == 'y'){
          ypos = i + 2;
          break;
        }else{
          strx[i-2] = data[i];
        }
      }
      for (int i=ypos; i <= len; i++){
        stry[i-ypos] = data[i];
      }
      //Serial.println(ypos);
      x = atoi(strx);
      y = atoi(stry);
      #ifdef debug
      Serial.print("x:");
      Serial.print(x);
      Serial.print(",y:");
      Serial.println(y);
      #endif
    }
  }
}

そんでもってESP32側のWebSocketサーバーでバーチャルジョイスティックのデータを受けたらグローバル変数に突っ込んでloopの中でモーターにデータを送る形にしてみた。

HTMLファイルとvirtualjoystick.jsは結構大きくなってしまったのでOnline converter: File to (cpp) gzip byte arrayを使用してgz圧縮してスケッチ内に組み込んでしまった。

とりあえず前に作った車体に乗っけてみて実験してみた。(写真撮りながら難しすぎ

対向二輪タイプの車体なのでバーチャルジョイスティックの値をそれぞれの車輪にどう振り分けるかの計算をもう少し調整しないといけないかも。レスポンス的にも十分問題なさそうなのでもう少し改良を進めようかな。

blynkとかでも同じようなことができるけどESP32だけでスマホ側にアプリのインストール要らないのは便利かもしれない。


2022年1月4日火曜日

Mirakurunにリバースプロキシで外部からアクセスする

 Mirakurunを外部からアクセスするのにApache2のリバースプロキシを使用してみた。これならDigest認証をApache側でやったり、バーチャルホストでサブドメインで管理できたりして便利そうだし。でもMirakurunの作者もリバースプロキシの下に置かないでくださいって言ってるので本当はやってはいけません…ちょっと設定するときだけ使いたかったので…

まずはMirakurun側の設定。Hostnameをちゃんと設定しないとリバースプロキシからアクセスするとJavaScript系が全部403で弾かれてこまった…

sudo EDITOR=nano mirakurun config server

で設定ファイルを開いて、hostnameがもうあるならそちらを編集して、ない場合は追加する。

# logLevel: <number>
logLevel: 2

# path: <string>
path: /var/run/mirakurun.sock

# port: <number>
# You can change this if port conflicted.
# Don't expose this port on the internet, not even with NAPT.
# Use this in LAN or VPN.
# `~` to disable TCP port listening.
port: 40772
hostname: mk.hoge.f5.si

こんな感じでhostnameに使用するDDNSのアドレスを入れて保存する。

sudo mirakurun restart

で再起動する。

次にApache2の設定。

sudo nano /etc/apache2/sites-available/000-default.conf

設定するコンフィグファイルを開いて、バーチャルホストを追加。

<VirtualHost *:80>
ServerName mk.hoge.f5.si

        <IfModule mod_proxy.c>
        ProxyRequests   Off
        ProxyPreserveHost       Off
        ProxyErrorOverride Off
        ProxyPass       /rpc       ws://127.0.0.1:40772/rpc
        ProxyPassReverse        /rpc       ws://127.0.0.1:40772/rpc
        ProxyPass       /       http://127.0.0.1:40772/
        ProxyPassReverse        /       http://127.0.0.1:40772/
        </IfModule>

</VirtualHost>

ここでWebSocketの部分も追加しておかないとメインページしか表示されなくて焦った。認証とかは適宜追加しておく。

sudo a2enmod proxy proxy_wstunnel
sudo service apache2 restart

これで設定が有効になってるはず。

今回ハマったのはMirakurun側のHostnameがちゃんと設定されてないとうまくアクセスできない(mDNSとかでアクセスする際もちゃんと設定しておかないとIPからじゃないと行けないかも)のと、Apache側でWebSocketもリバースプロキシ設定してやらないと行けないところ。/rpcのところは/rpc/だと行けなかったり色々ハマってしまったのでまた設定するとき用のメモでした。

Mirakurunは外部ネットワークからアクセスする想定ではないので本来はVPNとかを使ったほうが良いかも。とりあえず設定が終わったらアクセスできないように設定しておいたほうがよいですな。