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なら簡単に書ける部分でしょう。


ライブラリの使い方全般として、
送信したい場面に応じて

  1. 各種payloadクラスのインスタンスを作る
  2. それをPTPIP_packetクラスのcreateメソッドに渡してパケットのインスタンスを作る
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する

逆に受信したデータは

  1. unpack("C*")バイト配列にする
  2. 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