sony-camera-remote 0.0.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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +5 -0
  4. data/CONTRIBUTING.markdown +7 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +12 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.markdown +52 -0
  9. data/Rakefile +9 -0
  10. data/TODO.markdown +73 -0
  11. data/bin/search-nex.py +25 -0
  12. data/bin/sonycam +48 -0
  13. data/lib/sony_camera_remote.rb +5 -0
  14. data/lib/sony_camera_remote/camera.rb +61 -0
  15. data/lib/sony_camera_remote/device.rb +19 -0
  16. data/lib/sony_camera_remote/discovery/client.rb +90 -0
  17. data/lib/sony_camera_remote/discovery/device_info.rb +78 -0
  18. data/lib/sony_camera_remote/discovery/m_search.erb +5 -0
  19. data/lib/sony_camera_remote/discovery/request.rb +20 -0
  20. data/lib/sony_camera_remote/discovery/response.rb +17 -0
  21. data/lib/sony_camera_remote/service.rb +24 -0
  22. data/lib/sony_camera_remote/version.rb +3 -0
  23. data/sony-camera-remote.gemspec +33 -0
  24. data/test/fixtures/DmsRmtDesc.xml +95 -0
  25. data/test/fixtures/discovery_request.txt +6 -0
  26. data/test/fixtures/discovery_response.txt +9 -0
  27. data/test/fixtures/vcr/CameraTest_setup.yml +26 -0
  28. data/test/fixtures/vcr/CameraTest_test_application_info.yml +26 -0
  29. data/test/fixtures/vcr/CameraTest_test_available_methods.yml +26 -0
  30. data/test/fixtures/vcr/CameraTest_test_shoot.yml +26 -0
  31. data/test/fixtures/vcr/CameraTest_test_shootmode_interval_still.yml +26 -0
  32. data/test/fixtures/vcr/CameraTest_test_shootmode_movie.yml +26 -0
  33. data/test/fixtures/vcr/CameraTest_test_shootmode_still.yml +26 -0
  34. data/test/fixtures/vcr/CameraTest_test_supported_shootmodes.yml +26 -0
  35. data/test/fixtures/vcr/DeviceInfoTest_setup.yml +43 -0
  36. data/test/helper.rb +26 -0
  37. data/test/unit/test_camera.rb +41 -0
  38. data/test/unit/test_device_info.rb +71 -0
  39. data/test/unit/test_request.rb +11 -0
  40. data/test/unit/test_response.rb +11 -0
  41. metadata +257 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: aeb620a8b307adabd51dd98244f0b151c6145414
