sony_camera_remote_api 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,199 @@
1
+ require 'sony_camera_remote_api/raw_api'
2
+ require 'sony_camera_remote_api/camera_api_group'
3
+ require 'forwardable'
4
+
5
+ module SonyCameraRemoteAPI
6
+
7
+ # Camera API layer class, which enables us to handle camera APIs more friendly.
8
+ class CameraAPIManager
9
+ extend Forwardable
10
+ include Logging
11
+
12
+ # Default timeout for waiting camera API becomes available
13
+ DEFAULT_API_CALL_TIMEOUT = 8
14
+ # Default timeout for waiting camera parameter changes
15
+ DEFAULT_PARAM_CHANGE_TIMEOUT = 15
16
+
17
+ def_delegators :@raw_api_manager, :apis, :support?
18
+ def_delegators :@api_group_manager, :get_parameter, :get_parameter!,
19
+ :set_parameter, :set_parameter!,
20
+ :get_current, :get_current!
21
+ def_delegator :@api_group_manager, :support_group?
22
+
23
+
24
+ # Create CameraAPIManager object.
25
+ # @param [Hash] endpoints
26
+ # @param [Proc] reconnect_by
27
+ def initialize(endpoints, reconnect_by: nil)
28
+ @raw_api_manager = RawAPIManager.new endpoints
29
+ @api_group_manager = CameraAPIGroupManager.new self
30
+ @reconnect_by = reconnect_by
31
+ end
32
+
33
+
34
+ # Call camera APIs with checking if it is available by 'getAvailableApiList'.
35
+ # If not available, wait a minute until the called API turns to be available.
36
+ # @param [String] service_type
37
+ # @param [String] method
38
+ # @param [Array,String] params
39
+ # @param [Fixnum] id
40
+ # @param [String] version
41
+ # @param [Fixnum] timeout Timeout in seconds for waiting until the API is available
42
+ def call_api_safe(service_type, method, params, id, version, timeout = DEFAULT_API_CALL_TIMEOUT, **args)
43
+ unless getAvailableApiList['result'][0].include? method
44
+ log.error "Method '#{method}' is not available now! waiting..."
45
+ begin
46
+ wait_event(timeout: timeout) { |res| res[0]['names'].include? method }
47
+ rescue EventTimeoutError => e
48
+ raise APINotAvailable.new, "Method '#{method}' is not available now!"
49
+ end
50
+ log.info "Method '#{method}' has become available."
51
+ end
52
+ @raw_api_manager.call_api(service_type, method, params, id, version)
53
+ end
54
+
55
+
56
+ # getEvent API.
57
+ # Long polling flag is set true as default.
58
+ # @param [Array,String] params
59
+ # @return [Hash] Response of API
60
+ def getEvent(params = [true], **opts)
61
+ params = [params] unless params.is_a? Array
62
+ name, service, id, version = @raw_api_manager.search_method(__method__, **opts)
63
+ response = nil
64
+ reconnect_and_retry do
65
+ if params[0]
66
+ response = @raw_api_manager.call_api_async(service, name, params, id, version, opts[:timeout])
67
+ else
68
+ response = @raw_api_manager.call_api(service, name, params, id, version)
69
+ end
70
+ end
71
+ response
72
+ end
73
+
74
+
75
+ # getAvailableApiList API.
76
+ # @return [Hash] Response of API
77
+ def getAvailableApiList(params = [], **opts)
78
+ name, service, id, version = @raw_api_manager.search_method(__method__, **opts)
79
+ reconnect_and_retry do
80
+ @raw_api_manager.call_api(service, name, params, id, version)
81
+ end
82
+ end
83
+
84
+
85
+ # Wait until 'getEvent' result response meets the specified condition.
86
+ # This method can be used to wait after calling APIs that change any camera parameters, such as 'setCameraFunction'.
87
+ # @yield [Array<Hash>] The block that returns +true+ or +false+ based on the condition of the response of getEvent
88
+ # @yieldparam [Array<Hash>] 'result' element in the response of getEvent
89
+ # @param [Fixnum] timeout Timeout in seconds for changing parameter
90
+ # @param [Boolean] polling This method has 3 patterns to handle long polling flag by 'polling' parameter.
91
+ # * default : The first getEvent call doesn't use long polling, but then later always use long polling.
92
+ # * polling = true : Always use long polling in getEvent call
93
+ # * polling = false : Never use long polling in getEvent call
94
+ # @raise EventTimeoutError
95
+ def wait_event(timeout: DEFAULT_PARAM_CHANGE_TIMEOUT, polling: nil, &block)
96
+ start_time = Time.now if timeout
97
+ # Long-polling is disabled only at the first call
98
+ poll = polling.nil? ? false : polling
99
+ while true do
100
+ response = get_event_both(poll, timeout: timeout)
101
+ begin
102
+ break if yield(response.result)
103
+ rescue StandardError => e
104
+ # Catch all exceptions raised by given block, e.g. NoMethodError of '[]' for nil class.
105
+ end
106
+ sleep 0.1
107
+ if timeout
108
+ raise EventTimeoutError, "Timeout expired: #{timeout} sec." if Time.now - start_time > timeout
109
+ end
110
+ poll = polling.nil? ? true : polling
111
+ log.debug "Waiting for #{block} returns true..."
112
+ end
113
+ log.debug "OK. (#{format('%.2f', Time.now-start_time)} sec.)"
114
+ # pp response['result']
115
+ response['result']
116
+ end
117
+
118
+
119
+ # Ghost method, which handles almost API calls.
120
+ # If Camera API (servie type == camera) is called, call_api_safe() is used.
121
+ # @param [String] method
122
+ # @param [Array,String] params
123
+ # @param [String] service_type
124
+ # @param [Fixnum] id
125
+ # @param [String] version
126
+ # @param [Fixnum] timeout Timeout in seconds for waiting until the API is available
127
+ def method_missing(method, params = [], *args, **opts)
128
+ ignore_error = true if method.to_s.end_with? '!'
129
+ method = method.to_s.delete('!').to_sym
130
+ params = [params] unless params.is_a? Array
131
+ response = nil
132
+ reconnect_and_retry do
133
+ begin
134
+ name, service, id, version = @raw_api_manager.search_method(method, **opts)
135
+ if service == 'camera' || name == ''
136
+ if opts[:timeout]
137
+ response = call_api_safe(service, name, params, id, version, opts[:timeout], **opts)
138
+ else
139
+ response = call_api_safe(service, name, params, id, version, **opts)
140
+ end
141
+ else
142
+ response = @raw_api_manager.call_api(service, name, params, id, version)
143
+ end
144
+ rescue APINotSupported, APINotAvailable, IllegalArgument, HTTPClient::BadResponseError => e
145
+ if ignore_error
146
+ return nil
147
+ else
148
+ raise e
149
+ end
150
+ end
151
+ end
152
+ response
153
+ end
154
+
155
+
156
+ private
157
+
158
+ # Call getEvent without polling if getEvent with polling failed.
159
+ def get_event_both(poll, timeout: nil)
160
+ getEvent(poll, timeout: timeout)
161
+ rescue EventTimeoutError => e
162
+ getEvent(false)
163
+ end
164
+
165
+
166
+ # Execute given block. And if the block raises Error caused by Wi-Fi disconnection,
167
+ # Reconnect by @reconnect_by Proc and retry the given block.
168
+ # @param [Boolean] retrying If true, retry given block. If false, return immediately after reconnection.
169
+ # @param [Fixnum] num Number of retry.
170
+ # @param [Proc] hook Hook method called after reconnection.
171
+ def reconnect_and_retry(retrying: true, num: 1, hook: nil)
172
+ yield
173
+ rescue HTTPClient::TimeoutError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED => e
174
+ retry_count ||= 0
175
+ raise e if @reconnect_by.nil? || retry_count >= num
176
+ log.error "#{e.class}: #{e.message}"
177
+ log.error 'The camera seems to be disconnected! Reconnecting...'
178
+ unless @reconnect_by.call
179
+ log.error 'Failed to reconnect.'
180
+ raise e
181
+ end
182
+ log.error 'Reconnected.'
183
+ @raw_api_manager.reset_connection
184
+ # For cameras that use Smart Remote Control app.
185
+ startRecMode! timeout: 0
186
+ return unless retrying
187
+
188
+ if hook
189
+ unless hook.call
190
+ log.error 'Before-retry hook failed.'
191
+ raise e
192
+ end
193
+ end
194
+ retry_count += 1
195
+ retry
196
+ end
197
+
198
+ end
199
+ end
@@ -0,0 +1,281 @@
1
+ require 'sony_camera_remote_api/camera_api'
2
+
3
+ module SonyCameraRemoteAPI
4
+ # Camera API group sublayer class, which is included in Camera API layer class.
5
+ # This class handles API groups that get/set specific parameters of the camera.
6
+ class CameraAPIGroupManager
7
+ include Logging
8
+ include Utils
9
+
10
+ # APIGroup class.
11
+ # One API group object represents one camera parameter.
12
+ class APIGroup
13
+
14
+ # Create API group object.
15
+ # @param [Symbol] param_symbol API Group name
16
+ # @param [Proc] supported_accessor Get the supported values from getSupportedXXX raw response.
17
+ # @param [Proc] available_accessor Get the available values from getAvailableXXX raw response.
18
+ # @param [Proc] get_accessor Get the current value from getXXX raw response.
19
+ # @param [Proc] set_accessor Set given value to setXXX request.
20
+ # @param [Proc] start_condition Wait until given condition before accessing parameter.
21
+ # @param [Proc] preprocess_value Convert given value and arguments into the internal format to be compared.
22
+ # @param [Proc] check_equality Compare given value and current one to judge whether new value should be set or not.
23
+ # @param [Proc] check_availability Compare given value and avialable ones to judge whether new value is available.
24
+ # @param [Proc] end_condition Wait until given condition after changing parameter.
25
+ def initialize(param_symbol, supported_accessor, available_accessor, get_accessor, set_accessor,
26
+ start_condition: nil,
27
+ preprocess_value: nil,
28
+ check_equality: method(:default_check_equality),
29
+ check_availability: method(:default_check_availability),
30
+ end_condition: nil
31
+ )
32
+ @param_str = param_symbol.to_s
33
+ @supported = ('getSupported' + @param_str).to_sym
34
+ @supported_accessor = supported_accessor
35
+ @available = ('getAvailable' + @param_str).to_sym
36
+ @available_accessor = available_accessor
37
+ @get = ('get' + @param_str).to_sym
38
+ @get_accessor = get_accessor
39
+ @set = ('set' + @param_str).to_sym
40
+ @set_accessor = set_accessor
41
+ @start_condition = start_condition
42
+ @preprocess_value = preprocess_value
43
+ @check_equality = check_equality
44
+ @check_availability = check_availability
45
+ @end_condition = end_condition
46
+ end
47
+
48
+ # Get suported values of this parameter through the defined accessor
49
+ # @param [CameraAPIManager] api_manager
50
+ # @param [Array<Hash>] condition
51
+ def supported_values(api_manager, condition, **opts)
52
+ raw_result = api_manager.send(@supported, **opts)['result']
53
+ { supported: @supported_accessor.call(raw_result, condition) }
54
+ end
55
+
56
+ # Get available values of this parameter through the defined accessor
57
+ # @param [CameraAPIManager] api_manager
58
+ # @param [Array<Hash>] condition
59
+ def available_values(api_manager, condition, **opts)
60
+ raw_result = api_manager.send(@available, **opts)['result']
61
+ { available: @available_accessor.call(raw_result, condition) }
62
+ end
63
+
64
+ # Get current values of this parameter through the defined accessor
65
+ def current_value(api_manager, condition, **opts)
66
+ raw_result = api_manager.send(@get, **opts)['result']
67
+ { current: @get_accessor.call(raw_result, condition) }
68
+ end
69
+
70
+ # If start_condition is defined, wait until the condition satisfies.
71
+ # @param [CameraAPIManager] api_manager
72
+ def start_condition(api_manager, **opts)
73
+ if @start_condition
74
+ api_manager.wait_event { |r| @start_condition.call(r) }
75
+ end
76
+ end
77
+
78
+ # Preprocess given value and arguments to the value which can be compared.
79
+ # @param [Object] value
80
+ # @param [Array] args
81
+ # @param [Array<Hash>] condition
82
+ def preprocess_value(value, args, condition)
83
+ if @preprocess_value
84
+ @preprocess_value.call(value, args, condition)
85
+ else
86
+ value
87
+ end
88
+ end
89
+
90
+ def is_available?(value, available_values, condition)
91
+ @check_availability.call(value, available_values, condition)
92
+ end
93
+
94
+
95
+ def eq_current?(value, current_value, condition)
96
+ @check_equality.call(value, current_value, condition)
97
+ end
98
+
99
+ # @param [CameraAPIManager] api_manager
100
+ # @param [Object] value
101
+ # @param [Array] availables
102
+ # @param [Array<Hash>] condition
103
+ def set_value(api_manager, value, availables, condition)
104
+ api_manager.send(@set, @set_accessor.call(value, availables, condition))
105
+ if @end_condition
106
+ condition_block = (@end_condition.curry)[value]
107
+ api_manager.wait_event &condition_block
108
+ end
109
+ { current: value }
110
+ end
111
+
112
+ private
113
+
114
+ def default_check_availability(value, available_values, condition)
115
+ available_values.include? value
116
+ end
117
+
118
+ def default_check_equality(value, current_value, condition)
119
+ current_value == value
120
+ end
121
+
122
+ end
123
+
124
+
125
+ # Create CameraAPIManager object.
126
+ # @param [CameraAPIManager] camera_api_manager
127
+ def initialize(camera_api_manager)
128
+ @api_manager = camera_api_manager
129
+ @api_groups = make_api_group_list camera_api_manager.apis
130
+ end
131
+
132
+
133
+ # Get current value of a camera parameter.
134
+ # @param [Symbol] group_name Parameter name
135
+ # @return [Object] current value
136
+ # @raise APINotSupported, APINotAvailable, IllegalArgument
137
+ def get_current(group_name, **opts)
138
+ get_parameter(group_name, available: false, supported: false, **opts)[:current]
139
+ end
140
+
141
+
142
+ # Almost same as get_current, but this method does not raise Exception
143
+ # even if the parameter is not supported, available or its arguments illegal.
144
+ # @param [Symbol] group_name Parameter name
145
+ # @return [Object] current value
146
+ def get_current!(group_name, **opts)
147
+ get_parameter!(group_name, available: false, supported: false, **opts)[:current]
148
+ end
149
+
150
+
151
+ # Get supported/available/current value of a camera parameter.
152
+ # @param [Symbol] group_name Parameter name
153
+ # @param [Boolean] available Flag to get available values
154
+ # @param [Boolean] supported Flag to get supported values
155
+ # @return [Hash] current/available/supported values
156
+ # @raise APINotSupported, APINotAvailable, IllegalArgument
157
+ def get_parameter(group_name, available: true, supported: true, **opts)
158
+ result = { current: nil, available: [], supported: [] }
159
+ begin
160
+ grp = search_group group_name
161
+ rescue APIForbidden, APINotSupported => e
162
+ raise e.class.new(result), e.message
163
+ end
164
+ condition = grp.start_condition(@api_manager)
165
+ begin
166
+ result.merge! grp.current_value(@api_manager, condition, **opts)
167
+ rescue APINotAvailable, IllegalArgument => e
168
+ raise e.class.new(result), e.message
169
+ end
170
+ begin
171
+ # Timeout is set shorter than usual for getting hardware-affected parameter.
172
+ result.merge! grp.available_values(@api_manager, condition, timeout: 1, **opts) if available
173
+ result.merge! grp.supported_values(@api_manager, condition, timeout: 1, **opts) if supported
174
+ rescue APINotAvailable, IllegalArgument => e
175
+ # Comes here if the parameter is hardware-affected.
176
+ end
177
+ result
178
+ end
179
+
180
+
181
+ # Almost same as get_parameter, but this method does not raise Exception
182
+ # even if the parameter is not supported, available or its arguments illegal.
183
+ def get_parameter!(group_name, **opts)
184
+ get_parameter(group_name, **opts)
185
+ rescue APIForbidden, APINotSupported, APINotAvailable, IllegalArgument => e
186
+ log.error e.message
187
+ e.object
188
+ rescue HTTPClient::BadResponseError => e
189
+ log.error e.message
190
+ end
191
+
192
+
193
+ # Set a camera parameter to the given value.
194
+ # @param [Symbol] group_name Parameter name
195
+ # @param [Object] value New value to be set
196
+ # @return [Hash] current/available/old values
197
+ # @raise APINotSupported, APINotAvailable, IllegalArgument
198
+ def set_parameter(group_name, value, *args, **opts)
199
+ result = { current: nil, available: [], old: nil }
200
+ begin
201
+ grp = search_group group_name
202
+ rescue APIForbidden, APINotSupported => e
203
+ raise e.class.new(result), e.message
204
+ end
205
+
206
+ condition = grp.start_condition(@api_manager)
207
+ begin
208
+ value = grp.preprocess_value(value, args, condition)
209
+ # If value is equal to current value, do nothing.
210
+ result.merge! grp.current_value(@api_manager, condition, **opts)
211
+ result[:old] = result[:current]
212
+ if grp.eq_current? value, result[:current], condition
213
+ return result
214
+ end
215
+ # If not, check if the value is available.
216
+ result.merge! grp.available_values(@api_manager, condition, **opts)
217
+ if grp.is_available? value, result[:available], condition
218
+ # Save current value and call set API.
219
+ result[:old] = result[:current]
220
+ result.merge! grp.set_value(@api_manager, value, result[:available], condition)
221
+ else
222
+ # If the value is not available, raise error.
223
+ raise IllegalArgument.new, "The value '#{value}' is not available for parameter '#{group_name}'. current: #{result[:current]}, available: #{result[:available]}"
224
+ end
225
+ rescue APINotAvailable, IllegalArgument => e
226
+ raise e.class.new(result), e.message
227
+ end
228
+ result
229
+ end
230
+
231
+
232
+ # Almost same as set_parameter, but this method does not raise Exception
233
+ # even if the parameter is not supported, available or its arguments illegal.
234
+ def set_parameter!(group_name, value, **opts)
235
+ set_parameter(group_name, value, **opts)
236
+ rescue APIForbidden, APINotSupported, APINotAvailable, IllegalArgument => e
237
+ log.error e.message
238
+ e.object
239
+ rescue HTTPClient::BadResponseError => e
240
+ log.error e.message
241
+ end
242
+
243
+
244
+ # Returns whether the parameter is supported or not.
245
+ # @return [Boolean] +true+ if the parameter is supported, false otherwise.
246
+ def support_group?(group_name)
247
+ @api_groups.key? group_name
248
+ end
249
+
250
+
251
+ private
252
+
253
+ # Make API Group hash list from APIInfo list.
254
+ # @param [Array<APIInfo>] apis
255
+ # @return [Hash<Symbol, APIGroup>]
256
+ def make_api_group_list(apis)
257
+ api_names = apis.values.map { |a| a.name }
258
+ setters = api_names.select { |k| k =~ /^getAvailable/ }
259
+ api_groups = {}
260
+ setters.map do |s|
261
+ group_name = s.gsub(/^getAvailable/, '')
262
+ members = [ "get#{group_name}", "getAvailable#{group_name}", "getSupported#{group_name}" ]
263
+ result = members.map { |m| api_names.include?(m) }
264
+ api_groups[group_name.to_sym] = @@api_groups_all[group_name.to_sym] if result.all?
265
+ end
266
+ api_groups
267
+ end
268
+
269
+ # @param [Symbol] group_name
270
+ def search_group(group_name)
271
+ if support_group? group_name
272
+ return @api_groups[group_name]
273
+ else
274
+ raise APINotSupported.new, "Parameter '#{group_name}' is not supported!"
275
+ end
276
+ end
277
+
278
+ require 'sony_camera_remote_api/camera_api_group_def'
279
+
280
+ end
281
+ end