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.
- checksums.yaml +7 -0
- data/.gitignore +55 -0
- data/.rspec +2 -0
- data/.simplecov +16 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +78 -0
- data/LICENSE +21 -0
- data/README.md +99 -0
- data/Rakefile +145 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/sonycam +6 -0
- data/lib/core_ext/hash_patch.rb +15 -0
- data/lib/sony_camera_remote_api/camera_api.rb +199 -0
- data/lib/sony_camera_remote_api/camera_api_group.rb +281 -0
- data/lib/sony_camera_remote_api/camera_api_group_def.rb +362 -0
- data/lib/sony_camera_remote_api/client/config.rb +266 -0
- data/lib/sony_camera_remote_api/client/main.rb +646 -0
- data/lib/sony_camera_remote_api/error.rb +41 -0
- data/lib/sony_camera_remote_api/logging.rb +76 -0
- data/lib/sony_camera_remote_api/packet.rb +109 -0
- data/lib/sony_camera_remote_api/raw_api.rb +196 -0
- data/lib/sony_camera_remote_api/scripts.rb +64 -0
- data/lib/sony_camera_remote_api/ssdp.rb +72 -0
- data/lib/sony_camera_remote_api/utils.rb +98 -0
- data/lib/sony_camera_remote_api/version.rb +3 -0
- data/lib/sony_camera_remote_api.rb +1044 -0
- data/scripts/Linux/add_ssdp_route.sh +39 -0
- data/scripts/Linux/connect_wifi.sh +114 -0
- data/scripts/connect.sh +36 -0
- data/sony_camera_remote_api.gemspec +35 -0
- metadata +231 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
# Logging function module
|
4
|
+
module Logging
|
5
|
+
# Generic delegator class
|
6
|
+
class MultiDelegator
|
7
|
+
def initialize(*targets)
|
8
|
+
@targets = targets
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.delegate(*methods)
|
12
|
+
methods.each do |m|
|
13
|
+
define_method(m) do |*args|
|
14
|
+
@targets.map { |t| t.send(m, *args) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
class <<self
|
21
|
+
alias to new
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
#--------------------PUBLIC METHODS BEGIN--------------------
|
26
|
+
|
27
|
+
# Get Logger class instance for the user class.
|
28
|
+
# @return [Logger] Logging class instance
|
29
|
+
def log
|
30
|
+
@logger ||= Logging.logger_for(self.class.name)
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
# Set file name or stream to output log.
|
35
|
+
# Log file is common in each user class.
|
36
|
+
# @param [String, IO, Array<String, IO>] log_file file name or stream to output log.
|
37
|
+
# @return [void]
|
38
|
+
def output_to(*log_file)
|
39
|
+
Logging.log_file self.class.name, log_file
|
40
|
+
end
|
41
|
+
|
42
|
+
#--------------------PUBLIC METHODS END----------------------
|
43
|
+
|
44
|
+
|
45
|
+
# Log files and streams shared in user classes.
|
46
|
+
@log_file = []
|
47
|
+
# Use a hash class-ivar to cache a unique Logger per class.
|
48
|
+
@loggers = {}
|
49
|
+
|
50
|
+
# These module class methods are private to user class.
|
51
|
+
class << self
|
52
|
+
# Set log files and streams. Logger class instance is re-created.
|
53
|
+
def log_file(classname, log_file)
|
54
|
+
@log_file = Array(log_file)
|
55
|
+
@loggers[classname] = nil
|
56
|
+
end
|
57
|
+
|
58
|
+
# Get Logger class instance or create it for the user class
|
59
|
+
def logger_for(classname)
|
60
|
+
@loggers[classname] ||= configure_logger_for(classname)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Configure Logger instance for the user class.
|
64
|
+
# 1. create Logger instance with given log file and sterams
|
65
|
+
# 2. set progname to user class name
|
66
|
+
def configure_logger_for(classname)
|
67
|
+
@log_file.compact!
|
68
|
+
fios = @log_file.map do |f|
|
69
|
+
f.is_a?(String) ? File.open(f, 'a') : f
|
70
|
+
end
|
71
|
+
logger = Logger.new MultiDelegator.delegate(:write, :close).to(*fios)
|
72
|
+
logger.progname = classname.split('::')[-1]
|
73
|
+
logger
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'bindata'
|
2
|
+
|
3
|
+
module SonyCameraRemoteAPI
|
4
|
+
|
5
|
+
# The payload of Liveview image packet
|
6
|
+
# @private
|
7
|
+
class LiveviewImagePayload < BinData::Record
|
8
|
+
endian :big
|
9
|
+
# Payload header part
|
10
|
+
uint32 :start_code, assert: 0x24356879
|
11
|
+
uint24 :payload_data_size_wo_padding
|
12
|
+
uint8 :padding_size
|
13
|
+
uint32
|
14
|
+
uint8 :flag
|
15
|
+
uint920
|
16
|
+
# Payload data part
|
17
|
+
string :jpeg_data, :length => lambda { payload_data_size_wo_padding }
|
18
|
+
string :padding_data, :length => lambda { padding_size }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Liveview frame information data version
|
22
|
+
# @private
|
23
|
+
class DataVersion < BinData::Record
|
24
|
+
endian :big
|
25
|
+
uint8 :major
|
26
|
+
uint8 :minor
|
27
|
+
end
|
28
|
+
|
29
|
+
# Point of liveview frame data
|
30
|
+
class Point < BinData::Record
|
31
|
+
endian :big
|
32
|
+
uint16 :x
|
33
|
+
uint16 :y
|
34
|
+
end
|
35
|
+
|
36
|
+
# The payload of Liveview frame information packet
|
37
|
+
# @private
|
38
|
+
class LiveviewFrameInformationPayload < BinData::Record
|
39
|
+
endian :big
|
40
|
+
# Payload header part
|
41
|
+
uint32 :start_code, assert: 0x24356879
|
42
|
+
uint24 :payload_data_size_wo_padding
|
43
|
+
uint8 :padding_size
|
44
|
+
data_version :frame_information_data_version
|
45
|
+
uint16 :frame_count
|
46
|
+
uint16 :single_frame_data_size
|
47
|
+
uint912
|
48
|
+
# Payload data part
|
49
|
+
array :frame_data, :initial_length => lambda { frame_count } do
|
50
|
+
point :top_left
|
51
|
+
point :bottom_right
|
52
|
+
uint8 :category
|
53
|
+
uint8 :status
|
54
|
+
uint8 :additional_status
|
55
|
+
uint40
|
56
|
+
string :padding_data, :length => lambda { padding_size }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Liveview packet definition
|
61
|
+
# @private
|
62
|
+
class LiveviewPacket < BinData::Record
|
63
|
+
endian :big
|
64
|
+
uint8 :start_byte, assert: 0xFF
|
65
|
+
uint8 :payload_type, assert: -> { value == 0x01 || value == 0x02 }
|
66
|
+
uint16 :sequence_number
|
67
|
+
uint32 :time_stamp
|
68
|
+
choice :payload, selection: :payload_type do
|
69
|
+
liveview_image_payload 0x01
|
70
|
+
liveview_frame_information_payload 0x02
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# Liveview image class
|
76
|
+
class LiveviewImage
|
77
|
+
attr_reader :sequence_number, :time_stamp, :jpeg_data
|
78
|
+
def initialize(packet)
|
79
|
+
@sequence_number = packet.sequence_number
|
80
|
+
@time_stamp = packet.time_stamp
|
81
|
+
@jpeg_data = packet.payload.jpeg_data
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Liveview frame information class
|
86
|
+
class LiveviewFrameInformation
|
87
|
+
# Frame class
|
88
|
+
class Frame
|
89
|
+
attr_reader :top_left, :bottom_right, :category, :status, :additional_status
|
90
|
+
|
91
|
+
def initialize(frame)
|
92
|
+
@top_left = frame.top_left
|
93
|
+
@bottom_right = frame.bottom_right
|
94
|
+
@category = frame.category
|
95
|
+
@status = frame.status
|
96
|
+
@additional_status = frame.additional_status
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
attr_reader :sequence_number, :time_stamp, :data_version, :frames
|
101
|
+
def initialize(packet)
|
102
|
+
@sequence_number = packet.sequence_number
|
103
|
+
@time_stamp = packet.time_stamp
|
104
|
+
@data_version = "#{packet.payload.frame_information_data_version.major}.#{packet.payload.frame_information_data_version.minor}"
|
105
|
+
@frames = packet.payload.frame_data.map { |f| Frame.new(f) }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'sony_camera_remote_api/error'
|
2
|
+
require 'httpclient'
|
3
|
+
|
4
|
+
module SonyCameraRemoteAPI
|
5
|
+
|
6
|
+
# Raw API layer class, which call APIs by HTTP POST to endpoint URLs and recieve the response.
|
7
|
+
class RawAPIManager
|
8
|
+
include Logging
|
9
|
+
|
10
|
+
# API Information class
|
11
|
+
class APIInfo
|
12
|
+
attr_accessor :name, :versions, :service_types
|
13
|
+
|
14
|
+
def initialize(name, versions, service_types)
|
15
|
+
@name = name
|
16
|
+
@versions = [versions]
|
17
|
+
@service_types = [service_types]
|
18
|
+
end
|
19
|
+
|
20
|
+
def multi_versions?
|
21
|
+
@versions.length > 1
|
22
|
+
end
|
23
|
+
|
24
|
+
def multi_service_types?
|
25
|
+
@service_types.length > 1
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :apis
|
30
|
+
|
31
|
+
|
32
|
+
# @param [Hash] endpoints Endpoint URIs
|
33
|
+
def initialize(endpoints)
|
34
|
+
@endpoints = endpoints
|
35
|
+
@cli = HTTPClient.new
|
36
|
+
@cli.connect_timeout = @cli.send_timeout = @cli.receive_timeout = 30
|
37
|
+
|
38
|
+
unless call_api('camera', 'getApplicationInfo', [], 1, '1.0').result[1] >= '2.0.0'
|
39
|
+
raise ServerNotCompatible, 'API version of the server < 2.0.0.'
|
40
|
+
end
|
41
|
+
@apis = make_api_list
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Make supported API list
|
46
|
+
# @return [Hash<Symbol, APIInfo>] API list
|
47
|
+
def make_api_list
|
48
|
+
apis = {}
|
49
|
+
@endpoints.keys.each do |s|
|
50
|
+
versions = call_api(s, 'getVersions', [], 1, '1.0')['result'][0].sort.reverse
|
51
|
+
versions.each do |v|
|
52
|
+
results = call_api(s, 'getMethodTypes', [v], 1, '1.0')['results']
|
53
|
+
next unless results
|
54
|
+
results.each do |r|
|
55
|
+
name = r[0].to_sym
|
56
|
+
if apis.key?(name)
|
57
|
+
apis[name].versions << v unless apis[name].versions.index(v)
|
58
|
+
apis[name].service_types << s unless apis[name].service_types.index(s)
|
59
|
+
else
|
60
|
+
apis[name] = APIInfo.new(r[0], v, s)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
apis
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
# Call API by HTTP POST.
|
70
|
+
# @param [String] service_type
|
71
|
+
# @param [String] method
|
72
|
+
# @param [Array, String] params
|
73
|
+
# @param [Fixnum] id
|
74
|
+
# @param [String] version
|
75
|
+
# @return [Hash] Response of API
|
76
|
+
def call_api(service_type, method, params, id, version)
|
77
|
+
params = [params] unless params.is_a? Array
|
78
|
+
request = {
|
79
|
+
'method' => method,
|
80
|
+
'params' => params,
|
81
|
+
'id' => id,
|
82
|
+
'version' => version
|
83
|
+
}
|
84
|
+
# log.debug request
|
85
|
+
begin
|
86
|
+
raw_response = @cli.post_content(@endpoints[service_type], request.to_json)
|
87
|
+
rescue HTTPClient::BadResponseError => e
|
88
|
+
if e.res.status_code == 403
|
89
|
+
raise APIForbidden.new, "Method '#{method}' returned 403 Forbidden error. Maybe this method is not allowed to general users."
|
90
|
+
else
|
91
|
+
raise e
|
92
|
+
end
|
93
|
+
end
|
94
|
+
response = JSON.parse(raw_response)
|
95
|
+
if response.key? 'error'
|
96
|
+
raise APIExecutionError.new(method, request, response), "Request:#{request}, Response:#{response}"
|
97
|
+
end
|
98
|
+
# log.debug response
|
99
|
+
response
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
# Asynchronous call API by HTTP POST.
|
104
|
+
# Currently only used by 'getEvent' API with long polling.
|
105
|
+
# @param [String] service_type
|
106
|
+
# @param [String] method
|
107
|
+
# @param [Array, String] params
|
108
|
+
# @param [Fixnum] id
|
109
|
+
# @param [String] version
|
110
|
+
# @param [Fixnum] timeout Timeout in seconds for waiting response
|
111
|
+
# @return [Hash] Response of API
|
112
|
+
def call_api_async(service_type, method, params, id, version, timeout = nil)
|
113
|
+
request = {
|
114
|
+
'method' => method,
|
115
|
+
'params' => params,
|
116
|
+
'id' => id,
|
117
|
+
'version' => version
|
118
|
+
}
|
119
|
+
conn = @cli.post_async(@endpoints[service_type], request.to_json)
|
120
|
+
start_time = Time.now if timeout
|
121
|
+
loop do
|
122
|
+
break if conn.finished?
|
123
|
+
if timeout
|
124
|
+
raise EventTimeoutError, "Timeout expired: #{timeout} sec." if Time.now - start_time > timeout
|
125
|
+
end
|
126
|
+
sleep 0.1
|
127
|
+
end
|
128
|
+
raw_response = conn.pop.content.read
|
129
|
+
response = JSON.parse(raw_response)
|
130
|
+
if response.key? 'error'
|
131
|
+
raise APIExecutionError.new(method, request, response), "Request:#{request}, Response:#{response}"
|
132
|
+
end
|
133
|
+
response
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
# Search given API from API list.
|
138
|
+
# @param [Symbol] method The method name
|
139
|
+
# @param [String] service
|
140
|
+
# @param [Fixnum] id
|
141
|
+
# @param [String] version
|
142
|
+
def search_method(method, **opts)
|
143
|
+
if @apis && @apis.key?(method)
|
144
|
+
api_info = @apis[method]
|
145
|
+
# use ID=1 if not given
|
146
|
+
id = opts.key?(:id) ? opts[:id] : 1
|
147
|
+
if opts.key? :version
|
148
|
+
if api_info.versions.include? opts[:version]
|
149
|
+
version = opts[:version]
|
150
|
+
else
|
151
|
+
raise APIVersionInvalid, "The version '#{opts[:version]}' is invalid for method '#{method}'."
|
152
|
+
end
|
153
|
+
else
|
154
|
+
# use newest version if not given
|
155
|
+
if api_info.multi_versions?
|
156
|
+
# log.debug "Using newest version '#{api_info.versions[0]}' for method '#{method}'."
|
157
|
+
end
|
158
|
+
version = api_info.versions[0]
|
159
|
+
end
|
160
|
+
if opts.key? :service
|
161
|
+
service = opts[:service]
|
162
|
+
if api_info.service_types.include? opts[:service]
|
163
|
+
service = opts[:service]
|
164
|
+
else
|
165
|
+
raise ServiceTypeInvalid, "The service type '#{opts[:service]}' is invalid for method '#{method}'."
|
166
|
+
end
|
167
|
+
else
|
168
|
+
# raise error if service type is not given for method having multi service types
|
169
|
+
if api_info.multi_service_types?
|
170
|
+
strs = api_info.service_types.map { |item| "'#{item}'" }
|
171
|
+
raise ServiceTypeNotGiven, "The method '#{method}' must be specified service type from #{strs.join(' or ')}."
|
172
|
+
end
|
173
|
+
service = api_info.service_types[0]
|
174
|
+
end
|
175
|
+
return api_info.name, service, id, version
|
176
|
+
else
|
177
|
+
raise APINotSupported.new, "The method '#{method}' is not supported by this camera."
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
# @param [Symbol] method The method name
|
183
|
+
# @return [Boolean] +true+ if the API is supported by this camera. +false+ otherwise.
|
184
|
+
def support?(method)
|
185
|
+
@apis.key? method
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
# Reset HTTPClient.
|
190
|
+
# @return [void]
|
191
|
+
def reset_connection
|
192
|
+
@cli.reset_all
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module SonyCameraRemoteAPI
|
4
|
+
# Helper module for connecting to camera by wi-fi.
|
5
|
+
module Scripts
|
6
|
+
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# Connects to camera by Wi-Fi.
|
10
|
+
# This method does nothing if already connected, which is judged by ifconfig command.
|
11
|
+
# @param [String] interface Interface name, e.g. wlan0
|
12
|
+
# @param [String] ssid SSID of the camera to connect
|
13
|
+
# @param [String] pass Password of the camera to connect
|
14
|
+
# @return [Boolean] +true+ if succeeded, +false+ otherwise.
|
15
|
+
def connect(interface, ssid, pass)
|
16
|
+
run_external_command "sudo bash #{connection_script} #{interface} #{ssid} #{pass}"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Restart the interface and connect to camera by Wi-Fi.
|
20
|
+
# @param [String] interface Interface name, e.g. wlan0
|
21
|
+
# @param [String] ssid SSID of the camera to connect
|
22
|
+
# @param [String] pass Password of the camera to connect
|
23
|
+
# @return [Boolean] +true+ if succeeded, +false+ otherwise.
|
24
|
+
def restart_and_connect(interface, ssid, pass)
|
25
|
+
run_external_command "sudo bash #{connection_script} -r #{interface} #{ssid} #{pass}"
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# Run shell command.
|
30
|
+
# Command output are written to stdout witout buffering.
|
31
|
+
# @param [String] command Command to execute
|
32
|
+
# @return [Boolean] +true+ if succeeded, +false+ otherwise.
|
33
|
+
def run_external_command(command)
|
34
|
+
puts command
|
35
|
+
Open3.popen2e(command) do |_i, oe, w|
|
36
|
+
oe.each do |line|
|
37
|
+
puts line
|
38
|
+
end
|
39
|
+
# Return code
|
40
|
+
if w.value != 0
|
41
|
+
return false
|
42
|
+
else
|
43
|
+
return true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Get gem root path (not smart)
|
49
|
+
def root
|
50
|
+
File.expand_path '../../..', __FILE__
|
51
|
+
end
|
52
|
+
|
53
|
+
# Path where scripts are located
|
54
|
+
def path
|
55
|
+
File.join root, 'scripts'
|
56
|
+
end
|
57
|
+
|
58
|
+
# Full path of connection script
|
59
|
+
def connection_script
|
60
|
+
File.join path, 'connect.sh'
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'frisky/ssdp'
|
2
|
+
require 'httpclient'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module SonyCameraRemoteAPI
|
7
|
+
# Module providing SSDP function to discover camera (Device discovery)
|
8
|
+
module SSDP
|
9
|
+
|
10
|
+
# Cannot find camera
|
11
|
+
class DeviceNotFound < StandardError;
|
12
|
+
end
|
13
|
+
# Failed to Parse device description
|
14
|
+
class DeviceDescriptionInvalid < StandardError;
|
15
|
+
end
|
16
|
+
|
17
|
+
# The search target for Sony camera (fixed)
|
18
|
+
SSDP_SEARCH_TARGET = 'urn:schemas-sony-com:service:ScalarWebAPI:1'.freeze
|
19
|
+
# Retrying limit for SSDP search
|
20
|
+
SSDP_SEARCH_RETRY = 4
|
21
|
+
# Retrying interval for SSDP search
|
22
|
+
SSDP_RETRY_INTERVAL = 5
|
23
|
+
|
24
|
+
# Perform SSDP discover to find camera on network.
|
25
|
+
# @return [Hash] Endpoint URLs
|
26
|
+
def ssdp_search
|
27
|
+
log.info 'Trying SSDP discover...'
|
28
|
+
try = 1
|
29
|
+
while true do
|
30
|
+
response = Frisky::SSDP.search SSDP_SEARCH_TARGET
|
31
|
+
if response.present?
|
32
|
+
break
|
33
|
+
elsif try < SSDP_SEARCH_RETRY
|
34
|
+
try += 1
|
35
|
+
log.warn "SSDP discover failed, retrying... (#{try}/#{SSDP_SEARCH_RETRY})"
|
36
|
+
sleep(SSDP_RETRY_INTERVAL)
|
37
|
+
else
|
38
|
+
raise DeviceNotFound, 'Cannot find camera API server. Please confirm network connection is correct.'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
log.info 'SSDP discover succeeded.'
|
42
|
+
|
43
|
+
# get device description
|
44
|
+
dd = HTTPClient.new.get_content(response[0][:location])
|
45
|
+
# puts dd
|
46
|
+
parse_device_description(dd)
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
# Parse device description and get endpoint URLs
|
51
|
+
def parse_device_description(dd)
|
52
|
+
dd_xml = Nokogiri::XML(dd)
|
53
|
+
raise DeviceDescriptionInvalid if dd_xml.nil?
|
54
|
+
dd_xml.remove_namespaces!
|
55
|
+
camera_name = dd_xml.css('device friendlyName').inner_text
|
56
|
+
services = dd_xml.css('device X_ScalarWebAPI_Service')
|
57
|
+
endpoints = {}
|
58
|
+
services.each do |sv|
|
59
|
+
service_type = sv.css('X_ScalarWebAPI_ServiceType').inner_text
|
60
|
+
endpoints[service_type] = File.join(sv.css('X_ScalarWebAPI_ActionList_URL').inner_text, service_type)
|
61
|
+
end
|
62
|
+
# endpoints['liveview'] = dd_xml.css('device X_ScalarWebAPI_LiveView_URL').inner_text
|
63
|
+
# endpoints.delete_if { |k, v| v.blank? }
|
64
|
+
log.info "model-name: #{camera_name}"
|
65
|
+
log.debug 'endpoints:'
|
66
|
+
endpoints.each do |e|
|
67
|
+
log.debug " #{e}"
|
68
|
+
end
|
69
|
+
endpoints
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module SonyCameraRemoteAPI
|
2
|
+
# Module providing utility methods
|
3
|
+
module Utils
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# Get file name in the format of <prefix>_<num>.<ext>
|
8
|
+
# If there are files that matches this format, get the next number of it.
|
9
|
+
# @param [String] prefix
|
10
|
+
# @param [String] ext
|
11
|
+
# @param [Fixnum] start
|
12
|
+
# @param [Fixnum] num
|
13
|
+
# @param [String] dir
|
14
|
+
def generate_sequencial_filenames(prefix, ext, start: nil, num: nil, dir: nil)
|
15
|
+
if start
|
16
|
+
count = start
|
17
|
+
else
|
18
|
+
count = get_next_file_number prefix, ext, dir: dir
|
19
|
+
end
|
20
|
+
gen = Enumerator.new do |y|
|
21
|
+
loop do
|
22
|
+
y << "#{prefix}_#{count}.#{ext}"
|
23
|
+
count += 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
if num
|
27
|
+
return (0..(num-1)).map { gen.next }
|
28
|
+
else
|
29
|
+
return gen
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
# Get the next file number by searching files with format '<prefix>_\d+.<ext>' in <dir>.
|
35
|
+
# @param [String] prefix
|
36
|
+
# @param [String] ext
|
37
|
+
# @param [String] dir
|
38
|
+
def get_next_file_number(prefix, ext, dir: nil)
|
39
|
+
numbers = []
|
40
|
+
Dir["#{dir}/*"].map do |f|
|
41
|
+
begin
|
42
|
+
num = f[/#{dir}\/#{prefix}_(\d+).#{ext}/, 1]
|
43
|
+
numbers << num.to_i if num.present?
|
44
|
+
rescue
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
if numbers.empty?
|
49
|
+
0
|
50
|
+
else
|
51
|
+
numbers.sort[-1] + 1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# Search pattern in candidates.
|
57
|
+
# @param [String] pattern Pattern
|
58
|
+
# @param [Array<String>] candidates Candidates
|
59
|
+
# @return [Array<String, Fixnum>] matched candidate and the number of matched candidates.
|
60
|
+
def partial_and_unique_match(pattern, candidates)
|
61
|
+
result = candidates.find { |c| c == pattern }
|
62
|
+
return result, 1 if result
|
63
|
+
result = candidates.select { |c| c =~ /#{pattern}/i }
|
64
|
+
return result[0], 1 if result.size == 1
|
65
|
+
result = candidates.select { |c| c =~ /#{pattern}/ }
|
66
|
+
return result[0], 1 if result.size == 1
|
67
|
+
return nil, result.size
|
68
|
+
end
|
69
|
+
|
70
|
+
# Print array.
|
71
|
+
# @param [Array] array
|
72
|
+
# @param [Fixnum] horizon
|
73
|
+
# @param [Fixnum] space
|
74
|
+
# @param [Fixnum] threshold
|
75
|
+
def print_array_in_columns(array, horizon, space, threshold)
|
76
|
+
if array.size >= threshold
|
77
|
+
longest = array.map { |s| s.size }.max
|
78
|
+
num_columns = (horizon + space ) / (longest + space)
|
79
|
+
num_rows = array.size / num_columns + 1
|
80
|
+
array += [''] * ((num_columns * num_rows) - array.size)
|
81
|
+
array_2d = array.each_slice(num_rows).to_a
|
82
|
+
longests = array_2d.map { |column| column.map { |e| e.size }.max }
|
83
|
+
array_2d = array_2d.transpose
|
84
|
+
array_2d.each do |row|
|
85
|
+
row.zip(longests).each do |e, len|
|
86
|
+
print e + ' ' * (len - e.size) + ' ' * space
|
87
|
+
end
|
88
|
+
puts ''
|
89
|
+
end
|
90
|
+
else
|
91
|
+
array.each do |e|
|
92
|
+
puts e
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|