4
+ data.tar.gz: 4667ae3c570441c343c3390cb234e0917564e7bc
5
+ SHA512:
6
+ metadata.gz: 5ee13de0fdac63aa8cf5d32abb75c2f008722d9bda2c91200b8f0f6a5c3113d48a7e24123d3e5fd99767da370e0e18c757d8f32a18eb340fcfca9a0b1bf864be
7
+ data.tar.gz: 987a756c1c49a3caaa6244d8600ed5773b8f90535d14aa5f14975da4e169463f257e6ef93f7d07736c6cb2227c7756c01dc9808fecd051bdf818d7b4a70f957a
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
@@ -0,0 +1,7 @@
1
+ # Contributing
2
+
3
+ 1. Fork it
4
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
5
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
6
+ 4. Push to the branch (`git push origin my-new-feature`)
7
+ 5. Create new Pull Request
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sony-camera-remote.gemspec
4
+ gemspec
@@ -0,0 +1,12 @@
1
+ guard 'bundler' do
2
+ watch('Gemfile')
3
+ watch(/^.+\.gemspec/)
4
+ end
5
+
6
+ guard 'minitest' do
7
+ watch(%r|^test/unit/test_(.*)\.rb|){|m| "test/unit/test_#{m[1]}.rb"}
8
+ watch(%r|^lib/*\.rb|){'test/unit'}
9
+ watch(%r|^lib/.*/*\.rb|){'test/unit'}
10
+ watch(%r{^lib/.*/([^/]+)\.rb$}){|m| "test/unit/test_#{m[1]}.rb"}
11
+ watch(%r|^test/helper\.rb|){'test/unit'}
12
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Nicholas E. Rabenau
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,52 @@
1
+ # SonyCameraRemote
2
+
3
+ [![Build Status](https://secure.travis-ci.org/nerab/sony-camera-remote.png?branch=master)](http://travis-ci.org/nerab/sony-camera-remote)
4
+
5
+ Provides a Ruby wrapper around the API for cameras that support the Sony [Camera Remote API](http://developer.sony.com/develop/cameras/).
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'sony-camera-remote'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install sony-camera-remote
20
+
21
+ ## Usage
22
+
23
+ require 'sony_camera_remote'
24
+
25
+ # Discover cameras and their services using UPnP
26
+ Discovery::Client.new.discover
27
+ # => ['http://10.0.0.1:10000/sony/camera']
28
+
29
+ cam = Camera.new('http://10.0.0.1:10000/sony/camera')
30
+ img_uri = cam.shoot
31
+ # => http://10.0.0.1:60152/pict140119_1923230000.JPG?%211234%21http%2dget%3a%2a%3aimage%2fjpeg%3a%2a%21%21%21%21%21
32
+
33
+ ## Command-line client
34
+
35
+ # Assuming that the camera is in photo mode
36
+ $ sonycam shoot
37
+ http://10.0.0.1:60152/pict140119_1923230000.JPG?%211234%21http%2dget%3a%2a%3aimage%2fjpeg%3a%2a%21%21%21%21%21
38
+
39
+ # Shoot and open browser with the returned image URL
40
+ $ open $(sonycam) # Mac
41
+ $ xdg-open $(sonycam) # GNOME
42
+
43
+ ## Manually testing the Camera Remote API
44
+
45
+ # Discover devices using UPnP
46
+ bin/search-nex.py
47
+
48
+ # Fetch the endpoint URLs manually
49
+ curl http://10.0.0.1:64321/DmsRmtDesc.xml | xmllint --format -
50
+
51
+ # take a picture manually
52
+ curl -v -X POST -H "Content-Type: application/json" -d '{"method":"actTakePicture", "params":[], "id":1, "version":"1.0"}' http://10.0.0.1:10000/sony/camera
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |test|
5
+ test.libs << 'lib' << 'test' << 'test/unit'
6
+ test.pattern = 'test/unit/test_*.rb'
7
+ end
8
+
9
+ task :default => :test
@@ -0,0 +1,73 @@
1
+ # Test private APIs from http://chdk.setepontos.com/index.php?topic=10736.0
2
+
3
+ camera/setFlashMode
4
+ camera/getFlashMode
5
+ camera/getSupportedFlashMode
6
+ camera/getAvailableFlashMode
7
+ camera/setExposureCompensation
8
+ camera/getExposureCompensation
9
+ camera/getSupportedExposureCompensation
10
+ camera/getAvailableExposureCompensation
11
+ camera/setSteadyMode
12
+ camera/getSteadyMode
13
+ camera/getSupportedSteadyMode
14
+ camera/getAvailableSteadyMode
15
+ camera/setViewAngle
16
+ camera/getViewAngle
17
+ camera/getSupportedViewAngle
18
+ camera/getAvailableViewAngle
19
+ camera/setMovieQuality
20
+ camera/getMovieQuality
21
+ camera/getSupportedMovieQuality
22
+ camera/getAvailableMovieQuality
23
+ camera/setFocusMode
24
+ camera/getFocusMode
25
+ camera/getSupportedFocusMode
26
+ camera/getAvailableFocusMode
27
+ camera/setStillSize
28
+ camera/getStillSize
29
+ camera/getSupportedStillSize
30
+ camera/getAvailableStillSize
31
+ camera/setBeepMode
32
+ camera/getBeepMode
33
+ camera/getSupportedBeepMode
34
+ camera/getAvailableBeepMode
35
+ camera/setCameraFunction
36
+ camera/getCameraFunction
37
+ camera/getSupportedCameraFunction
38
+ camera/getAvailableCameraFunction
39
+ camera/setLiveviewSize
40
+ camera/getLiveviewSize
41
+ camera/getSupportedLiveviewSize
42
+ camera/getAvailableLiveviewSize
43
+ camera/setTouchAFPosition
44
+ camera/getTouchAFPosition
45
+ camera/cancelTouchAFPosition
46
+ camera/setFNumber
47
+ camera/getFNumber
48
+ camera/getSupportedFNumber
49
+ camera/getAvailableFNumber
50
+ camera/setShutterSpeed
51
+ camera/getShutterSpeed
52
+ camera/getSupportedShutterSpeed
53
+ camera/getAvailableShutterSpeed
54
+ camera/setIsoSpeedRate
55
+ camera/getIsoSpeedRate
56
+ camera/getSupportedIsoSpeedRate
57
+ camera/getAvailableIsoSpeedRate
58
+ camera/setExposureMode
59
+ camera/getExposureMode
60
+ camera/getSupportedExposureMode
61
+ camera/getAvailableExposureMode
62
+ camera/setWhiteBalance
63
+ camera/getWhiteBalance
64
+ camera/getSupportedWhiteBalance
65
+ camera/getAvailableWhiteBalance
66
+ camera/setProgramShift
67
+ camera/getSupportedProgramShift
68
+ camera/getStorageInformation
69
+ camera/startLiveviewWithSize
70
+ camera/startIntervalStillRec
71
+ camera/stopIntervalStillRec
72
+ camera/actFormatStorage
73
+ system/setCurrentTime
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/python
2
+
3
+ # from https://github.com/crquan/work-on-sony-apis/blob/master/search-nex.py
4
+ # Added bind() in order to specify which local address (interface) to use
5
+
6
+ import sys
7
+ import socket
8
+ import time
9
+
10
+ SSDP_ADDR = "239.255.255.250";
11
+ SSDP_PORT = 1900;
12
+ SSDP_MX = 1;
13
+ SSDP_ST = "urn:schemas-sony-com:service:ScalarWebAPI:1";
14
+
15
+ ssdpRequest = "M-SEARCH * HTTP/1.1\r\n" + \
16
+ "HOST: %s:%d\r\n" % (SSDP_ADDR, SSDP_PORT) + \
17
+ "MAN: \"ssdp:discover\"\r\n" + \
18
+ "MX: %d\r\n" % (SSDP_MX, ) + \
19
+ "ST: %s\r\n" % (SSDP_ST, ) + "\r\n";
20
+
21
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
22
+ sock.bind(("10.0.1.1", 0))
23
+
24
+ sock.sendto(ssdpRequest, (SSDP_ADDR, SSDP_PORT))
25
+ print sock.recv(1000)
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ require 'sony_camera_remote'
5
+ require 'yaml'
6
+ include SonyCameraRemote
7
+
8
+ config_file = File.join(ENV['HOME'], '.' << File.basename(__FILE__))
9
+
10
+ if File.exist?(config_file)
11
+ config = YAML::load(File.read(config_file))
12
+ else
13
+ config = {:verbose => true}
14
+ end
15
+
16
+ verbose = config[:verbose]
17
+ method = ARGV[0] || 'shoot'
18
+
19
+ begin
20
+ camera_uris = config[:uris]
21
+
22
+ if camera_uris.nil? || camera_uris.empty?
23
+ # Discover and save endpoints
24
+ camera_uris = Discovery::Client.new.discover.map do |device|
25
+ if camera_service = device.services['camera']
26
+ STDERR.puts "#{device.name} has camera service at #{camera_service.base_uri}." if verbose
27
+ camera_service.base_uri.to_s
28
+ else
29
+ STDERR.puts "Error: #{device.name} has no camera service."
30
+ end
31
+ end.compact
32
+
33
+ config[:uris] = camera_uris
34
+ File.open(config_file, 'w'){|f| YAML.dump(config, f)}
35
+ end
36
+
37
+ camera_uris.each do |uri|
38
+ puts Camera.new(uri).send(method)
39
+ end
40
+ rescue
41
+ STDERR.puts("Error: #{$!}")
42
+
43
+ if verbose
44
+ $!.backtrace.each do |b|
45
+ STDERR.puts(b)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ require 'require_all'
2
+ require_rel 'sony_camera_remote'
3
+
4
+ module SonyCameraRemote
5
+ end
@@ -0,0 +1,61 @@
1
+ require 'httparty'
2
+ require 'json'
3
+
4
+ module SonyCameraRemote
5
+ class Camera
6
+ API_VERSION = '2.0.0'
7
+
8
+ include HTTParty
9
+
10
+ DEFAULT_OPTIONS = {:version => '1.0', :params => [], :id => 1}
11
+
12
+ class ApplicationInfo < Struct.new(:name, :api_version)
13
+ def to_s
14
+ "#{name} v#{api_version}"
15
+ end
16
+ end
17
+
18
+ class IncompatibleAPIVersion < StandardError
19
+ attr_reader :supported, :actual
20
+
21
+ def initialize(supported, actual)
22
+ super("The camera provides API version #{actual}, but this library only supports #{supported}.")
23
+ @supported, @actual = supported, actual
24
+ end
25
+ end
26
+
27
+ def initialize(uri)
28
+ self.class.base_uri(URI(uri).to_s)
29
+
30
+ ai = application_info
31
+ raise IncompatibleAPIVersion(API_VERSION, ai.api_version) if API_VERSION != ai.api_version
32
+ end
33
+
34
+ def shootmode
35
+ options = {:body => DEFAULT_OPTIONS.merge({:method => 'getShootMode'}).to_json}
36
+ self.class.post('', options)['result'].first
37
+ end
38
+
39
+ def supported_shootmodes
40
+ options = {:body => DEFAULT_OPTIONS.merge({:method => 'getSupportedShootMode'}).to_json}
41
+ self.class.post('', options)['result'].first
42
+ end
43
+
44
+ def application_info
45
+ options = {:body => DEFAULT_OPTIONS.merge({:method => 'getApplicationInfo'}).to_json}
46
+ result = self.class.post('', options)['result']
47
+ ApplicationInfo.new(result.first, result.last)
48
+ end
49
+
50
+ def available_methods
51
+ options = {:body => DEFAULT_OPTIONS.merge({:method => 'getAvailableApiList'}).to_json}
52
+ self.class.post('', options)['result'].first
53
+ end
54
+
55
+ # blocking
56
+ def shoot
57
+ options = {:body => DEFAULT_OPTIONS.merge({:method => 'actTakePicture'}).to_json}
58
+ self.class.post('', options)['result'].first
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,19 @@
1
+ module SonyCameraRemote
2
+ class Model < Struct.new(:name, :description, :url)
3
+ end
4
+
5
+ class Manufacturer < Struct.new(:name, :url)
6
+ end
7
+
8
+ class Device < Struct.new(:name, :manufacturer, :model)
9
+ attr_reader :services
10
+
11
+ def initialize
12
+ @services = {}
13
+ end
14
+ end
15
+
16
+ class ScalarWebAPIDevice < Device
17
+ attr_accessor :version, :imaging_device
18
+ end
19
+ end
@@ -0,0 +1,90 @@
1
+ require 'socket'
2
+ require 'system/getifaddrs'
3
+ require 'timeout'
4
+
5
+ module SonyCameraRemote
6
+ module Discovery
7
+ class Client
8
+ include Socket::Constants
9
+
10
+ class UnknownInterface < StandardError
11
+ def initialize(offending, available)
12
+ super("The interface '#{offending}' is not known. Possible values are #{available.join(', ')}.")
13
+ end
14
+ end
15
+
16
+ SSDP_ADDR = {
17
+ nil => '239.255.255.250',
18
+ :ipv6_link_local => 'FF02::C',
19
+ :ipv6_subnet => 'FF03::C',
20
+ :ipv6_administrative => 'FF04::C',
21
+ :ipv6_site_local => 'FF05::C',
22
+ :ipv6_global => 'FF0E::C',
23
+ }
24
+
25
+ SSDP_PORT = 1900
26
+
27
+ # TODO Move the enumeration and address lookup to its own class. This class does too much.
28
+ def initialize(local_addrs = nil, timeout = 10, scope = nil)
29
+ if local_addrs.nil?
30
+ # Use all of the system's IP addresses
31
+ @local_addrs = ip_addresses
32
+ elsif local_addrs !~ /\b(?:\d{1,3}\.){3}\d{1,3}\b/ # http://www.regular-expressions.info/examples.html
33
+ # Not exactly an IP address, so we treat it as interface name
34
+ # TODO Support not just one, but a list of interfaces
35
+ # TODO System.get_ifaddrs seems to return IPv4 addresses only
36
+ if_addr = System.get_ifaddrs.fetch(local_addrs.to_sym) do |ip|
37
+ raise UnknownInterface.new(ip, System.get_ifaddrs.keys)
38
+ end[:inet_addr]
39
+
40
+ @local_addrs = addr_info(if_addr)
41
+ else
42
+ @local_addrs = addr_info(local_addrs)
43
+ end
44
+
45
+ @addr = SSDP_ADDR[@scope]
46
+ @timeout = timeout
47
+ end
48
+
49
+ def discover
50
+ Array(@local_addrs).map do |local_addr|
51
+ begin
52
+ DeviceInfo.fetch(inquire(local_addr).location)
53
+ rescue Errno::EADDRNOTAVAIL
54
+ # TODO Notify observers instead of writing to STDOUT
55
+ STDOUT.puts("Warning: Could not bind to #{local_addr.ip_address}.")
56
+ rescue Errno::EINVAL
57
+ STDOUT.puts("Warning: Address #{local_addr.ip_address} not supported.")
58
+ rescue Timeout::Error
59
+ STDOUT.puts("Warning: Timeout binding to #{local_addr.ip_address}.")
60
+ end
61
+ end.compact
62
+ end
63
+
64
+ private
65
+
66
+ def inquire(local_addr)
67
+ if local_addr.ipv4?
68
+ socket = UDPSocket.new
69
+ else
70
+ socket = UDPSocket.new(Socket::AF_INET6)
71
+ end
72
+
73
+ socket.bind(local_addr.ip_address, 0)
74
+ socket.send(Request.new(@addr, SSDP_PORT, @timeout).to_s, 0, @addr, SSDP_PORT)
75
+
76
+ Timeout::timeout(@timeout) do
77
+ Response.new(socket.recv(1000))
78
+ end
79
+ end
80
+
81
+ def ip_addresses
82
+ Socket.ip_address_list.reject{|a| a.ipv4_loopback? || a.ipv6_loopback? || a.ipv6_linklocal?}
83
+ end
84
+
85
+ def addr_info(if_addr)
86
+ Addrinfo.new(Socket.sockaddr_in(SSDP_PORT, if_addr))
87
+ end
88
+ end
89
+ end
90
+ end