RICOH THETAでRuby-PTP-IPの紹介
RICOH THETAってPTP-IPで制御できるんですね↓
http://mobilehackerz.jp/contents/Review/RICOH_THETA/WiFi_Control
昔作ったPTP-IPのライブラリがあるのでRubyからシャッターを動作させてみます。
今後も時間を作って、写真の取得などの記事も書きたいですね。 →書いた
ライブラリは、この記事を書くまではruby_ptp_responderと言うプロジェクト名でしたが少しキャッチーにruby_ptp_ipへと変更しました。
こちら(github)
https://github.com/stoikheia/ruby_ptp_ip
に置いてあります。
今見るとmoduleでまとめていなかったりするのがムズムズしますね。
Rubyでどうやって動かすのか、という点に興味ある方は最小コードでのシャッター制御をGOROman氏が公開しているので↓
https://gist.github.com/GOROman/7596186#file-thetatest-rb
そちらのほうがまずいいかもです。
まあー、自分がやることは既に無い感じですけど、
ライブラリの紹介ついでの記事にします。
(2014/08/23修正)
シーケンス
THETAのシャッターを動作させる手順は、以下のシーケンスです。
(青いノートはRuby-PTP-IP上での説明です)
より詳細に知りたい人は
PTP(PIMA15740-2000)とPTP-IP(CIPA_DC-005-2005)を読むと良いです。
THETA側がレスポンダと呼ばれる、機能を提供する側です。
要求を出すこっち側はイニシエータと呼ばれます。
コマンド用とイベント用それぞれ2つのコネクションを確立します。
上の図では緑の矢印がコマンドコネクション、
赤の矢印がイベントコネクションでの送受信として見てください。
イベント用のコネクションを開いた後、
InitCommandRequestのAckで取得したコネクションIDをInitEventRequestで渡し、
コマンドとイベントそれぞれのコネクションの対応をTHETAに教えます。
GetDeviceInfoはリクエストを送った後にデータフェイズになるオペレーションコードです。
DeviceInfoがデータパケットで送られてきた後、データフェイズが完了してからレスポンスパケットが帰ってきます。
DeviceInfoの取得は必須ではないです。
OpenSessionでセッションを開始後、シャッターを動作させるのですが
コマンドには同期コマンドと非同期コマンドがあり、
THETAのシャッターを開始するInitiateCaptureは非同期コマンドです。
THETAがコマンドを受け取った後に、
イベント用のコネクションからObjectAddedイベント、続いてCaptureCompleteイベントが来ます。
シャッター操作はコネクション確立、データフェイズ、セッション、非同期イベントと、PTP-IPの要素が全て入っているのでPTP-IPの学習にもってこいですね。
コード
このライブラリはレスポンダという、いわばTHETA側の機能を実現するために作ったものですが、PTP-IPの命令とパケットタイプはすべて実装してあるのでTHETAに命令するイニシエータとしても使えます。
汎用性のためにプリミティブなライブラリになっているので、PTP-IPのパケットやデータをSocketなどで送受信する処理は使用者が書くようになっていますが、そこはRubyなら簡単に書ける部分でしょう。
ライブラリの使い方全般として、
送信したい場面に応じて
payload = PTPIP_payload_HOGE.new() payload.aaaa = bbbb #データ格納とか packet = PTPIP_packet.create(payload)
パケットクラスはto_dataメソッドでバイト配列を作成するので
それをpack("C*")でStringにしてSocketに書き込みます。
write_socket(packet.to_data.pack("C*")) #to_dataの時点ではバイト配列なのでpackする
逆に受信したデータは
- unpack("C*")バイト配列にする
- PTPIP_packet.new()に渡す
packed_data = read_socket() packet = PTPIP_packet.new(packed_data.unpack("C*")) p packet.payload.cccc #データに応じたペイロードが格納されている
という流れです。
GUIDは適当につくりました。
縦に長いコードですが、
- 値を入れる
- 送る
- 受信
の繰り返しなだけなので内容は簡単だと思います。
#!/bin/ruby require 'socket' require './ruby_ptp_ip/ptp.rb' require './ruby_ptp_ip/ptp_ip.rb' # THETAのIPアドレスとPTP-IPのポート番号 ADDR = "192.168.1.1" PORT = 15740 # THETAに送るレスポンダ情報 NAME = "Ruby_THETA_Shutter" GUID = "ab653fb8-4add-44f0-980e-939b5f6ea266" PROTOCOL_VERSION = 65536 # GUID文字列を整数の配列にする関数 def str2guid(str) hexes = str.scan /([a-fA-F0-9]{2})-*/ hexes.flatten! raise "Invalid GUID" if hexes.length != 16 hexes.map do |s| s.hex end end # パケットを書き込む関数 def write_packet(sock, pkt) sock.send(pkt.to_data.pack("C*"), 0) end # パケットを読み込む関数 def read_packet(sock) data = [] data += sock.read(PTPIP_packet::MIN_PACKET_SIZE).unpack("C*") len = PTPIP_packet.parse_length(data) data += sock.read(len-PTPIP_packet::MIN_PACKET_SIZE).unpack("C*") PTPIP_packet.new(data) end # データフェイズの読み込み関数 def recv_data(sock, transaction_id) recv_pkt = read_packet(sock) raise "Invalid Packet : #{recv_pkt.to_s}" if recv_pkt.type != PTPIP_PT_StartDataPacket raise "Invalid Transaction ID" if recv_pkt.payload.transaction_id != transaction_id data_len = recv_pkt.payload.total_data_length_low data = [] while recv_pkt.type != PTPIP_PT_EndDataPacket recv_pkt = read_packet(sock) raise "Invalid Packet : #{recv_pkt.to_s}" if recv_pkt.type != PTPIP_PT_DataPacket && recv_pkt.type != PTPIP_PT_EndDataPacket raise "Invalid Transaction ID" if recv_pkt.payload.transaction_id != transaction_id data += recv_pkt.payload.data_payload end raise "Invalid Data Size" unless data_len == data.length return data end # コマンドコネクション初期化関数 def init_command(sock) init_command_payload = PTPIP_payload_INIT_CMD_PKT.new() init_command_payload.guid = str2guid(GUID) init_command_payload.friendly_name = NAME init_command_payload.protocol_version = PROTOCOL_VERSION init_command = PTPIP_packet.create(init_command_payload) write_packet(sock, init_command) read_packet(sock)#ACK end # イベントコネクション初期化関数 def init_event(sock, conn_number) init_event_payload = PTPIP_payload_INIT_EVENT_REQ_PKT.new() init_event_payload.conn_number = @conn_number init_event = PTPIP_packet.create(init_event_payload); write_packet(sock, init_event) read_packet(sock) end # データフェイズのあるオペレーションコード実行関数 def data_operation(sock, operation_code, transaction_id, parameters = []) op_payload = PTPIP_payload_OPERATION_REQ_PKT.new() op_payload.data_phase_info = PTPIP_payload_OPERATION_REQ_PKT::NO_DATA_OR_DATA_IN_PHASE op_payload.operation_code = operation_code op_payload.transaction_id = transaction_id op_payload.parameters = parameters op_pkt = PTPIP_packet.create(op_payload) write_packet(sock, op_pkt) data = recv_data(sock, transaction_id) recv_pkt = read_packet(sock) return recv_pkt, data end # シンプルなオペレーションコードの実行関数 def simple_operation(sock, operation_code, transaction_id, parameters = []) op_payload = PTPIP_payload_OPERATION_REQ_PKT.new() op_payload.data_phase_info = PTPIP_payload_OPERATION_REQ_PKT::NO_DATA_OR_DATA_IN_PHASE op_payload.operation_code = operation_code op_payload.transaction_id = transaction_id op_payload.parameters = parameters op_pkt = PTPIP_packet.create(op_payload) write_packet(sock, op_pkt) read_packet(sock) end # # ここから処理 # TCPSocket.open(ADDR, PORT) do |s| # コマンドコネクション recv_pkt = init_command(s) raise "Initialization Failed (Command) #{recv_pkt.payload.reason}" if recv_pkt.type == PTPIP_PT_InitFailPacket p @conn_number = recv_pkt.payload.conn_number p @guid = recv_pkt.payload.guid p @name = recv_pkt.payload.friendly_name p @protocol_version = recv_pkt.payload.protocol_version TCPSocket.open(ADDR, PORT) do |es| # イベントコネクション recv_pkt = init_event(es, @conn_number) raise "Initialization Failed (Event) #{recv_pkt.payload.reason}" if recv_pkt.type == PTPIP_PT_InitFailPacket print "Command/Event Connections are established.\n" @session_id = 1 @transaction_id = 1 # DeviceInfoの取得とパースと表示 recv_pkt, data = data_operation(s, PTP_OC_GetDeviceInfo, @transaction_id) dev_info = PTP_DeviceInfo.create(data) print "Device Info :\n" p dev_info.to_s raise "GetDeviceInfo Failed #{recv_pkt.payload.response_code}" if recv_pkt.payload.response_code != PTP_RC_OK @transaction_id += 1 # セッション開始 recv_pkt = simple_operation(s, PTP_OC_OpenSession, @transaction_id, [@session_id]) raise "Open Session Failed #{recv_pkt.payload.response_code}" if recv_pkt.payload.response_code != PTP_RC_OK @transaction_id += 1 # キャプチャ開始 # [0,0]はストレージIDとキャプチャフォーマット # それぞれ0の時はデバイス側が判断する. recv_pkt = simple_operation(s, PTP_OC_InitiateCapture, @transaction_id, [0,0]) raise "Initiate Capture Failed #{recv_pkt.payload.response_code}" if recv_pkt.payload.response_code != PTP_RC_OK @transaction_id +=1 @event_transaction_id = recv_pkt.payload.transaction_id print "Capturing...\r" # ObjectAddedイベントの待機 recv_pkt = read_packet(es) raise "Invalid Event Packet" unless recv_pkt.type == PTPIP_PT_EventPacket raise "Invalid Event Code" unless recv_pkt.payload.event_code == PTP_EC_ObjectAdded print "Object Added!\n" # CaptureCompletedイベントの待機 recv_pkt = read_packet(es) raise "Invalid Event Packet" unless recv_pkt.type == PTPIP_PT_EventPacket raise "Invalid Event Code" unless recv_pkt.payload.event_code == PTP_EC_CaptureComplete print "Capture Completed!\n" # セッション終了 recv_pkt = simple_operation(s, PTP_OC_CloseSession, @transaction_id) raise "Close Session Failed #{recv_pkt.payload.response_code}" if recv_pkt.payload.response_code != PTP_RC_OK @transaction_id += 1 end end
イベントのトランザクションIDはめちゃくちゃな値が入っているのでエラーチェックに使えなかったです。(2014/08/23 : こちらのバグでした。)
THETAのPTP-IPでのデバイス情報
上のコードでもDeviceInfoをダンプしているので確認できると思いますが表にしておきます。
ベンダ独自のコードもあるので、そのあたりまだ発掘しがいがありそうです。
使えるオペレーションコード
オペレーションコード | 値 |
---|---|
PTP_OC_GetDeviceInfo | 0x1001 |
PTP_OC_OpenSession | 0x1002 |
PTP_OC_CloseSession | 0x1003 |
PTP_OC_GetStorageIDs | 0x1004 |
PTP_OC_GetStorageInfo | 0x1005 |
PTP_OC_GetNumObjects | 0x1006 |
PTP_OC_GetObjectHandles | 0x1007 |
PTP_OC_GetObjectInfo | 0x1008 |
PTP_OC_GetObject | 0x1009 |
PTP_OC_GetThumb | 0x100A |
PTP_OC_DeleteObject | 0x100B |
PTP_OC_InitiateCapture | 0x100E |
PTP_OC_GetDevicePropDesc | 0x1014 |
PTP_OC_GetDevicePropValue | 0x1015 |
PTP_OC_SetDevicePropValue | 0x1016 |
???? | 0x1022 |
使用されるイベントコード
イベントコード | 値 |
---|---|
PTP_EC_ObjectAdded | 0x4002 |
PTP_EC_DevicePropChanged | 0x4006 |
PTP_EC_StoreFull | 0x400a |
PTP_EC_CaptureComplete | 0x400d |
使用できるデバイスプロパティコード
デバイスプロパティコード | 値 |
---|---|
PTP_DPC_BatteryLevel | 0x5001 |
PTP_DPC_ExposureBiasCompensation | 0x5010 |
PTP_DPC_DateTime | 0x5011 |
PTP_DPC_CaptureDelay | 0x5012 |
???? | 0x502C |
???? | 0xD006 |
???? | 0xD801 |
???? | 0xD802 |
???? | 0xD803 |
???? | 0xD805 |
???? | 0xD806 |
???? | 0xD807 |
可能なキャプチャフォーマット
キャプチャフォーマット | 値 |
---|---|
PTP_OFC_Association | 0x3001 |
PTP_OFC_EXIF_JPEG | 0x3801 |
(と出ているものの、InitiateCaptureで0(デバイスまかせ)以外を指定するとParameterNotSupportedになりました)
可能なイメージフォーマット
イメージフォーマット | 値 |
---|---|
PTP_OFC_Association | 0x3001 |
PTP_OFC_EXIF_JPEG | 0x3801 |