MESHをTHETA Sのリモートシャッターにしてみた


この記事は[RICOH THETA Advent Calendar 2015](http://qiita.com/advent-calendar/2015/theta) 21日目の記事です。
MESHを使ってThetaのシャッターをリモートで切ることができるソフトウェアタグを作ります。
なお、THETA API v2 を利用するため現在対応している機種はTheta Sのみとなります。

##MESHって？
これ https://meshprj.com/
BluetoothでiOSデバイスとペアリングして使えるワイヤレス電子ブロック的なものです。
![MESH](https://first-flight.sony.com/images/pj/mesh/productDetail/3_bnr_tag.jpg)

Move（動き）タグ、Button（ボタン）タグ等のハードウェアタグとソフトウェアタグを組み合わせたレシピを定義することができます。

![Recipe](https://first-flight.sony.com/images/pj/mesh/productDetail/whatmesh-app.png)
先日SDK（ベータ版）が公開され、jsベースでのソフトウェアタグの開発が可能となりました。
これにより、オンライン開発環境でコードを書いて、自分の端末にダウンロードして使うことができるようになります。
また、iPhone版のアプリも提供されたので、iPadがなくても使えるようになり、より手軽に遊べるようになっています。

ソフトウェアタグの開発手順等は以下のリンクなどが参考になりました。

https://meshprj.com/sdk/doc/ja/ （要MESH Developerアカウント）
http://qiita.com/masato/items/159a35c5ce741b6f1058

##Theta Sのシャッターを切るソフトウェアタグ
上記のMESH SDKと開発環境を利用して、MESHのレシピ上からTheta Sのシャッターを切れるソフトウェアタグを書いてみました。
ボタンタグ等と組み合わせて、リモートシャッターが切れるようになります。

ソフトウェアタグの内容は以下のようになります(initialize、executeのみ記載)。
このほか入力をトリガーにするため、input connectorを一つ以上定義します。

```initialize.js
return {
	runtimeValues : { sessionId: "" }
}
```

```execute.js
//Theta Endpoint URL の設定（デフォルトは192.168.1.1）
var endPointURL = "http://192.168.1.1"

var startSessionJSONData = {
	name: "camera.startSession"
} 

ajax( {
	url : endPointURL + "/osc/commands/execute",
	type : "post",
	data : JSON.stringify( startSessionJSONData),
	contentType: "application/json",
	dataType: "json",
	timeout : 5000,
	success : function (contents) {
		//取得したセッションIDをruntimeValuesに保管
		runtimeValues.sessionId = contents.results.sessionId;
		log("Theta session started: " + runtimeValues.sessionId);
		
		//CaptureModeのチェック
		checkCaptureMode(endPointURL);
		
		//シャッターを切る
		takeThetaPicture(endPointURL);
		
		//セッションのクローズ
		closeThetaSession(endPointURL);
		callbackSuccess( {
					resultType : "continue",     
					runtimeValues : runtimeValues
		});
	},
	error : function (XMLHttpRequest, textStatus, errorThrown) {
		log(XMLHttpRequest + " | " + textStatus + " | " + errorThrown);		
		callbackSuccess({
			resultType: "continue"
		});
	}
});

function checkCaptureMode(endPointURL) {
	var getOptionsJSONData = {
		name: "camera.getOptions",
		parameters: {
			sessionId: runtimeValues.sessionId,
			optionNames: ["captureMode"]
		}
	} 
	
	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( getOptionsJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			log("CaptureMode: " + JSON.stringify(contents.results.options.captureMode));
			if(contents.results.options.captureMode != "image"){
				log("Please change capture mode to take photo");
				callbackSuccess({
					resultType: "stop"
				});
			};	
			callbackSuccess( {
					resultType : "continue"
			});
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "continue"
			});
		}
	} );
}

function takeThetaPicture(endPointURL) {
	var takePictureJSONData = {
		name: "camera.takePicture",
		parameters: {
			sessionId: runtimeValues.sessionId
		}
	} 

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( takePictureJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
				log("Took picture");
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "continue"
			});
		}
	} );
}

function closeThetaSession(endPointURL) {
	var closeSessionJSONData = {
		name: "camera.closeSession",
		parameters: {
			sessionId: runtimeValues.sessionId
		}
	} 

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( closeSessionJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			log("Theta session closed: " + runtimeValues.sessionId);
			},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "continue"
			});
		}
	} );
}

return {
	resultType : "continue"
};
```

ちなみにMESHのexport機能で取り出したものは下記。
これを直接importしても使えるはずです。

```
{"formatVersion":"1.0","tagData":{"name":"ControlThetaS","icon":"data:image/png;base64,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","description":"This software tag enables mesh to control Ricoh Theta S camera, such as taking pictures ","functions":[{"id":"function_0","name":"TakePicture","connector":{"inputs":[],"outputs":[]},"properties":[],"extension":{"initialize":"return {\n\truntimeValues : { sessionId: \"\" }\n}","receive":"","execute":"//Theta Endpoint URL の設定（デフォルトは192.168.1.1）\nvar endPointURL = \"http://192.168.1.1\"\n\nvar startSessionJSONData = {\n\tname: \"camera.startSession\"\n} \n\najax( {\n\turl : endPointURL + \"/osc/commands/execute\",\n\ttype : \"post\",\n\tdata : JSON.stringify( startSessionJSONData),\n\tcontentType: \"application/json\",\n\tdataType: \"json\",\n\ttimeout : 5000,\n\tsuccess : function (contents) {\n\t\truntimeValues.sessionId = contents.results.sessionId;\n\t\tlog(\"Theta session started: \" + runtimeValues.sessionId);\n\t\tcheckCaptureMode(endPointURL);\n\t\ttakeThetaPicture(endPointURL);\n\t\tcloseThetaSession(endPointURL);\n\t\tcallbackSuccess( {\n\t\t\t\t\tresultType : \"continue\",     \n\t\t\t\t\truntimeValues : runtimeValues\n\t\t});\n\t},\n\terror : function (XMLHttpRequest, textStatus, errorThrown) {\n\t\tlog(XMLHttpRequest + \" | \" + textStatus + \" | \" + errorThrown);\t\t\n\t\tcallbackSuccess({\n\t\t\tresultType: \"continue\"\n\t\t});\n\t}\n});\n\n//Theta sの撮影モードがimageモードになっていることを確認する\nfunction checkCaptureMode(endPointURL) {\n\tvar getOptionsJSONData = {\n\t\tname: \"camera.getOptions\",\n\t\tparameters: {\n\t\t\tsessionId: runtimeValues.sessionId,\n\t\t\toptionNames: [\"captureMode\"]\n\t\t}\n\t} \n\t\n\tajax( {\n\t\turl : endPointURL + \"/osc/commands/execute\",\n\t\ttype : \"post\",\n\t\tdata : JSON.stringify( getOptionsJSONData),\n\t\tcontentType: \"application/json\",\n\t\tdataType: \"json\",\n\t\ttimeout : 5000,\n\t\tsuccess : function (contents) {\n\t\t\tlog(\"CaptureMode: \" + JSON.stringify(contents.results.options.captureMode));\n\t\t\tif(contents.results.options.captureMode != \"image\"){\n\t\t\t\tlog(\"Please change capture mode to take photo\");\n\t\t\t\tcallbackSuccess({\n\t\t\t\t\tresultType: \"stop\"\n\t\t\t\t});\n\t\t\t};\t\n\t\t\tcallbackSuccess( {\n\t\t\t\t\tresultType : \"continue\"\n\t\t\t});\n\t\t},\n\t\terror : function (XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tlog(\"Error:\" + textStatus + \" \" + errorThrown);\t\t\n\t\t\tcallbackSuccess({\n\t\t\t\tresultType: \"continue\"\n\t\t\t});\n\t\t}\n\t} );\n}\n\n//Theta のシャッターを切る\nfunction takeThetaPicture(endPointURL) {\n\tvar takePictureJSONData = {\n\t\tname: \"camera.takePicture\",\n\t\tparameters: {\n\t\t\tsessionId: runtimeValues.sessionId\n\t\t}\n\t} \n\n\tajax( {\n\t\turl : endPointURL + \"/osc/commands/execute\",\n\t\ttype : \"post\",\n\t\tdata : JSON.stringify( takePictureJSONData),\n\t\tcontentType: \"application/json\",\n\t\tdataType: \"json\",\n\t\ttimeout : 5000,\n\t\tsuccess : function (contents) {\n\t\t\t\tlog(\"Took picture\");\n\t\t},\n\t\terror : function (XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tlog(\"Error:\" + textStatus + \" \" + errorThrown);\t\t\n\t\t\tcallbackSuccess({\n\t\t\t\tresultType: \"continue\"\n\t\t\t});\n\t\t}\n\t} );\n}\n\n//セッションをクローズする\nfunction closeThetaSession(endPointURL) {\n\tvar closeSessionJSONData = {\n\t\tname: \"camera.closeSession\",\n\t\tparameters: {\n\t\t\tsessionId: runtimeValues.sessionId\n\t\t}\n\t} \n\n\tajax( {\n\t\turl : endPointURL + \"/osc/commands/execute\",\n\t\ttype : \"post\",\n\t\tdata : JSON.stringify( closeSessionJSONData),\n\t\tcontentType: \"application/json\",\n\t\tdataType: \"json\",\n\t\ttimeout : 5000,\n\t\tsuccess : function (contents) {\n\t\t\tlog(\"Theta session closed: \" + runtimeValues.sessionId);\n\t\t\t},\n\t\terror : function (XMLHttpRequest, textStatus, errorThrown) {\n\t\t\tlog(\"Error:\" + textStatus + \" \" + errorThrown);\t\t\n\t\t\tcallbackSuccess({\n\t\t\t\tresultType: \"continue\"\n\t\t\t});\n\t\t}\n\t} );\n}\n\nreturn {\n\tresultType : \"continue\"\n};","result":""}}]}}
```

##利用例
ボタンタグと組み合わせたレシピ例。この状態でボタンを押すとThetaのシャッターが切られます。

<img width="375" alt="IMG_7983.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/51577/f95fc663-43fb-8c4a-c3d2-77e552eba86b.png">


##追加でやりたいことなど

- 以下のAdvent Calendarの記事でシャッター音を消す方法が紹介されていたので、ソフトウェアタグでできるようにしたい http://hima-zinn.hatenablog.com/entry/2015/12/13/000000

- 動画撮影用ソフトウェアタグも作りたい

##まとめ
THETA API v2はREST APIで触れるので敷居が低く、とっつきやすかったです。
なお、本題とはずれますがAPIの動作チェックはElasticsearch用のChromeプラグイン[Sense](https://chrome.google.com/webstore/detail/sense-beta/lhjgkmllcaadmopgmanpapmpjgmfcfig)を利用するととても便利でした。

![SnapCrab_NoName_2015-12-21_2-15-12_No-01.png](https://qiita-image-store.s3.amazonaws.com/0/51577/23cc8936-93d0-dac2-3b98-05e13380d992.png)