├── .rspec ├── lib ├── gphoto2 │ ├── version.rb │ ├── camera_event.rb │ ├── camera_widgets │ │ ├── window_camera_widget.rb │ │ ├── menu_camera_widget.rb │ │ ├── section_camera_widget.rb │ │ ├── date_camera_widget.rb │ │ ├── toggle_camera_widget.rb │ │ ├── text_camera_widget.rb │ │ ├── range_camera_widget.rb │ │ ├── radio_camera_widget.rb │ │ └── camera_widget.rb │ ├── struct.rb │ ├── port_result.rb │ ├── camera_file_path.rb │ ├── context.rb │ ├── camera_list.rb │ ├── camera_file_info │ │ ├── file_camera_file_info.rb │ │ └── camera_file_info.rb │ ├── entry.rb │ ├── port_info_list.rb │ ├── port.rb │ ├── camera │ │ ├── filesystem.rb │ │ ├── event.rb │ │ ├── capture.rb │ │ └── configuration.rb │ ├── camera_abilities.rb │ ├── camera_abilities_list.rb │ ├── port_info.rb │ ├── camera_folder.rb │ ├── camera_file.rb │ └── camera.rb ├── ffi │ ├── gphoto2_port │ │ ├── gp_port_serial_parity.rb │ │ ├── gp_port_settings_usb_scsi.rb │ │ ├── gp_port_settings_usb_disk_direct.rb │ │ ├── gp_port_info.rb │ │ ├── gp_port_settings_serial.rb │ │ ├── gp_port_settings.rb │ │ ├── gp_port_info_list.rb │ │ ├── gp_port_type.rb │ │ ├── gp_port_settings_usb.rb │ │ ├── gp_port.rb │ │ └── gp_port_result.rb │ ├── gphoto2 │ │ ├── camera_file_status.rb │ │ ├── entry.rb │ │ ├── gphoto_device_type.rb │ │ ├── camera_capture_type.rb │ │ ├── camera_file_access_type.rb │ │ ├── camera_file_path.rb │ │ ├── camera_driver_status.rb │ │ ├── camera_file_info.rb │ │ ├── camera_file_permissions.rb │ │ ├── camera_event_type.rb │ │ ├── camera_file_info_audio.rb │ │ ├── camera_file_type.rb │ │ ├── camera_list.rb │ │ ├── camera_folder_operation.rb │ │ ├── camera.rb │ │ ├── camera_file_info_preview.rb │ │ ├── camera_file_operation.rb │ │ ├── camera_abilities_list.rb │ │ ├── camera_widget_type.rb │ │ ├── camera_operation.rb │ │ ├── camera_file_info_file.rb │ │ ├── camera_file_info_fields.rb │ │ ├── camera_file.rb │ │ ├── camera_widget.rb │ │ ├── camera_abilities.rb │ │ └── gp_context.rb │ ├── gphoto2_port.rb │ └── gphoto2.rb └── gphoto2.rb ├── Gemfile ├── Rakefile ├── examples ├── capture.rb ├── intervalometer.rb ├── live_view.rb ├── autofocus.rb ├── record_movie.rb ├── continuous_burst.rb ├── list_config.rb └── list_files.rb ├── .gitignore ├── spec ├── gphoto2 │ ├── context_spec.rb │ ├── camera_widgets │ │ ├── date_camera_widget_spec.rb │ │ ├── text_camera_widget_spec.rb │ │ ├── toggle_camera_widget_spec.rb │ │ ├── range_camera_widget_spec.rb │ │ └── radio_camera_widget_spec.rb │ ├── port_spec.rb │ ├── entry_spec.rb │ ├── camera_file_path_spec.rb │ ├── camera_list_spec.rb │ ├── port_info_list_spec.rb │ ├── camera_abilities_list_spec.rb │ ├── camera_file_info │ │ └── file_camera_file_info_spec.rb │ ├── port_info_spec.rb │ ├── camera_abilities_spec.rb │ ├── camera_file_spec.rb │ ├── camera_folder_spec.rb │ └── camera_spec.rb ├── spec_helper.rb ├── gphoto2_spec.rb └── support │ └── shared_examples │ ├── camera_file_info_examples.rb │ ├── camera │ ├── event_examples.rb │ ├── filesystem_examples.rb │ ├── capture_examples.rb │ └── configuration_examples.rb │ └── camera_widget_examples.rb ├── .github └── workflows │ └── ci.yml ├── LICENSE.txt ├── ffi-gphoto2.gemspec ├── CHANGELOG.md └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /lib/gphoto2/version.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | VERSION = '0.10.0' 3 | end 4 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_event.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | CameraEvent = ::Struct.new(:type, :data) 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in ffi-gphoto2.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_widgets/window_camera_widget.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class WindowCameraWidget < CameraWidget 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_widgets/menu_camera_widget.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class MenuCameraWidget < RadioCameraWidget 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_widgets/section_camera_widget.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class SectionCameraWidget < CameraWidget 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | require 'yard' 4 | 5 | RSpec::Core::RakeTask.new('spec') 6 | YARD::Rake::YardocTask.new 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2_port/gp_port_serial_parity.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2Port 3 | # libgphoto2_port/libgphoto2_port/gphoto2-port.h 4 | GPPortSerialParity = enum :off, :even, :odd 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /examples/capture.rb: -------------------------------------------------------------------------------- 1 | require 'gphoto2' 2 | 3 | # Captures a single photo and saves it to the current working directory. 4 | 5 | GPhoto2::Camera.first do |camera| 6 | file = camera.capture 7 | file.save 8 | end 9 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_file_status.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-filesys.h 4 | CameraFileStatus = enum :not_downloaded, 5 | :downloaded 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/gphoto2/struct.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | module Struct 3 | # @return [FFI::Struct] 4 | attr_reader :ptr 5 | 6 | # @return [FFI::Struct] 7 | def to_ptr 8 | ptr 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/entry.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | class Entry < FFI::Struct 4 | # libgphoto2/gphoto2-list.c 5 | layout :name, :string, 6 | :value, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/gphoto_device_type.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-abilities-list.h 4 | GphotoDeviceType = enum :still_camera, 0, 5 | :audio_player, 1 << 0 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 18 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_capture_type.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-camera.h 4 | CameraCaptureType = enum :image, 5 | :movie, 6 | :sound 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/gphoto2/port_result.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class PortResult 3 | # @param [Integer] rc 4 | # @return [String] 5 | def self.as_string(rc) 6 | FFI::GPhoto2Port.gp_port_result_as_string(rc) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_file_access_type.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-file.h 4 | CameraFileAccessType = enum :memory, 5 | :fd, 6 | :handler 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2_port/gp_port_settings_usb_scsi.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2Port 3 | class GPPortSettingsUsbScsi < FFI::Struct 4 | # libgphoto2_port/libgphoto2_port/gphoto2-port.h 5 | layout :path, [:char, 128] 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_file_path.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | class CameraFilePath < FFI::Struct 4 | # gphoto2/gphoto2-camera.h 5 | layout :name, [:char, 128], 6 | :folder, [:char, 1024] 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2_port/gp_port_settings_usb_disk_direct.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2Port 3 | class GPPortSettingsUsbDiskDirect < FFI::Struct 4 | # libgphoto2_port/libgphoto2_port/gphoto2-port.h 5 | layout :path, [:char, 128] 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_driver_status.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-abilities-list.h 4 | CameraDriverStatus = enum :production, 5 | :testing, 6 | :experimental, 7 | :deprecated 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_file_info.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | class CameraFileInfo < FFI::Struct 4 | # libgphoto2/gphoto2-filesys.h 5 | layout :preview, CameraFileInfoPreview, 6 | :file, CameraFileInfoFile, 7 | :audio, CameraFileInfoAudio 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_file_permissions.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-filesys.h 4 | CameraFilePermissions = enum :none, 0, 5 | :read, 1 << 0, 6 | :delete, 1 << 1, 7 | :all, 0xff 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_event_type.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-camera.h 4 | CameraEventType = enum :unknown, 5 | :timeout, 6 | :file_added, 7 | :folder_added, 8 | :capture_complete 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_file_info_audio.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | class CameraFileInfoAudio < FFI::Struct 4 | # gphoto2/gphoto2-filesys.h 5 | layout :fields, CameraFileInfoFields, 6 | :status, CameraFileStatus, 7 | :size, :uint64, 8 | :type, [:char, 64] 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_file_type.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-file.h 4 | CameraFileType = enum :preview, 5 | :normal, 6 | :raw, 7 | :audio, 8 | :exif, 9 | :metadata 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2_port/gp_port_info.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2Port 3 | class GPPortInfo < FFI::Struct 4 | # libgphoto2_port/libgphoto2_port/gphoto2-port-info.h 5 | layout :type, GPPortType, 6 | :name, :string, 7 | :path, :string, 8 | :library_filename, :string 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /examples/intervalometer.rb: -------------------------------------------------------------------------------- 1 | require 'gphoto2' 2 | 3 | # Take a photo every 10 seconds for 2 hours. 4 | 5 | interval = 10 # seconds 6 | two_hours = 60 * 60 * 2 # (2 hours) 7 | stop_time = Time.now + two_hours 8 | 9 | GPhoto2::Camera.first do |camera| 10 | until Time.now >= stop_time 11 | camera.capture.save 12 | sleep interval 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2_port/gp_port_settings_serial.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2Port 3 | class GPPortSettingsSerial < FFI::Struct 4 | # libgphoto2_port/libgphoto2_port/gphoto2-port.h 5 | layout :port, [:char, 128], 6 | :speed, :int, 7 | :bits, :int, 8 | :parity, GPPortSerialParity, 9 | :stopbits, :int 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_list.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | class CameraList < FFI::ManagedStruct 4 | # libgphoto2/gphoto2-list.c 5 | layout :used, :int, 6 | :max, :int, 7 | :_entry, Entry.by_ref, 8 | :ref_count, :int 9 | 10 | def self.release(ptr) 11 | FFI::GPhoto2.gp_list_free(ptr) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_folder_operation.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-abilities-list.h 4 | CameraFolderOperation = enum :none, 0, 5 | :delete_all, 1 << 0, 6 | :put_file, 1 << 1, 7 | :make_dir, 1 << 2, 8 | :remove_dir, 1 << 3 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2_port/gp_port_settings.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2Port 3 | class GPPortSettings < FFI::Struct 4 | # libgphoto2_port/libgphoto2_port/gphoto2-port.h 5 | layout :serial, GPPortSettingsSerial, 6 | :usb, GPPortSettingsUSB, 7 | :usbdiskdirect, GPPortSettingsUsbDiskDirect, 8 | :usbscsi, GPPortSettingsUsbScsi 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /examples/live_view.rb: -------------------------------------------------------------------------------- 1 | require 'gphoto2' 2 | 3 | # Run and pipe to a video player that can demux raw mjpeg. For example, 4 | # 5 | # ruby live_view.rb | mpv --demuxer-lavf-format=mjpeg - 6 | 7 | # Automatically flush the IO buffer. 8 | STDOUT.sync = true 9 | 10 | GPhoto2::Camera.first do |camera| 11 | loop do 12 | # Write the preview image to stdout. 13 | print camera.preview.data 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | class Camera < FFI::Struct 4 | # gphoto2/gphoto2-camera.h 5 | layout :port, :pointer, # GPPort 6 | :fs, :pointer, # CameraFilesystem 7 | :functions, :pointer, # CameraFunctions 8 | :pl, :pointer, # CameraPrivateLibrary 9 | :pc, :pointer # CameraPrivateCore 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_file_path.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class CameraFilePath 3 | include GPhoto2::Struct 4 | 5 | def initialize(ptr = nil) 6 | @ptr = ptr || FFI::GPhoto2::CameraFilePath.new 7 | end 8 | 9 | # @return [String] 10 | def name 11 | ptr[:name].to_s 12 | end 13 | 14 | # @return [String] 15 | def folder 16 | ptr[:folder].to_s 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_file_info_preview.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | class CameraFileInfoPreview < FFI::Struct 4 | # gphoto2/gphoto2-filesys.h 5 | layout :fields, CameraFileInfoFields, 6 | :status, CameraFileStatus, 7 | :size, :uint64, 8 | :type, [:char, 64], 9 | :width, :uint32, 10 | :height, :uint32 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_file_operation.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-abilities-list.h 4 | CameraFileOperation = enum :none, 0, 5 | :delete, 1 << 1, 6 | :preview, 1 << 3, 7 | :raw, 1 << 4, 8 | :audio, 1 << 5, 9 | :exif, 1 << 6 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/gphoto2/context_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe Context do 5 | before do 6 | allow_any_instance_of(Context).to receive(:new) 7 | end 8 | 9 | describe '#finalize' do 10 | it 'decrements the reference counter' do 11 | context = Context.new 12 | expect(context).to receive(:unref) 13 | context.finalize 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_abilities_list.rb: -------------------------------------------------------------------------------- 1 | require 'ffi/gphoto2/camera_abilities' 2 | 3 | module FFI 4 | module GPhoto2 5 | class CameraAbilitiesList < FFI::ManagedStruct 6 | # libgphoto2/gphoto2-abilities-list.c 7 | layout :count, :int, 8 | :abilities, CameraAbilities.by_ref 9 | 10 | def self.release(ptr) 11 | FFI::GPhoto2.gp_abilities_list_free(ptr) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2_port/gp_port_info_list.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2Port 3 | class GPPortInfoList < FFI::ManagedStruct 4 | # libgphoto2_port/libgphoto2_port/gphoto2-port-info-list.c 5 | layout :info, GPPortInfo.by_ref, 6 | :count, :uint, 7 | :iolib_count, :uint 8 | 9 | def self.release(ptr) 10 | FFI::GPhoto2Port.gp_port_info_list_free(ptr) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2_port/gp_port_type.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2Port 3 | # libgphoto2_port/gphoto2/gphoto2-port-info-list.h 4 | GPPortType = enum :none, 0, 5 | :serial, 1 << 0, 6 | :usb, 1 << 2, 7 | :disk, 1 << 3, 8 | :ptpip, 1 << 4, 9 | :usb_disk_direct, 1 << 5, 10 | :usb_scsi, 1 << 6 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_widgets/date_camera_widget_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe DateCameraWidget do 5 | it_behaves_like CameraWidget 6 | 7 | describe '#value' do 8 | it 'has a Time return value' do 9 | widget = DateCameraWidget.new(nil) 10 | allow(widget).to receive(:value).and_return(Time.now) 11 | expect(widget.value).to be_kind_of(Time) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_widgets/text_camera_widget_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe TextCameraWidget do 5 | it_behaves_like CameraWidget 6 | 7 | describe '#value' do 8 | it 'has a String return value' do 9 | widget = TextCameraWidget.new(nil) 10 | allow(widget).to receive(:value).and_return('text') 11 | expect(widget.value).to be_kind_of(String) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'gphoto2' 3 | 4 | __dir__ ||= File.dirname(__FILE__) 5 | Dir[File.join(__dir__, 'support/**/*.rb')].each { |f| require f } 6 | 7 | RSpec.configure do |config| 8 | config.expect_with :rspec do |expectations| 9 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 10 | end 11 | 12 | config.mock_with :rspec do |mocks| 13 | mocks.verify_partial_doubles = true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_widget_type.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-widget.h 4 | CameraWidgetType = enum :window, 5 | :section, 6 | :text, 7 | :range, 8 | :toggle, 9 | :radio, 10 | :menu, 11 | :button, 12 | :date 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2_port/gp_port_settings_usb.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2Port 3 | class GPPortSettingsUSB < FFI::Struct 4 | # libgphoto2_port/libgphoto2_port/gphoto2-port.h 5 | layout :inep, :int, 6 | :outep, :int, 7 | :intep, :int, 8 | :config, :int, 9 | :interface, :int, 10 | :altsetting, :int, 11 | :maxpacketsize, :int, 12 | :port, [:char, 64] 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_operation.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-abilities-list.h 4 | CameraOperation = enum :none, 0, 5 | :capture_image, 1 << 0, 6 | :capture_video, 1 << 1, 7 | :capture_audio, 1 << 2, 8 | :capture_preview, 1 << 3, 9 | :config, 1 << 4, 10 | :trigger_capture, 1 << 5 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_file_info_file.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | class CameraFileInfoFile < FFI::Struct 4 | # gphoto2/gphoto2-filesys.h 5 | layout :fields, CameraFileInfoFields, 6 | :status, CameraFileStatus, 7 | :size, :uint64, 8 | :type, [:char, 64], 9 | :width, :uint32, 10 | :height, :uint32, 11 | :permissions, CameraFilePermissions, 12 | :mtime, :time_t 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/gphoto2/context.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class Context 3 | include FFI::GPhoto2 4 | include GPhoto2::Struct 5 | 6 | def initialize 7 | new 8 | end 9 | 10 | # @return [void] 11 | def finalize 12 | unref 13 | end 14 | alias_method :close, :finalize 15 | 16 | private 17 | 18 | def new 19 | ctx = gp_context_new() 20 | @ptr = GPContext.new(ctx) 21 | end 22 | 23 | def unref 24 | gp_context_unref(ptr) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/gphoto2/port_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe Port do 5 | before do 6 | allow_any_instance_of(Port).to receive(:new) 7 | end 8 | 9 | describe '#info=' do 10 | let(:port) { Port.new } 11 | 12 | before do 13 | allow(port).to receive(:set_info) 14 | end 15 | 16 | it 'returns the input value' do 17 | info = double(:port_info) 18 | expect(port.info = info).to eq(info) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_widgets/date_camera_widget.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class DateCameraWidget < CameraWidget 3 | protected 4 | 5 | def get_value 6 | val = FFI::MemoryPointer.new(:int) 7 | rc = gp_widget_get_value(ptr, val) 8 | GPhoto2.check!(rc) 9 | Time.at(val.read_int).utc 10 | end 11 | 12 | def set_value(date) 13 | val = FFI::MemoryPointer.new(:int) 14 | val.write_int(date.to_i) 15 | rc = gp_widget_set_value(ptr, val) 16 | GPhoto2.check!(rc) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_widgets/toggle_camera_widget.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class ToggleCameraWidget < CameraWidget 3 | protected 4 | 5 | def get_value 6 | val = FFI::MemoryPointer.new(:int) 7 | rc = gp_widget_get_value(ptr, val) 8 | GPhoto2.check!(rc) 9 | (val.read_int == 1) 10 | end 11 | 12 | def set_value(toggle) 13 | val = FFI::MemoryPointer.new(:int) 14 | val.write_int(toggle ? 1 : 0) 15 | rc = gp_widget_set_value(ptr, val) 16 | GPhoto2.check!(rc) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2_port/gp_port.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2Port 3 | class GPPort < FFI::ManagedStruct 4 | # libgphoto2_port/libgphoto2_port/gphoto2-port.h 5 | layout :type, GPPortType, 6 | :settings, GPPortSettings, 7 | :settings_pending, GPPortSettings, 8 | :timeout, :int, 9 | :pl, :pointer, # GPPortPrivateLibrary * 10 | :pc, :pointer # GPPortPrivateCore * 11 | 12 | def self.release(ptr) 13 | FFI::GPhoto2.gp_port_free(ptr) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/gphoto2_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GPhoto2 do 4 | describe '.check!' do 5 | context 'the return code is not GP_OK' do 6 | it 'raises GPhoto2::Error with a message and error code' do 7 | code = -1 8 | message = "Unspecified error (#{code})" 9 | 10 | expect { GPhoto2.check!(code) }.to raise_error do |e| 11 | expect(e).to be_kind_of(GPhoto2::Error) 12 | expect(e.message).to eq(message) 13 | expect(e.code).to eq(code) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_file_info_fields.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | # gphoto2/gphoto2-filesys.h 4 | CameraFileInfoFields = enum :none, 0, 5 | :type, 1 << 0, 6 | :size, 1 << 2, 7 | :width, 1 << 3, 8 | :height, 1 << 4, 9 | :permissions, 1 << 5, 10 | :status, 1 << 6, 11 | :mtime, 1 << 7, 12 | :all, 0xff 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_widgets/text_camera_widget.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class TextCameraWidget < CameraWidget 3 | protected 4 | 5 | def get_value 6 | val_ptr = FFI::MemoryPointer.new(:pointer) 7 | 8 | rc = gp_widget_get_value(ptr, val_ptr) 9 | GPhoto2.check!(rc) 10 | 11 | val_ptr = val_ptr.read_pointer 12 | val_ptr.null? ? nil : val_ptr.read_string 13 | end 14 | 15 | def set_value(text) 16 | val = FFI::MemoryPointer.from_string(text.to_s) 17 | rc = gp_widget_set_value(ptr, val) 18 | GPhoto2.check!(rc) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: actions/checkout@v2 10 | - run: | 11 | sudo apt-get update 12 | sudo apt-get --yes install gcc make libgphoto2-6 libgphoto2-port12 13 | sudo ln -s /usr/lib/x86_64-linux-gnu/libgphoto2.so{.6,} 14 | sudo ln -s /usr/lib/x86_64-linux-gnu/libgphoto2_port.so{.12,} 15 | - uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: "3.1.2" 18 | - run: bundle install 19 | - run: bundle exec rake 20 | -------------------------------------------------------------------------------- /examples/autofocus.rb: -------------------------------------------------------------------------------- 1 | require 'gphoto2' 2 | 3 | # Capture an image only after successfully autofocusing. 4 | # 5 | # Typically, if the camera fails to autofocus, updating the `autofocusdrive` 6 | # key will throw an "Unspecified error (-1)". This catches the exception and 7 | # continues to autofocus until it is successful. 8 | 9 | GPhoto2::Camera.first do |camera| 10 | begin 11 | camera.update(autofocusdrive: true) 12 | rescue GPhoto2::Error 13 | puts "autofocus failed... retrying" 14 | camera.reload 15 | retry 16 | ensure 17 | camera.update(autofocusdrive: false) 18 | end 19 | 20 | camera.capture 21 | end 22 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_widgets/toggle_camera_widget_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe ToggleCameraWidget do 5 | it_behaves_like CameraWidget 6 | 7 | describe '#value' do 8 | it 'can have a TrueClass return value' do 9 | widget = ToggleCameraWidget.new(nil) 10 | allow(widget).to receive(:value).and_return(true) 11 | expect(widget.value).to be_kind_of(TrueClass) 12 | end 13 | 14 | it 'can have a FalseClass return value' do 15 | widget = ToggleCameraWidget.new(nil) 16 | allow(widget).to receive(:value).and_return(false) 17 | expect(widget.value).to be_kind_of(FalseClass) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/gphoto2/entry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe Entry do 5 | let(:entry) { Entry.new(double('camera_list'), 0) } 6 | 7 | let(:name) { 'model' } 8 | let(:value) { 'usb:250,006' } 9 | 10 | before do 11 | allow(entry).to receive(:get_name).and_return(name) 12 | allow(entry).to receive(:get_value).and_return(value) 13 | end 14 | 15 | describe '#name' do 16 | it 'returns the name of the entry' do 17 | expect(entry.name).to eq(name) 18 | end 19 | end 20 | 21 | describe '#value' do 22 | it 'returns the value of the entry' do 23 | expect(entry.value).to eq(value) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_file.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | class CameraFile < FFI::ManagedStruct 4 | MAX_PATH = 256 5 | 6 | # libgphoto2/gphoto2-file.c 7 | layout :mime_type, [:char, 64], 8 | :name, [:char, MAX_PATH], 9 | :ref_count, :int, 10 | :mtime, :time_t, 11 | 12 | :accesstype, CameraFileAccessType, 13 | 14 | :size, :ulong, 15 | :data, :pointer, # unsigned char* 16 | :offset, :ulong, 17 | 18 | :fd, :int, 19 | 20 | :handler, :pointer, # CameraFileHandler 21 | :private, :pointer # void* 22 | 23 | def self.release(ptr) 24 | FFI::GPhoto2.gp_file_free(ptr) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /examples/record_movie.rb: -------------------------------------------------------------------------------- 1 | require 'gphoto2' 2 | 3 | # The camera must have a `movie` config key for this to work. 4 | # 5 | # Unlike capturing photos, which typically save to internal memory, videos 6 | # save to the memory card. The next `file_added` event from the camera after 7 | # stopping the recording will contain data pointing to the video file. 8 | 9 | GPhoto2::Camera.first do |camera| 10 | # Start recording. 11 | camera.update(movie: true) 12 | 13 | # Record for ~10 seconds. 14 | sleep 10 15 | 16 | # Stop recording. 17 | camera.update(movie: false) 18 | 19 | # Block until the camera finishes with the file. 20 | event = camera.wait_for(:file_added) 21 | 22 | # The event data has a camera file that can be saved. 23 | file = event.data 24 | file.save 25 | end 26 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_list.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class CameraList 3 | include FFI::GPhoto2 4 | include GPhoto2::Struct 5 | 6 | def initialize 7 | new 8 | end 9 | 10 | # @return [Integer] 11 | def size 12 | count 13 | end 14 | alias_method :length, :size 15 | 16 | # @return [Array] 17 | def to_a 18 | size.times.map { |i| Entry.new(self, i) } 19 | end 20 | 21 | private 22 | 23 | def new 24 | ptr = FFI::MemoryPointer.new(:pointer) 25 | rc = gp_list_new(ptr) 26 | GPhoto2.check!(rc) 27 | @ptr = FFI::GPhoto2::CameraList.new(ptr.read_pointer) 28 | end 29 | 30 | def count 31 | rc = gp_list_count(ptr) 32 | GPhoto2.check!(rc) 33 | rc 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_file_path_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe CameraFilePath do 5 | let(:name) { 'capt0001.jpg' } 6 | let(:folder) { '/' } 7 | 8 | before do 9 | allow_any_instance_of(CameraFilePath).to receive(:name).and_return(name) 10 | allow_any_instance_of(CameraFilePath).to receive(:folder).and_return(folder) 11 | end 12 | 13 | describe '#name' do 14 | it 'returns the name of the file' do 15 | path = CameraFilePath.new 16 | expect(path.name).to eq(name) 17 | end 18 | end 19 | 20 | describe '#folder' do 21 | it 'returns the folder that contains the file' do 22 | path = CameraFilePath.new 23 | expect(path.folder).to eq(folder) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /examples/continuous_burst.rb: -------------------------------------------------------------------------------- 1 | require 'gphoto2' 2 | 3 | # This example is limited to Nikon cameras with a continuous burst mode. 4 | 5 | # The number of frames to capture. 6 | N = 3 7 | 8 | GPhoto2::Camera.first do |camera| 9 | # Set the camera to continuous burst mode. 10 | camera.update(capturemode: 'Burst', burstnumber: N) 11 | 12 | # Use `#trigger` instead of `#capture` so the data remains in the camera 13 | # buffer. 14 | camera.trigger 15 | 16 | # Wait for the camera to process all the images. 17 | queue = N.times.map do 18 | event = camera.wait_for(:file_added) 19 | 20 | # The event data contains a `CameraFile` when the `:file_added` event is 21 | # triggered. 22 | event.data 23 | end 24 | 25 | # Save all the images to the current working directory. 26 | queue.each(&:save) 27 | end 28 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_widgets/range_camera_widget_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe RangeCameraWidget do 5 | it_behaves_like CameraWidget 6 | 7 | describe '#value' do 8 | it 'has a Float return value' do 9 | widget = RangeCameraWidget.new(nil) 10 | allow(widget).to receive(:value).and_return(0.0) 11 | expect(widget.value).to be_kind_of(Float) 12 | end 13 | end 14 | 15 | describe '#range' do 16 | it 'returns a list of valid range options' do 17 | min, max, inc = 0, 2, 0.5 18 | range = (min..max).step(inc).to_a 19 | 20 | widget = RangeCameraWidget.new(nil) 21 | allow(widget).to receive(:get_range).and_return([min, max, inc]) 22 | 23 | expect(widget.range).to eq(range) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_file_info/file_camera_file_info.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class FileCameraFileInfo < CameraFileInfo 3 | # @return [Integer, nil] 4 | def width 5 | fetch(:width) 6 | end 7 | 8 | # @return [Integer, nil] 9 | def height 10 | fetch(:height) 11 | end 12 | 13 | # @return [Boolean, nil] 14 | def readable? 15 | permissions = fetch(:permissions) 16 | (permissions & CameraFilePermissions[:read]) != 0 if permissions 17 | end 18 | 19 | # @return [Boolean, nil] 20 | def deletable? 21 | permissions = fetch(:permissions) 22 | (permissions & CameraFilePermissions[:delete]) != 0 if permissions 23 | end 24 | 25 | # @return [Time, nil] the last modification time 26 | def mtime 27 | Time.at(ptr[:mtime]) if has_field?(:mtime) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe CameraList do 5 | before do 6 | allow_any_instance_of(CameraList).to receive(:new) 7 | end 8 | 9 | describe '#size' do 10 | it 'returns the number of camera entries in the list' do 11 | size = 2 12 | 13 | list = CameraList.new 14 | allow(list).to receive(:count).and_return(size) 15 | 16 | expect(list.size).to eq(size) 17 | end 18 | end 19 | 20 | describe '#to_a' do 21 | it 'returns an array of camera entries' do 22 | size = 2 23 | 24 | list = CameraList.new 25 | allow(list).to receive(:size).and_return(size) 26 | 27 | ary = list.to_a 28 | 29 | expect(ary.size).to eq(size) 30 | 31 | ary.each { |e| expect(e).to be_kind_of(Entry) } 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /examples/list_config.rb: -------------------------------------------------------------------------------- 1 | require 'gphoto2' 2 | 3 | # List all configuration values in tree form. 4 | 5 | def visit(widget, level = 0) 6 | indent = ' ' * level 7 | 8 | puts "#{indent}#{widget.name}" 9 | 10 | if widget.type == :window || widget.type == :section 11 | widget.children.each { |child| visit(child, level + 1) } 12 | return 13 | end 14 | 15 | indent << ' ' 16 | 17 | puts "#{indent}label: #{widget.label}" 18 | puts "#{indent}type: #{widget.type}" 19 | puts "#{indent}value: #{widget.value}" 20 | 21 | case widget.type 22 | when :range 23 | range = widget.range 24 | step = (range.size > 1) ? range[1] - range[0] : 1.0 25 | puts "#{indent}options: #{range.first}..#{range.last}:step(#{step})" 26 | when :radio, :menu 27 | puts "#{indent}options: #{widget.choices.inspect}" 28 | end 29 | end 30 | 31 | GPhoto2::Camera.first do |camera| 32 | visit(camera.window) 33 | end 34 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2_port/gp_port_result.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2Port 3 | # libgphoto2_port/gphoto2/gphoto2-port-result.h 4 | GP_OK = 0 5 | 6 | GP_ERROR = -1 7 | GP_ERROR_BAD_PARAMETERS = -2 8 | GP_ERROR_NO_MEMORY = -3 9 | GP_ERROR_LIBRARY = -4 10 | GP_ERROR_UNKNOWN_PORT = -5 11 | GP_ERROR_NOT_SUPPORTED = -6 12 | GP_ERROR_IO = -7 13 | GP_ERROR_FIXED_LIMIT_EXCEEDED = -8 14 | 15 | GP_ERROR_TIMEOUT = -10 16 | 17 | GP_ERROR_IO_SUPPORTED_SERIAL = -20 18 | GP_ERROR_IO_SUPPORTED_USB = -21 19 | 20 | GP_ERROR_IO_INIT = -31 21 | GP_ERROR_IO_READ = -34 22 | GP_ERROR_IO_WRITE = -35 23 | GP_ERROR_IO_UPDATE = -37 24 | 25 | GP_ERROR_IO_SERIAL_SPEED = -41 26 | 27 | GP_ERROR_IO_USB_CLEAR_HALT = -51 28 | GP_ERROR_IO_USB_FIND = -52 29 | GP_ERROR_IO_USB_CLAIM = -53 30 | 31 | GP_ERROR_IO_LOCK = -60 32 | 33 | GP_ERROR_HAL = -70 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/gphoto2/entry.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class Entry 3 | include FFI::GPhoto2 4 | 5 | def initialize(camera_list, index) 6 | @camera_list = camera_list 7 | @index = index 8 | end 9 | 10 | # @return [String] 11 | def name 12 | get_name 13 | end 14 | 15 | # @return [String] 16 | def value 17 | get_value 18 | end 19 | 20 | private 21 | 22 | def get_name 23 | ptr = FFI::MemoryPointer.new(:pointer) 24 | 25 | rc = gp_list_get_name(@camera_list.ptr, @index, ptr) 26 | GPhoto2.check!(rc) 27 | 28 | ptr = ptr.read_pointer 29 | ptr.null? ? nil : ptr.read_string 30 | end 31 | 32 | def get_value 33 | ptr = FFI::MemoryPointer.new(:pointer) 34 | 35 | rc = gp_list_get_value(@camera_list.ptr, @index, ptr) 36 | GPhoto2.check!(rc) 37 | 38 | ptr = ptr.read_pointer 39 | ptr.null? ? nil : ptr.read_string 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_widgets/radio_camera_widget_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe RadioCameraWidget do 5 | it_behaves_like CameraWidget 6 | 7 | describe '#value' do 8 | it 'has a String return value' do 9 | widget = RadioCameraWidget.new(nil) 10 | allow(widget).to receive(:value).and_return('text') 11 | expect(widget.value).to be_kind_of(String) 12 | end 13 | end 14 | 15 | describe '#choices' do 16 | it 'returns a list of valid radio choices' do 17 | size = 2 18 | 19 | widget = RadioCameraWidget.new(nil) 20 | allow(widget).to receive(:count_choices).and_return(size) 21 | allow(widget).to receive(:get_choice).and_return("choice") 22 | 23 | expect(widget).to receive(:get_choice).exactly(size).times 24 | 25 | choices = widget.choices 26 | 27 | expect(choices.size).to eq(size) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/gphoto2/port_info_list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe PortInfoList do 5 | before do 6 | allow_any_instance_of(PortInfoList).to receive(:new) 7 | allow_any_instance_of(PortInfoList).to receive(:load) 8 | end 9 | 10 | describe '#lookup_path' do 11 | it 'returns the index of the port in the list' do 12 | port = 'usb:250,006' 13 | index = 0 14 | 15 | list = PortInfoList.new 16 | allow(list).to receive(:_lookup_path).and_return(index) 17 | 18 | expect(list.lookup_path(port)).to eq(index) 19 | end 20 | end 21 | 22 | describe '#at' do 23 | it 'returns a new PortInfo instance at the specified index' do 24 | allow_any_instance_of(PortInfo).to receive(:new) 25 | 26 | list = PortInfoList.new 27 | port_info = list.at(0) 28 | 29 | expect(port_info).to be_kind_of(PortInfo) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_widget.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | class CameraWidget < FFI::Struct 4 | # libgphoto2/gphoto2-widget.c 5 | layout :type, CameraWidgetType, 6 | :label, [:char, 256], 7 | :info, [:char, 1024], 8 | :name, [:char, 256], 9 | 10 | :parent, CameraWidget.by_ref, 11 | 12 | :value_string, :string, 13 | :value_int, :int, 14 | :value_float, :float, 15 | 16 | :choice, :pointer, 17 | :choice_count, :int, 18 | 19 | :min, :float, 20 | :max, :float, 21 | :increment, :float, 22 | 23 | :children, :pointer, # CameraWidget** 24 | :children_count, :int, 25 | 26 | :changed, :int, 27 | :readonly, :int, 28 | :ref_count, :int, 29 | :id, :int, 30 | :callback, :pointer # CameraWidgetCallback 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_widgets/range_camera_widget.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class RangeCameraWidget < CameraWidget 3 | # @return [Array] 4 | def range 5 | min, max, inc = get_range 6 | (min..max).step(inc).to_a 7 | end 8 | 9 | protected 10 | 11 | def get_value 12 | val = FFI::MemoryPointer.new(:float) 13 | rc = gp_widget_get_value(ptr, val) 14 | GPhoto2.check!(rc) 15 | val.read_float 16 | end 17 | 18 | def set_value(value) 19 | val = FFI::MemoryPointer.new(:float) 20 | val.write_float(value) 21 | rc = gp_widget_set_value(ptr, val) 22 | GPhoto2.check!(rc) 23 | end 24 | 25 | private 26 | 27 | def get_range 28 | min = FFI::MemoryPointer.new(:float) 29 | max = FFI::MemoryPointer.new(:float) 30 | inc = FFI::MemoryPointer.new(:float) 31 | 32 | rc = gp_widget_get_range(ptr, min, max, inc) 33 | GPhoto2.check!(rc) 34 | 35 | [min.read_float, max.read_float, inc.read_float] 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/gphoto2/port_info_list.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class PortInfoList 3 | include FFI::GPhoto2Port 4 | include GPhoto2::Struct 5 | 6 | def initialize 7 | new 8 | load 9 | end 10 | 11 | # @param [String] port 12 | # @return [Integer] 13 | def lookup_path(port) 14 | _lookup_path(port) 15 | end 16 | alias_method :index, :lookup_path 17 | 18 | # @param [Integer] index 19 | # @return [GPhoto2::PortInfo] 20 | def at(index) 21 | PortInfo.new(self, index) 22 | end 23 | alias_method :[], :at 24 | 25 | private 26 | 27 | def new 28 | ptr = FFI::MemoryPointer.new(:pointer) 29 | rc = gp_port_info_list_new(ptr) 30 | GPhoto2.check!(rc) 31 | @ptr = GPPortInfoList.new(ptr.read_pointer) 32 | end 33 | 34 | def load 35 | rc = gp_port_info_list_load(ptr) 36 | GPhoto2.check!(rc) 37 | end 38 | 39 | def _lookup_path(port) 40 | index = rc = gp_port_info_list_lookup_path(ptr, port) 41 | GPhoto2.check!(rc) 42 | index 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/support/shared_examples/camera_file_info_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for GPhoto2::CameraFileInfo do 2 | let(:camera_file_info) do 3 | OpenStruct.new( 4 | fields: 0xff, 5 | status: :not_downloaded, 6 | size: 7355608, 7 | type: 'image/jpeg' 8 | ) 9 | end 10 | 11 | let(:info) { described_class.new(camera_file_info) } 12 | 13 | describe '#fields' do 14 | it 'returns a bit field of set fields' do 15 | expect(info.fields).to eq(0xff) 16 | end 17 | end 18 | 19 | describe '#has_field?' do 20 | it 'returns whether a field is set' do 21 | expect(info.has_field?(:size)).to be(true) 22 | end 23 | end 24 | 25 | describe '#status' do 26 | it 'returns the file status' do 27 | expect(info.status).to eq(:not_downloaded) 28 | end 29 | end 30 | 31 | describe '#size' do 32 | it 'returns the filesize' do 33 | expect(info.size).to eq(7355608) 34 | end 35 | end 36 | 37 | describe '#type' do 38 | it 'returns the file type' do 39 | expect(info.type).to eq('image/jpeg') 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Michael Macias 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. 23 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/camera_abilities.rb: -------------------------------------------------------------------------------- 1 | require 'ffi/gphoto2_port/gp_port_type' 2 | 3 | module FFI 4 | module GPhoto2 5 | class CameraAbilities < FFI::Struct 6 | # gphoto2/gphoto2-abilities-list.h 7 | layout :model, [:char, 128], 8 | :status, CameraDriverStatus, 9 | 10 | :port, GPhoto2Port::GPPortType, 11 | :speed, [:int, 64], 12 | 13 | :operations, CameraOperation, 14 | :file_operations, CameraFileOperation, 15 | :folder_operations, CameraFolderOperation, 16 | 17 | :usb_vendor, :int, 18 | :usb_product, :int, 19 | :usb_class, :int, 20 | :usb_subclass, :int, 21 | :usb_protocol, :int, 22 | 23 | :library, [:char, 1024], 24 | :id, [:char, 1024], 25 | 26 | :device_type, GphotoDeviceType, 27 | 28 | :reserved2, :int, 29 | :reserved3, :int, 30 | :reserved4, :int, 31 | :reserved5, :int, 32 | :reserved6, :int, 33 | :reserved7, :int, 34 | :reserved8, :int 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_widgets/radio_camera_widget.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class RadioCameraWidget < CameraWidget 3 | # @return [Array] 4 | def choices 5 | count_choices.times.map { |i| get_choice(i) } 6 | end 7 | 8 | protected 9 | 10 | def get_value 11 | val_ptr = FFI::MemoryPointer.new(:pointer) 12 | 13 | rc = gp_widget_get_value(ptr, val_ptr) 14 | GPhoto2.check!(rc) 15 | 16 | val_ptr = val_ptr.read_pointer 17 | val_ptr.null? ? nil : val_ptr.read_string 18 | end 19 | 20 | def set_value(value) 21 | val = FFI::MemoryPointer.from_string(value.to_s) 22 | rc = gp_widget_set_value(ptr, val) 23 | GPhoto2.check!(rc) 24 | end 25 | 26 | private 27 | 28 | def count_choices 29 | rc = gp_widget_count_choices(ptr) 30 | GPhoto2.check!(rc) 31 | rc 32 | end 33 | 34 | def get_choice(i) 35 | val_ptr = FFI::MemoryPointer.new(:pointer) 36 | 37 | rc = gp_widget_get_choice(ptr, i, val_ptr) 38 | GPhoto2.check!(rc) 39 | 40 | val_ptr = val_ptr.read_pointer 41 | val_ptr.null? ? nil : val_ptr.read_string 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2/gp_context.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module GPhoto2 3 | class GPContext < FFI::Struct 4 | # libgphoto2/gphoto2-context.c 5 | layout :idle_func, :pointer, # GPContextIdleFunc 6 | :idle_func_data, :pointer, # void* 7 | 8 | :progress_start_func, :pointer, # GPContextProgressStartFunc 9 | :progress_update_func, :pointer, # GPContextProgressUpdateFunc 10 | :progress_stop_func, :pointer, # GPContextProgressStopFunc 11 | :progress_func_data, :pointer, # void* 12 | 13 | :error_func, :pointer, # GPContextErrorFunc 14 | :error_func_data, :pointer, # void* 15 | 16 | :question_func, :pointer, # GPContextQuestionFunc 17 | :question_func_data, :pointer, # void* 18 | 19 | :cancel_func, :pointer, # GPContextCancelFunc 20 | :cancel_func_data, :pointer, # void* 21 | 22 | :status_func, :pointer, # GPContextStatusFunc 23 | :status_func_data, :pointer, # void* 24 | 25 | :message_func, :pointer, # GPContextMessageFunc 26 | :message_func_data, :pointer, # void* 27 | 28 | :ref_count, :uint 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/shared_examples/camera/event_examples.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | shared_examples_for Camera::Event do 3 | describe '#wait' do 4 | let(:camera) { Camera.new(model, port) } 5 | let(:event) { double('camera_event') } 6 | 7 | before do 8 | allow(camera).to receive(:wait_for_event).and_return(event) 9 | end 10 | 11 | it 'waits for a camera event' do 12 | expect(camera).to receive(:wait_for_event) 13 | camera.wait 14 | end 15 | 16 | it 'returns an event symbol' do 17 | expect(camera.wait).to eq(event) 18 | end 19 | end 20 | 21 | describe '#wait_for' do 22 | let(:camera) { Camera.new(model, port) } 23 | let(:event) { double('camera_event', type: :capture_complete) } 24 | 25 | before do 26 | allow(camera).to receive(:wait).and_return(event) 27 | end 28 | 29 | it 'blocks until a given event is returned from #wait' do 30 | expect(camera).to receive(:wait) 31 | camera.wait_for(:capture_complete) 32 | end 33 | 34 | it 'returns the first event of the given type' do 35 | expect(camera.wait_for(:capture_complete)).to eq(event) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/gphoto2/port.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class Port 3 | include FFI::GPhoto2Port 4 | include GPhoto2::Struct 5 | 6 | def initialize 7 | @ptr = new 8 | end 9 | 10 | # @param [PortInfo] info 11 | # @return [PortInfo] 12 | def info=(info) 13 | set_info(info) 14 | info 15 | end 16 | 17 | # @return [void] 18 | def open 19 | _open 20 | end 21 | 22 | # @return [void] 23 | def close 24 | _close 25 | end 26 | 27 | # @return [void] 28 | def reset 29 | _reset 30 | end 31 | 32 | private 33 | 34 | def new 35 | ptr = FFI::MemoryPointer.new(:pointer) 36 | rc = gp_port_new(ptr) 37 | GPhoto2.check!(rc) 38 | FFI::GPhoto2Port::GPPort.new(ptr.read_pointer) 39 | end 40 | 41 | def _open 42 | rc = gp_port_open(ptr) 43 | GPhoto2.check!(rc) 44 | end 45 | 46 | def _close 47 | rc = gp_port_close(ptr) 48 | GPhoto2.check!(rc) 49 | end 50 | 51 | def _reset 52 | rc = gp_port_reset(ptr) 53 | GPhoto2.check!(rc) 54 | end 55 | 56 | def set_info(port_info) 57 | rc = gp_port_set_info(ptr, port_info.ptr) 58 | GPhoto2.check!(rc) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /ffi-gphoto2.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | 4 | require 'gphoto2/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'ffi-gphoto2' 8 | spec.summary = 'A Ruby FFI for common functions in libgphoto2' 9 | spec.homepage = 'https://github.com/zaeleus/ffi-gphoto2' 10 | 11 | spec.authors = ['Michael Macias'] 12 | spec.email = ['zaeleus@gmail.com'] 13 | 14 | spec.version = GPhoto2::VERSION 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files`.split($/) 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 20 | spec.require_paths = ['lib'] 21 | 22 | spec.requirements << 'libgphoto2 >= 2.5.2' 23 | spec.requirements << 'libgphoto2_port >= 0.10.1' 24 | spec.required_ruby_version = '>= 2.3' 25 | 26 | spec.add_development_dependency 'bundler', '~> 2.0' 27 | spec.add_development_dependency 'rake', '~> 13.0' 28 | spec.add_development_dependency 'rspec', '~> 3.11.0' 29 | spec.add_development_dependency 'yard', '~> 0.9.0' 30 | 31 | spec.add_dependency 'ffi', '~> 1.15.0' 32 | end 33 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_file_info/camera_file_info.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | # @abstract 3 | class CameraFileInfo 4 | include FFI::GPhoto2 5 | include GPhoto2::Struct 6 | 7 | # @param [FFI::GPhoto2::CameraFileInfo] ptr 8 | def initialize(ptr) 9 | @ptr = ptr 10 | end 11 | 12 | # @return [Integer] a bit field of set info fields 13 | def fields 14 | fields = ptr[:fields] 15 | 16 | if fields.is_a?(Symbol) 17 | CameraFileInfoFields[fields] 18 | else 19 | fields 20 | end 21 | end 22 | 23 | # return [Boolean] whether the given field is set 24 | def has_field?(field) 25 | (fields & CameraFileInfoFields[field]) != 0 26 | end 27 | 28 | # @return [CameraFileStatus, nil] 29 | def status 30 | fetch(:status) 31 | end 32 | 33 | # @return [Integer, nil] the size of the file in bytes 34 | def size 35 | fetch(:size) 36 | end 37 | 38 | # @return [String, nil] the media type of the file 39 | def type 40 | type = fetch(:type) 41 | type ? type.to_s : nil 42 | end 43 | 44 | protected 45 | 46 | # param [Symbol] key 47 | # @return [Object, nil] 48 | def fetch(key) 49 | ptr[key] if has_field?(key) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/gphoto2/camera/filesystem.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class Camera 3 | module Filesystem 4 | # @example 5 | # # Get a list of filenames in a path. 6 | # folder = camera/'store_00010001/DCIM/100D5100' 7 | # folder.files.map(&:name) 8 | # # => ["DSC_0001.JPG", "DSC_0002.JPG", ...] 9 | # 10 | # @param [String] root 11 | # @return [CameraFolder] 12 | def filesystem(root = '/') 13 | root = "/#{root}" if root[0] != '/' 14 | CameraFolder.new(self, root) 15 | end 16 | alias_method :/, :filesystem 17 | 18 | # @param [CameraFile] file 19 | # @return [CameraFile] 20 | def file(file) 21 | file_get(file) 22 | end 23 | 24 | # @param [CameraFile] file 25 | # @return [void] 26 | def delete(file) 27 | file_delete(file) 28 | end 29 | 30 | private 31 | 32 | def file_get(file, type = :normal) 33 | rc = gp_camera_file_get(ptr, file.folder, file.name, type, file.ptr, context.ptr) 34 | GPhoto2.check!(rc) 35 | file 36 | end 37 | 38 | def file_delete(file) 39 | rc = gp_camera_file_delete(ptr, file.folder, file.name, context.ptr) 40 | GPhoto2.check!(rc) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /examples/list_files.rb: -------------------------------------------------------------------------------- 1 | require 'gphoto2' 2 | 3 | # Recursively list folder contents with extended metadata. 4 | 5 | MAGNITUDES = %w[bytes KiB MiB GiB].freeze 6 | 7 | # @param [Integer] size filesize in bytes 8 | # @param [Integer[ precision 9 | # @return [String] 10 | def format_filesize(size, precision = 1) 11 | n = 0 12 | 13 | while size >= 1024.0 && n < MAGNITUDES.size 14 | size /= 1024.0 15 | n += 1 16 | end 17 | 18 | "%.#{precision}f %s" % [size, MAGNITUDES[n]] 19 | end 20 | 21 | # @param [CameraFolder] folder a root directory 22 | def visit(folder) 23 | files = folder.files 24 | 25 | puts "#{folder.path} (#{files.size} files)" 26 | 27 | files.each do |file| 28 | info = file.info 29 | 30 | name = file.name 31 | # Avoid using `File#size` here to prevent having to load the data along 32 | # with it. 33 | size = format_filesize(info.size) 34 | mtime = info.mtime.utc.iso8601 35 | 36 | if info.has_field?(:width) && info.has_field?(:height) 37 | dimensions = "#{info.width}x#{info.height}" 38 | else 39 | dimensions = '-' 40 | end 41 | 42 | puts "#{name.ljust(30)} #{size.rjust(12)} #{dimensions.rjust(12)} #{mtime}" 43 | end 44 | 45 | puts 46 | 47 | folder.folders.each { |child| visit(child) } 48 | end 49 | 50 | GPhoto2::Camera.first do |camera| 51 | visit(camera.filesystem) 52 | end 53 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_abilities_list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe CameraAbilitiesList do 5 | let(:context) { double('context') } 6 | 7 | before do 8 | allow_any_instance_of(CameraAbilitiesList).to receive(:new) 9 | allow_any_instance_of(CameraAbilitiesList).to receive(:load) 10 | end 11 | 12 | describe '#detect' do 13 | it 'returns a list cameras' do 14 | camera_list = double('camera_list') 15 | 16 | abilities_list = CameraAbilitiesList.new(context) 17 | allow(abilities_list).to receive(:_detect).and_return(camera_list) 18 | 19 | expect(abilities_list.detect).to eq(camera_list) 20 | end 21 | end 22 | 23 | describe '#lookup_model' do 24 | it 'returns the index of the abilities in the list' do 25 | index = 0 26 | 27 | list = CameraAbilitiesList.new(context) 28 | allow(list).to receive(:_lookup_model).and_return(index) 29 | 30 | expect(list.lookup_model('model')).to eq(index) 31 | end 32 | end 33 | 34 | describe '#at' do 35 | it 'returns a new CameraAbilities instance at the specified index' do 36 | abilities = double('camera_abilities') 37 | 38 | allow(CameraAbilities).to receive(:new).and_return(abilities) 39 | list = CameraAbilitiesList.new(context) 40 | 41 | expect(list.at(0)).to eq(abilities) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/support/shared_examples/camera/filesystem_examples.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | shared_examples_for Camera::Filesystem do 3 | describe '#filesystem' do 4 | let(:camera) { Camera.new(model, port) } 5 | 6 | context 'when a path is passed' do 7 | let(:path) { '/store_00010001' } 8 | 9 | it 'assumes the path is absolute' do 10 | fs = camera.filesystem(path[1..-1]) 11 | expect(fs.path).to eq(path) 12 | end 13 | 14 | it 'returns a folder at the path that was passed' do 15 | fs = camera.filesystem(path) 16 | expect(fs.path).to eq(path) 17 | end 18 | end 19 | 20 | context 'when no path is passed' do 21 | it 'returns a folder at the root of the filesystem' do 22 | fs = camera.filesystem 23 | expect(fs.path).to eq('/') 24 | end 25 | end 26 | end 27 | 28 | describe '#file' do 29 | it 'retrieves a file from the camera' do 30 | camera = Camera.new(model, port) 31 | allow(camera).to receive(:file_get) 32 | expect(camera).to receive(:file_get) 33 | camera.file(double('camera_file')) 34 | end 35 | end 36 | 37 | describe '#delete' do 38 | it 'deletes a file from the camera' do 39 | camera = Camera.new(model, port) 40 | allow(camera).to receive(:file_delete) 41 | expect(camera).to receive(:file_delete) 42 | camera.delete(double('camera_file')) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_abilities.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class CameraAbilities 3 | include FFI::GPhoto2 4 | include GPhoto2::Struct 5 | 6 | # @param [String] model the name of the device 7 | # @return [GPhoto2::CameraAbilities] 8 | def self.find(model) 9 | context = Context.new 10 | 11 | camera_abilities_list = CameraAbilitiesList.new(context) 12 | index = camera_abilities_list.lookup_model(model) 13 | abilities = camera_abilities_list[index] 14 | 15 | context.finalize 16 | 17 | abilities 18 | end 19 | 20 | # @param [GPhoto2::CameraAbilitiesList] camera_abilities_list 21 | # @param [Integer] index 22 | def initialize(camera_abilities_list, index) 23 | @camera_abilities_list = camera_abilities_list 24 | @index = index 25 | get_abilities 26 | end 27 | 28 | # @return [Object] 29 | def [](field) 30 | ptr[field] 31 | end 32 | 33 | # @return [Integer] a bit field of supported operations 34 | def operations 35 | if self[:operations] == :none 36 | CameraOperation[:none] 37 | else 38 | self[:operations] 39 | end 40 | end 41 | 42 | private 43 | 44 | def get_abilities 45 | ptr = FFI::GPhoto2::CameraAbilities.new 46 | rc = gp_abilities_list_get_abilities(@camera_abilities_list.ptr, 47 | @index, 48 | ptr) 49 | GPhoto2.check!(rc) 50 | @ptr = ptr 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/support/shared_examples/camera/capture_examples.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | shared_examples_for Camera::Capture do 3 | describe '#capture' do 4 | let(:camera) { Camera.new(model, port) } 5 | let(:path) { double('camera_file_path', folder: 'folder', name: 'name') } 6 | 7 | before do 8 | allow(camera).to receive(:_capture).and_return(path) 9 | end 10 | 11 | it 'saves the camera configuration' do 12 | expect(camera).to receive(:save) 13 | camera.capture 14 | end 15 | 16 | it 'returns a new CameraFile' do 17 | expect(camera).to receive(:_capture) 18 | expect(camera.capture).to be_kind_of(CameraFile) 19 | end 20 | end 21 | 22 | describe '#trigger' do 23 | let(:camera) { Camera.new(model, port) } 24 | 25 | before do 26 | allow(camera).to receive(:trigger_capture) 27 | end 28 | 29 | it 'saves the camera configuration' do 30 | expect(camera).to receive(:save) 31 | camera.trigger 32 | end 33 | end 34 | 35 | describe '#preview' do 36 | let(:camera) { Camera.new(model, port) } 37 | let(:file) { double('camera_file') } 38 | 39 | before do 40 | allow(camera).to receive(:save) 41 | allow(camera).to receive(:capture_preview).and_return(file) 42 | end 43 | 44 | it 'saves the camera configuration' do 45 | expect(camera).to receive(:save) 46 | camera.preview 47 | end 48 | 49 | it 'returns a new CameraFile' do 50 | expect(camera.preview).to eq(file) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/gphoto2/camera/event.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class Camera 3 | module Event 4 | # @param [Integer] timeout time to wait in milliseconds 5 | # @return [CameraEvent] 6 | def wait(timeout = 2000) 7 | wait_for_event(timeout) 8 | end 9 | 10 | # @param [CameraEventType] event_type 11 | # @return [CameraEvent] 12 | def wait_for(event_type) 13 | begin 14 | event = wait 15 | end until event.type == event_type 16 | 17 | event 18 | end 19 | 20 | private 21 | 22 | def wait_for_event(timeout) 23 | # assume CameraEventType is an int 24 | type_ptr = FFI::MemoryPointer.new(:int) 25 | data_ptr = FFI::MemoryPointer.new(:pointer) 26 | 27 | rc = gp_camera_wait_for_event(ptr, timeout, type_ptr, data_ptr, context.ptr) 28 | GPhoto2.check!(rc) 29 | 30 | type = FFI::GPhoto2::CameraEventType[type_ptr.read_int] 31 | data = data_ptr.read_pointer 32 | 33 | data = 34 | case type 35 | when :unknown 36 | data.null? ? nil : data.read_string 37 | when :file_added 38 | path_ptr = FFI::GPhoto2::CameraFilePath.new(data) 39 | path = CameraFilePath.new(path_ptr) 40 | CameraFile.new(self, path.folder, path.name) 41 | when :folder_added 42 | path_ptr = FFI::GPhoto2::CameraFilePath.new(data) 43 | path = CameraFilePath.new(path_ptr) 44 | CameraFolder.new(self, '%s/%s' % [path.folder, path.name]) 45 | else 46 | nil 47 | end 48 | 49 | CameraEvent.new(type, data) 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_file_info/file_camera_file_info_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ostruct' 3 | 4 | module GPhoto2 5 | describe FileCameraFileInfo do 6 | it_behaves_like CameraFileInfo 7 | 8 | let(:camera_file_info_file) do 9 | OpenStruct.new( 10 | fields: 0xff, 11 | status: :not_downloaded, 12 | size: 7355608, 13 | type: 'image/jpeg', 14 | width: 4928, 15 | height: 3264, 16 | permissions: 0xff, 17 | mtime: Time.at(1467863342) 18 | ) 19 | end 20 | 21 | describe '#width' do 22 | it 'returns the width of the file' do 23 | info = FileCameraFileInfo.new(camera_file_info_file) 24 | expect(info.width).to eq(4928) 25 | end 26 | end 27 | 28 | describe '#height' do 29 | it 'returns the height of the file' do 30 | info = FileCameraFileInfo.new(camera_file_info_file) 31 | expect(info.height).to eq(3264) 32 | end 33 | end 34 | 35 | describe '#readable?' do 36 | it 'returns whether the file is readable' do 37 | info = FileCameraFileInfo.new(camera_file_info_file) 38 | expect(info.readable?).to be(true) 39 | end 40 | end 41 | 42 | describe '#deletable?' do 43 | it 'returns whether the file is deletable' do 44 | info = FileCameraFileInfo.new(camera_file_info_file) 45 | expect(info.deletable?).to be(true) 46 | end 47 | end 48 | 49 | describe '#mtime' do 50 | it 'returns the last modification time of the file' do 51 | info = FileCameraFileInfo.new(camera_file_info_file) 52 | expect(info.mtime).to eq(Time.at(1467863342)) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_abilities_list.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class CameraAbilitiesList 3 | include FFI::GPhoto2 4 | include GPhoto2::Struct 5 | 6 | # @param [GPhoto2::Context] context 7 | def initialize(context) 8 | @context = context 9 | new 10 | load 11 | end 12 | 13 | # @return [GPhoto2::CameraList] 14 | def detect 15 | _detect 16 | end 17 | 18 | # @param [String] model 19 | # @return [Integer] 20 | def lookup_model(model) 21 | _lookup_model(model) 22 | end 23 | alias_method :index, :lookup_model 24 | 25 | # @param [Integer] index 26 | # @return [GPhoto2::CameraAbilities] 27 | def at(index) 28 | CameraAbilities.new(self, index) 29 | end 30 | alias_method :[], :at 31 | 32 | private 33 | 34 | def new 35 | ptr = FFI::MemoryPointer.new(:pointer) 36 | rc = gp_abilities_list_new(ptr) 37 | GPhoto2.check!(rc) 38 | @ptr = FFI::GPhoto2::CameraAbilitiesList.new(ptr.read_pointer) 39 | end 40 | 41 | def load 42 | rc = gp_abilities_list_load(ptr, @context.ptr) 43 | GPhoto2.check!(rc) 44 | end 45 | 46 | def _detect 47 | port_info_list = PortInfoList.new 48 | camera_list = CameraList.new 49 | 50 | rc = gp_abilities_list_detect(ptr, 51 | port_info_list.ptr, 52 | camera_list.ptr, 53 | @context.ptr) 54 | GPhoto2.check!(rc) 55 | 56 | camera_list 57 | end 58 | 59 | def _lookup_model(model) 60 | index = rc = gp_abilities_list_lookup_model(ptr, model) 61 | GPhoto2.check!(rc) 62 | index 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/gphoto2/port_info_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe PortInfo do 5 | let(:port_info_list) { double('port_info_list') } 6 | let(:index) { 0 } 7 | 8 | before do 9 | allow_any_instance_of(PortInfo).to receive(:new) 10 | end 11 | 12 | describe '.find' do 13 | it 'returns a new PortInfo instance from a port path' do 14 | allow_any_instance_of(PortInfoList).to receive(:new) 15 | allow_any_instance_of(PortInfoList).to receive(:load) 16 | allow_any_instance_of(PortInfoList).to receive(:lookup_path) 17 | info = PortInfo.new(port_info_list, index) 18 | allow_any_instance_of(PortInfoList).to receive(:[]).and_return(info) 19 | 20 | port_info = PortInfo.find('usb:250,006') 21 | expect(port_info).to be_kind_of(PortInfo) 22 | end 23 | end 24 | 25 | describe '#name' do 26 | it 'returns the name of the port' do 27 | name = 'name' 28 | port_info = PortInfo.new(port_info_list, index) 29 | allow(port_info).to receive(:get_name).and_return(name) 30 | expect(port_info.name).to eq(name) 31 | end 32 | end 33 | 34 | describe '#path' do 35 | it 'returns the path of the port' do 36 | path = 'path' 37 | port_info = PortInfo.new(port_info_list, index) 38 | allow(port_info).to receive(:get_path).and_return(path) 39 | expect(port_info.path).to eq(path) 40 | end 41 | end 42 | 43 | describe '#type' do 44 | it 'returns the type of the port' do 45 | type = :usb 46 | port_info = PortInfo.new(port_info_list, index) 47 | allow(port_info).to receive(:get_type).and_return(type) 48 | expect(port_info.type).to eq(type) 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/gphoto2/port_info.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class PortInfo 3 | include FFI::GPhoto2Port 4 | include GPhoto2::Struct 5 | 6 | # @param [String] port 7 | # @return [GPhoto2::PortInfo] 8 | def self.find(port) 9 | port_info_list = PortInfoList.new 10 | index = port_info_list.lookup_path(port) 11 | port_info_list[index] 12 | end 13 | 14 | # @param [GPhoto2::PortInfoList] port_info_list 15 | # @param [Integer] index 16 | def initialize(port_info_list, index) 17 | @port_info_list = port_info_list 18 | @index = index 19 | new 20 | end 21 | 22 | # @return [String] 23 | def name 24 | get_name 25 | end 26 | 27 | # @return [String] 28 | def path 29 | get_path 30 | end 31 | 32 | # @return [GPPortType] 33 | def type 34 | get_type 35 | end 36 | 37 | private 38 | 39 | def new 40 | ptr = FFI::MemoryPointer.new(GPPortInfo) 41 | rc = gp_port_info_list_get_info(@port_info_list.ptr, @index, ptr) 42 | GPhoto2.check!(rc) 43 | @ptr = GPPortInfo.new(ptr.read_pointer) 44 | end 45 | 46 | def get_name 47 | name_ptr = FFI::MemoryPointer.new(:pointer) 48 | 49 | rc = gp_port_info_get_name(ptr, name_ptr) 50 | GPhoto2.check!(rc) 51 | 52 | name_ptr = name_ptr.read_pointer 53 | name_ptr.null? ? nil : name_ptr.read_string 54 | end 55 | 56 | def get_path 57 | path_ptr = FFI::MemoryPointer.new(:pointer) 58 | 59 | rc = gp_port_info_get_path(ptr, path_ptr) 60 | GPhoto2.check!(rc) 61 | 62 | path_ptr = path_ptr.read_pointer 63 | path_ptr.null? ? nil : path_ptr.read_string 64 | end 65 | 66 | def get_type 67 | # assume GPPortType is an int 68 | type = FFI::MemoryPointer.new(:int) 69 | rc = gp_port_info_get_type(ptr, type) 70 | GPhoto2.check!(rc) 71 | GPPortType[type.read_int] 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2_port.rb: -------------------------------------------------------------------------------- 1 | require 'ffi' 2 | 3 | module FFI 4 | module GPhoto2Port 5 | extend FFI::Library 6 | 7 | ffi_lib 'libgphoto2_port' 8 | 9 | # constants 10 | require 'ffi/gphoto2_port/gp_port_result' 11 | 12 | # enums 13 | require 'ffi/gphoto2_port/gp_port_serial_parity' 14 | require 'ffi/gphoto2_port/gp_port_type' 15 | 16 | # structs 17 | require 'ffi/gphoto2_port/gp_port_settings_serial' 18 | require 'ffi/gphoto2_port/gp_port_settings_usb' 19 | require 'ffi/gphoto2_port/gp_port_settings_usb_disk_direct' 20 | require 'ffi/gphoto2_port/gp_port_settings_usb_scsi' 21 | require 'ffi/gphoto2_port/gp_port_settings' 22 | require 'ffi/gphoto2_port/gp_port' 23 | require 'ffi/gphoto2_port/gp_port_info' 24 | require 'ffi/gphoto2_port/gp_port_info_list' 25 | 26 | # libgphoto2_port/gphoto2/gphoto2-port-info-list.h 27 | attach_function :gp_port_info_get_name, [GPPortInfo, :pointer], :int 28 | attach_function :gp_port_info_get_path, [GPPortInfo, :pointer], :int 29 | attach_function :gp_port_info_get_type, [GPPortInfo, :pointer], :int 30 | 31 | attach_function :gp_port_info_list_new, [:pointer], :int 32 | attach_function :gp_port_info_list_free, [:pointer], :int 33 | attach_function :gp_port_info_list_load, [GPPortInfoList.by_ref], :int 34 | attach_function :gp_port_info_list_lookup_path, [GPPortInfoList.by_ref, :string], :int 35 | attach_function :gp_port_info_list_get_info, [GPPortInfoList.by_ref, :int, :pointer], :int 36 | 37 | # libgphoto2_port/gphoto2/gphoto2-port-result.h 38 | attach_function :gp_port_result_as_string, [:int], :string 39 | 40 | # libgphoto2_port/gphoto2/gphoto2-port.h 41 | attach_function :gp_port_new, [:pointer], :int 42 | attach_function :gp_port_free, [:pointer], :int 43 | 44 | attach_function :gp_port_set_info, [GPPort.by_ref, GPPortInfo], :int 45 | 46 | attach_function :gp_port_open, [GPPort.by_ref], :int 47 | attach_function :gp_port_close, [GPPort.by_ref], :int 48 | 49 | attach_function :gp_port_reset, [GPPort.by_ref], :int 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.10.0 (2022-09-02) 4 | 5 | * [CHANGE] Mark `gp_camera_wait_for_event` as a blocking call ([#19]). 6 | 7 | [#19]: https://github.com/zaeleus/ffi-gphoto2/pull/19 8 | 9 | ## 0.9.0 (2021-04-17) 10 | 11 | * [CHANGE] Update to ffi 1.15.0. This raises the minimum Ruby version to 2.3.0. 12 | 13 | ## 0.8.0 (2020-06-29) 14 | 15 | * [FIX] Fix deprecation warning (`warning: rb_safe_level will be removed in 16 | Ruby 3.0`) by updating to ffi 1.12.0. 17 | * [CHANGE] Raise minimum Ruby version to 2.0.0, which comes from the dependency 18 | on ffi 1.12.0 (specifically 1.11.1). 19 | 20 | ## 0.7.1 (2017-01-02) 21 | 22 | * [FIX] Load `ffi` before any usage of the bindings. 23 | 24 | ## 0.7.0 (2016-09-19) 25 | 26 | * [CHANGE] Raise minimum `libgphoto2` version to 2.5.2. This version introduced 27 | `gp_port_reset`. 28 | * [ADD] Add `FFI::GPhoto2Port::GPPort` struct and related functions to do a 29 | port reset. 30 | 31 | ## 0.6.1 (2016-08-21) 32 | 33 | * [FIX] `ManagedStruct.release` actually calls `*_free` functions. Autorelease 34 | invocations were silently failing with a `TypeError` because the functions 35 | expected structs, not pointers. 36 | 37 | ## 0.6.0 (2016-07-11) 38 | 39 | * [FIX] Use the correct default filename when a `CameraFile` is a preview. 40 | * [ADD] Add `CameraFileInfo` and related operations. `CameraFile#info` only 41 | supports files. 42 | * [FIX] `CameraWidget#label` calls the correct widget label function. 43 | * [FIX] `Camera#[]=` raises an `ArgumentError` when passed an invalid key 44 | instead of failing on `nil`. 45 | * [ADD] Add `CameraAbilities#operations` to always return an `Integer`. 46 | libgphoto2 does not stay in the defined enum set of `CameraOperations`; 47 | therefore, it is treated as an integer bit field instead. 48 | * [CHANGE] Errors are thrown as `GPhoto2::Error` rather than `RuntimeError`. 49 | Use `#message` and `#code` to extract the the GPhoto2 error message and 50 | return code, respectively. 51 | * [ADD] `Camera#trigger` for trigger capture. 52 | * [FIX] `TextCameraWidget` values can by any object that implements `#to_s`. 53 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_folder.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class CameraFolder 3 | include FFI::GPhoto2 4 | 5 | # @return [String] 6 | attr_reader :path 7 | 8 | def initialize(camera, path = '/') 9 | @camera = camera 10 | @path = path 11 | end 12 | 13 | # @return [Boolean] 14 | def root? 15 | @path == '/' 16 | end 17 | 18 | # @return [String] 19 | def name 20 | if root? 21 | '/' 22 | else 23 | @path.rpartition('/').last 24 | end 25 | end 26 | 27 | # @return [Array] 28 | def folders 29 | folder_list_folders 30 | end 31 | 32 | # @return [Array] 33 | def files 34 | folder_list_files 35 | end 36 | 37 | # @param [String] name the name of the directory 38 | # @return [GPhoto2::CameraFolder] 39 | def cd(name) 40 | case name 41 | when '.' 42 | self 43 | when '..' 44 | up 45 | else 46 | CameraFolder.new(@camera, File.join(@path, name)) 47 | end 48 | end 49 | alias_method :/, :cd 50 | 51 | # @param [String] name the filename of the file to open 52 | # @return [GPhoto2::CameraFile] 53 | def open(name) 54 | CameraFile.new(@camera, @path, name) 55 | end 56 | 57 | # @return [GPhoto2::CameraFolder] 58 | def up 59 | if root? 60 | self 61 | else 62 | parent = @path.rpartition('/').first 63 | parent = '/' if parent.empty? 64 | CameraFolder.new(@camera, parent) 65 | end 66 | end 67 | 68 | # @return [String] 69 | def to_s 70 | name 71 | end 72 | 73 | private 74 | 75 | def folder_list_files 76 | list = CameraList.new 77 | 78 | rc = gp_camera_folder_list_files(@camera.ptr, @path, list.ptr, @camera.context.ptr) 79 | GPhoto2.check!(rc) 80 | 81 | list.to_a.map { |f| open(f.name) } 82 | end 83 | 84 | def folder_list_folders 85 | list = CameraList.new 86 | 87 | rc = gp_camera_folder_list_folders(@camera.ptr, @path, list.ptr, @camera.context.ptr) 88 | GPhoto2.check!(rc) 89 | 90 | list.to_a.map { |f| cd(f.name) } 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/gphoto2.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | require 'ffi/gphoto2' 4 | require 'ffi/gphoto2_port' 5 | 6 | require 'gphoto2/struct' 7 | 8 | require 'gphoto2/camera_widgets/camera_widget' 9 | require 'gphoto2/camera_widgets/radio_camera_widget' 10 | require 'gphoto2/camera_widgets/date_camera_widget' 11 | require 'gphoto2/camera_widgets/menu_camera_widget' 12 | require 'gphoto2/camera_widgets/range_camera_widget' 13 | require 'gphoto2/camera_widgets/section_camera_widget' 14 | require 'gphoto2/camera_widgets/text_camera_widget' 15 | require 'gphoto2/camera_widgets/toggle_camera_widget' 16 | require 'gphoto2/camera_widgets/window_camera_widget' 17 | 18 | require 'gphoto2/camera_file_info/camera_file_info' 19 | require 'gphoto2/camera_file_info/file_camera_file_info' 20 | 21 | require 'gphoto2/camera/capture' 22 | require 'gphoto2/camera/configuration' 23 | require 'gphoto2/camera/event' 24 | require 'gphoto2/camera/filesystem' 25 | require 'gphoto2/camera' 26 | 27 | require 'gphoto2/camera_abilities' 28 | require 'gphoto2/camera_abilities_list' 29 | require 'gphoto2/camera_event' 30 | require 'gphoto2/camera_file' 31 | require 'gphoto2/camera_file_path' 32 | require 'gphoto2/camera_folder' 33 | require 'gphoto2/camera_list' 34 | require 'gphoto2/context' 35 | require 'gphoto2/entry' 36 | require 'gphoto2/port' 37 | require 'gphoto2/port_info' 38 | require 'gphoto2/port_info_list' 39 | require 'gphoto2/port_result' 40 | 41 | require 'gphoto2/version' 42 | 43 | module GPhoto2 44 | # A runtime error for unsuccessful return codes. 45 | class Error < RuntimeError 46 | # @return [Integer] 47 | attr_reader :code 48 | 49 | # @param [String] message 50 | # @param [Integer] code 51 | def initialize(message, code) 52 | super(message) 53 | @code = code 54 | end 55 | 56 | # @return [String] 57 | def to_s 58 | "#{super} (#{code})" 59 | end 60 | end 61 | 62 | # @return [Logger] 63 | def self.logger 64 | @logger ||= Logger.new(STDERR) 65 | end 66 | 67 | # @param [Integer] rc 68 | # @return [void] 69 | # @raise [GPhoto2::Error] when the return code is not {FFI::GPhoto2Port::GP_OK} 70 | def self.check!(rc) 71 | logger.debug "#{caller.first} => #{rc}" if ENV['DEBUG'] 72 | return if rc >= FFI::GPhoto2Port::GP_OK 73 | raise Error.new(PortResult.as_string(rc), rc) 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_abilities_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe CameraAbilities do 5 | let(:camera_abilities_list) { double('camera_abilities_list') } 6 | let(:index) { 0 } 7 | 8 | before do 9 | allow_any_instance_of(CameraAbilities).to receive(:get_abilities) 10 | end 11 | 12 | describe '.find' do 13 | it 'returns a new CameraAbilities instance from a model name' do 14 | abilities = double('camera_abilities') 15 | 16 | context = double('context') 17 | allow(context).to receive(:finalize) 18 | allow(Context).to receive(:new).and_return(context) 19 | 20 | camera_abilities_list = double('camera_abilities_list') 21 | allow(CameraAbilitiesList).to receive(:new).and_return(camera_abilities_list) 22 | allow(camera_abilities_list).to receive(:lookup_model).and_return(0) 23 | allow(camera_abilities_list).to receive(:[]).and_return(abilities) 24 | 25 | expect(CameraAbilities.find('model')).to eq(abilities) 26 | end 27 | end 28 | 29 | describe '#[]' do 30 | it 'returns the value at the given field' do 31 | key, value = :model, 'name' 32 | abilities = CameraAbilities.new(camera_abilities_list, index) 33 | allow(abilities).to receive(:ptr).and_return({ key => value }) 34 | expect(abilities[key]).to eq(value) 35 | end 36 | end 37 | 38 | describe '#operations' do 39 | context 'when no or one operation is supported' do 40 | it 'returns a bit field of supported operations' do 41 | abilities = CameraAbilities.new(camera_abilities_list, index) 42 | allow(abilities).to receive(:[]).and_return(:none) 43 | expect(abilities.operations).to eq(0) 44 | end 45 | end 46 | 47 | context 'when multiple operations are supported' do 48 | it 'returns a bit field of supported operations' do 49 | abilities = CameraAbilities.new(camera_abilities_list, index) 50 | 51 | capture_image = FFI::GPhoto2::CameraOperation[:capture_image] 52 | config = FFI::GPhoto2::CameraOperation[:config] 53 | operations = capture_image | config 54 | 55 | allow(abilities).to receive(:[]).and_return(operations) 56 | expect(abilities.operations).to eq(0x11) 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/gphoto2/camera/capture.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class Camera 3 | module Capture 4 | # @example 5 | # # Take a photo. 6 | # file = camera.capture 7 | # 8 | # # And save it to the current working directory. 9 | # file.save 10 | # 11 | # @param [CameraCaptureType] type 12 | # @return [GPhoto2::CameraFile] 13 | def capture(type = :image) 14 | save 15 | path = _capture(type) 16 | CameraFile.new(self, path.folder, path.name) 17 | end 18 | 19 | # Triggers a capture and immedately returns. 20 | # 21 | # A camera trigger is the first half of a {#capture}. Instead of 22 | # returning a {GPhoto2::CameraFile}, a trigger immediately returns and 23 | # the caller has to poll for events. 24 | # 25 | # @example 26 | # camera.trigger 27 | # event = camera.wait_for(:file_added) 28 | # event.data # => CameraFile 29 | # 30 | # @return [void] 31 | def trigger 32 | save 33 | trigger_capture 34 | end 35 | 36 | # Captures a preview from the camera. 37 | # 38 | # Previews are not stored on the camera but are returned as data in a 39 | # {GPhoto2::CameraFile}. 40 | # 41 | # @example 42 | # # Capturing a preview is like using `Camera#capture`. 43 | # file = camera.preview 44 | # 45 | # # The resulting file will have neither a folder nor name. 46 | # file.preview? 47 | # # => true 48 | # 49 | # # But it will contain image data from the camera. 50 | # file.data 51 | # # => "\xFF\xD8\xFF\xDB\x00\x84\x00\x06..." 52 | # 53 | # @return [GPhoto2::CameraFile] 54 | def preview 55 | save 56 | capture_preview 57 | end 58 | 59 | private 60 | 61 | def _capture(type) 62 | path = CameraFilePath.new 63 | rc = gp_camera_capture(ptr, type, path.ptr, context.ptr) 64 | GPhoto2.check!(rc) 65 | path 66 | end 67 | 68 | def capture_preview 69 | file = CameraFile.new(self) 70 | rc = gp_camera_capture_preview(ptr, file.ptr, context.ptr) 71 | GPhoto2.check!(rc) 72 | file 73 | end 74 | 75 | def trigger_capture 76 | rc = gp_camera_trigger_capture(ptr, context.ptr) 77 | GPhoto2.check!(rc) 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_file.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class CameraFile 3 | include FFI::GPhoto2 4 | include GPhoto2::Struct 5 | 6 | # The preview data is assumed to be a jpg. 7 | PREVIEW_FILENAME = 'capture_preview.jpg'.freeze 8 | 9 | # @return [String] 10 | attr_reader :folder 11 | 12 | # @return [String] 13 | attr_reader :name 14 | 15 | # @param [GPhoto2::Camera] camera 16 | # @param [String] folder 17 | # @param [String] name 18 | def initialize(camera, folder = nil, name = nil) 19 | @camera = camera 20 | @folder, @name = folder, name 21 | new 22 | end 23 | 24 | # @return [Boolean] 25 | def preview? 26 | @folder.nil? && @name.nil? 27 | end 28 | 29 | # @param [String] pathname 30 | # @return [Integer] the number of bytes written 31 | def save(pathname = default_filename) 32 | File.binwrite(pathname, data) 33 | end 34 | 35 | # @return [void] 36 | def delete 37 | @camera.delete(self) 38 | end 39 | 40 | # @return [String] 41 | def data 42 | data_and_size.first 43 | end 44 | 45 | # @return [Integer] 46 | def size 47 | data_and_size.last 48 | end 49 | 50 | # @return [GPhoto2::CameraFileInfo, nil] 51 | def info 52 | preview? ? nil : get_info 53 | end 54 | 55 | private 56 | 57 | def data_and_size 58 | @data_and_size ||= begin 59 | @camera.file(self) unless preview? 60 | get_data_and_size 61 | end 62 | end 63 | 64 | def default_filename 65 | preview? ? PREVIEW_FILENAME : @name 66 | end 67 | 68 | def new 69 | ptr = FFI::MemoryPointer.new(:pointer) 70 | rc = gp_file_new(ptr) 71 | GPhoto2.check!(rc) 72 | @ptr = FFI::GPhoto2::CameraFile.new(ptr.read_pointer) 73 | end 74 | 75 | def get_data_and_size 76 | data_ptr = FFI::MemoryPointer.new(:pointer) 77 | size_ptr = FFI::MemoryPointer.new(:ulong) 78 | 79 | rc = gp_file_get_data_and_size(ptr, data_ptr, size_ptr) 80 | GPhoto2.check!(rc) 81 | 82 | size = size_ptr.read_ulong 83 | data = data_ptr.read_pointer.read_bytes(size) 84 | 85 | [data, size] 86 | end 87 | 88 | def get_info 89 | info = FFI::GPhoto2::CameraFileInfo.new 90 | 91 | rc = gp_camera_file_get_info(@camera.ptr, 92 | @folder, 93 | @name, 94 | info, 95 | @camera.context.ptr) 96 | GPhoto2.check!(rc) 97 | 98 | FileCameraFileInfo.new(info[:file]) 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/support/shared_examples/camera_widget_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for GPhoto2::CameraWidget do 2 | let(:klass) { described_class } 3 | 4 | describe '#name' do 5 | it 'returns the name of the widget' do 6 | name = 'name' 7 | widget = klass.new(nil) 8 | allow(widget).to receive(:get_name).and_return(name) 9 | expect(widget.name).to eq(name) 10 | end 11 | end 12 | 13 | describe '#value' do 14 | it 'returns the value of the widget' do 15 | value = 'value' 16 | widget = klass.new(nil) 17 | allow(widget).to receive(:get_value).and_return(value) 18 | expect(widget.value).to eq(value) 19 | end 20 | end 21 | 22 | describe '#value=' do 23 | let(:value) { 'value' } 24 | let(:widget) { klass.new(nil) } 25 | 26 | before do 27 | expect(widget).to receive(:set_value).with(value) 28 | end 29 | 30 | it 'sets a new value to the widget' do 31 | widget.value = value 32 | end 33 | 34 | it 'returns the passed value' do 35 | expect(widget.value = value).to eq(value) 36 | end 37 | end 38 | 39 | describe '#type' do 40 | it 'returns the type of the widget' do 41 | type = :window 42 | widget = klass.new(nil) 43 | allow(widget).to receive(:get_type).and_return(type) 44 | expect(widget.type).to eq(type) 45 | end 46 | end 47 | 48 | describe '#label' do 49 | it 'returns the label of the widget' do 50 | label = 'Beep' 51 | widget = klass.new(nil) 52 | allow(widget).to receive(:get_label).and_return(label) 53 | expect(widget.label).to eq(label) 54 | end 55 | end 56 | 57 | describe '#children' do 58 | it 'returns an array of child widgets' do 59 | size = 2 60 | 61 | widget = klass.new(nil) 62 | allow(widget).to receive(:count_children).and_return(size) 63 | allow(widget).to receive(:get_child) 64 | 65 | expect(widget).to receive(:get_child).exactly(size).times 66 | 67 | expect(widget.children).to be_kind_of(Array) 68 | end 69 | end 70 | 71 | describe '#flatten' do 72 | %w[a b].each do |name| 73 | let(name.to_sym) do 74 | widget = GPhoto2::TextCameraWidget.new(nil) 75 | allow(widget).to receive(:name).and_return(name) 76 | allow(widget).to receive(:type).and_return(:text) 77 | allow(widget).to receive(:children).and_return([]) 78 | widget 79 | end 80 | end 81 | 82 | it 'returns a map of name-widget pairs of its descendents' do 83 | widget = klass.new(nil) 84 | allow(widget).to receive(:name).and_return('a') 85 | allow(widget).to receive(:type).and_return(:section) 86 | allow(widget).to receive(:children).and_return([a, b]) 87 | 88 | expect(widget.flatten).to eq({ 'a' => a, 'b' => b }) 89 | end 90 | end 91 | 92 | describe '#to_s' do 93 | it "returns the string value of the widget" do 94 | value = 'value' 95 | widget = klass.new(nil) 96 | allow(widget).to receive(:value).and_return(value) 97 | expect(widget.to_s).to eq(value) 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe CameraFile do 5 | let(:camera) { double('camera') } 6 | let(:folder) { '/store_00010001' } 7 | let(:name) { 'capt0100.jpg' } 8 | let(:data_and_size) { ['data', 384] } 9 | 10 | before do 11 | allow_any_instance_of(CameraFile).to receive(:new) 12 | allow_any_instance_of(CameraFile).to receive(:data_and_size).and_return(data_and_size) 13 | end 14 | 15 | describe '#preview' do 16 | context 'when a folder and file are set' do 17 | it 'returns false' do 18 | file = CameraFile.new(camera, folder, name) 19 | expect(file.preview?).to be(false) 20 | end 21 | end 22 | 23 | context 'when no folder or file is set' do 24 | it 'returns true' do 25 | file = CameraFile.new(camera) 26 | expect(file.preview?).to be(true) 27 | end 28 | end 29 | end 30 | 31 | describe '#save' do 32 | let(:file) { CameraFile.new(camera, folder, name) } 33 | let(:data) { data_and_size.first } 34 | 35 | before do 36 | allow(File).to receive(:binwrite) 37 | end 38 | 39 | context 'when a pathname is passed' do 40 | it 'saves the data to the passed pathname' do 41 | pathname = '/tmp/capt0100.jpg' 42 | expect(File).to receive(:binwrite).with(pathname, data) 43 | file.save(pathname) 44 | end 45 | end 46 | 47 | context 'when no arguments are passed' do 48 | it 'saves the data to the working directory using file path name' do 49 | expect(File).to receive(:binwrite).with(name, data) 50 | file.save 51 | end 52 | end 53 | end 54 | 55 | describe '#delete' do 56 | it 'delegates the deletion of the file to the camera' do 57 | file = CameraFile.new(camera, folder, name) 58 | expect(camera).to receive(:delete).with(file) 59 | file.delete 60 | end 61 | end 62 | 63 | describe '#data' do 64 | it 'returns the data of the camera file' do 65 | file = CameraFile.new(camera, folder, name) 66 | allow(file).to receive(:data_and_size).and_return(data_and_size) 67 | expect(file.data).to eq(data_and_size.first) 68 | end 69 | end 70 | 71 | describe '#size' do 72 | it 'returns the size of the camera file' do 73 | file = CameraFile.new(camera, folder, name) 74 | allow(file).to receive(:data_and_size).and_return(data_and_size) 75 | expect(file.size).to eq(data_and_size.last) 76 | end 77 | end 78 | 79 | describe '#info' do 80 | context 'the file is a preview' do 81 | it 'returns nil' do 82 | file = CameraFile.new(camera) 83 | expect(file.info).to be(nil) 84 | end 85 | end 86 | 87 | context 'the file is a file' do 88 | it 'it returns an instance of FileCameraFileInfo' do 89 | file = CameraFile.new(camera, folder, name) 90 | allow(file).to receive(:get_info).and_return(FileCameraFileInfo.new(nil)) 91 | expect(file.info).to be_kind_of(FileCameraFileInfo) 92 | end 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_folder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe CameraFolder do 5 | let(:camera) { double('camera') } 6 | 7 | describe '#root?' do 8 | it 'returns true if the folder is the root' do 9 | folder = CameraFolder.new(camera, '/') 10 | expect(folder).to be_root 11 | end 12 | end 13 | 14 | describe '#name' do 15 | context 'when the folder is the root' do 16 | it 'returns /' do 17 | folder = CameraFolder.new(camera, '/') 18 | expect(folder.name).to eq('/') 19 | end 20 | end 21 | 22 | context 'when the folder is not the root' do 23 | it 'returns the current folder name' do 24 | folder = CameraFolder.new(camera, '/store_00010001/DCIM') 25 | expect(folder.name).to eq('DCIM') 26 | end 27 | end 28 | end 29 | 30 | describe '#folders' do 31 | it 'returns a list of subfolders' do 32 | folder = CameraFolder.new(camera) 33 | 34 | folders = 2.times.map { folder } 35 | allow(folder).to receive(:folder_list_folders).and_return(folders) 36 | 37 | expect(folder.folders).to eq(folders) 38 | end 39 | end 40 | 41 | describe '#files' do 42 | it 'returns a list of files in the folder' do 43 | folder = CameraFolder.new(camera) 44 | 45 | file = double('camera_file') 46 | files = 2.times.map { file } 47 | allow(folder).to receive(:folder_list_files).and_return(files) 48 | 49 | expect(folder.files).to eq(files) 50 | end 51 | end 52 | 53 | describe '#cd' do 54 | let(:folder) { CameraFolder.new(camera, '/store_00010001') } 55 | 56 | context 'when passed "."' do 57 | it 'returns self' do 58 | expect(folder.cd('.')).to be(folder) 59 | end 60 | end 61 | 62 | context 'when passed ".."' do 63 | it 'returns the parent folder' do 64 | parent = folder.cd('..') 65 | expect(parent.path).to eq('/') 66 | end 67 | end 68 | 69 | context 'when passed a normal folder name' do 70 | it 'returns a new folder changed to the new path' do 71 | child = folder.cd('DCIM') 72 | expect(child.path).to eq('/store_00010001/DCIM') 73 | end 74 | end 75 | end 76 | 77 | describe '#open' do 78 | it 'returns a new CameraFile of a file in the folder' do 79 | file = double('camera_file') 80 | allow(CameraFile).to receive(:new).and_return(file) 81 | 82 | folder = CameraFolder.new(camera) 83 | expect(folder.open('capt0001.jpg')).to eq(file) 84 | end 85 | end 86 | 87 | describe '#up' do 88 | context 'when the folder is root' do 89 | it 'returns self' do 90 | folder = CameraFolder.new(camera, '/') 91 | expect(folder.up).to be(folder) 92 | end 93 | end 94 | 95 | context 'when the folder is not root' do 96 | it 'returns the parent folder' do 97 | folder = CameraFolder.new(camera, '/store_00010001') 98 | expect(folder.up.path).to eq('/') 99 | end 100 | end 101 | end 102 | 103 | describe '#to_s' do 104 | it 'returns the name of the folder' do 105 | folder = CameraFolder.new(camera, '/store_00010001') 106 | expect(folder.to_s).to eq('store_00010001') 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ffi-gphoto2 2 | 3 | 4 | [![Gem Version](https://badge.fury.io/rb/ffi-gphoto2.svg)](https://badge.fury.io/rb/ffi-gphoto2) 5 | [![Build Status](https://github.com/zaeleus/ffi-gphoto2/actions/workflows/ci.yml/badge.svg)](https://github.com/zaeleus/ffi-gphoto2/actions/workflows/ci.yml) 6 | 7 | **ffi-gphoto2** provides an FFI for common functions in [libgphoto2][gphoto]. 8 | It also includes a facade to interact with the library in a more 9 | idiomatic Ruby way. 10 | 11 | ## Installation 12 | 13 | ### Prerequisites 14 | 15 | * Ruby >= 2.3 16 | * libgphoto2 >= 2.5.2 17 | * libgphoto2_port >= 0.10.1 18 | 19 | ### Gem 20 | 21 | $ gem install ffi-gphoto2 22 | 23 | ## Usage 24 | 25 | ```ruby 26 | require 'gphoto2' 27 | 28 | # list available cameras 29 | cameras = GPhoto2::Camera.all 30 | # => [#, ...] 31 | 32 | # list found cameras by model and port path 33 | cameras.map { |c| [c.model, c.port] } 34 | # => [['Nikon DSC D5100 (PTP mode)', 'usb:250,006'], ...] 35 | 36 | # use the first camera 37 | camera = cameras.first 38 | 39 | # ...or more conveniently 40 | camera = GPhoto2::Camera.first 41 | 42 | # search by model name 43 | camera = GPhoto2::Camera.where(model: /nikon/i).first 44 | 45 | # the above examples require the camera be manually closed when done 46 | camera.close 47 | 48 | # pass a block to automatically close the camera 49 | GPhoto2::Camera.first do |camera| 50 | # ... 51 | end 52 | 53 | # check camera abilities (see `FFI::GPhoto2::CameraOperation.symbols`) 54 | camera.can? :capture_image 55 | # => true 56 | 57 | # list camera configuration names 58 | camera.config.keys 59 | # => ['autofocusdrive', 'manualfocusdrive', 'controlmode', ...] 60 | 61 | # read the current configuration value of an option 62 | camera['expprogram'].value 63 | # => "M" 64 | camera['whitebalance'].value 65 | # => "Automatic" 66 | 67 | # list valid choices of a configuration option 68 | camera['whitebalance'].choices 69 | # => ["Automatic", "Daylight", "Fluorescent", "Tungsten", ...] 70 | 71 | # check if the configuration has changed 72 | camera.dirty? 73 | # => false 74 | 75 | # change camera configuration 76 | camera['iso'] = 800 77 | camera['f-number'] = 'f/4.5' 78 | camera['shutterspeed2'] = '1/30' 79 | 80 | # check if the configuration has changed 81 | camera.dirty? 82 | # => true 83 | 84 | # apply the new configuration to the device 85 | camera.save 86 | 87 | # alternatively, update the camera configuration in one go 88 | camera.update(iso: 200, shutterspeed2: '1/60', 'f-number' => 'f/1.8') 89 | 90 | # take a photo 91 | file = camera.capture 92 | 93 | # ...and save it to the current working directory 94 | file.save 95 | 96 | # ...or to a specific pathname 97 | file.save('/tmp/out.jpg') 98 | 99 | # traverse the camera filesystem 100 | folder = camera/'store_00010001/DCIM/100D5100' 101 | 102 | # list files 103 | files = folder.files 104 | folder.files.map(&:name) 105 | # => ["DSC_0001.JPG", "DSC_0002.JPG", ...] 106 | 107 | # copy a file from the camera 108 | file = files.first 109 | file.save 110 | 111 | # ...and then delete it from the camera 112 | file.delete 113 | ``` 114 | 115 | More examples can be found in [`examples/`][examples]. YARD documentation can be 116 | generated using the `rake yard` task or [browsed online][rubydoc]. 117 | 118 | [gphoto]: http://www.gphoto.org/ 119 | [examples]: https://github.com/zaeleus/ffi-gphoto2/tree/master/examples 120 | [rubydoc]: http://www.rubydoc.info/gems/ffi-gphoto2/frames 121 | -------------------------------------------------------------------------------- /lib/gphoto2/camera_widgets/camera_widget.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | # @abstract 3 | class CameraWidget 4 | include FFI::GPhoto2 5 | include GPhoto2::Struct 6 | 7 | # @param [FFI::GPhoto2::CameraWidget] ptr 8 | # @param [GPhoto2::CameraWidget] parent 9 | def self.factory(ptr, parent = nil) 10 | # ptr fields are supposed to be private, but we ignore that here 11 | type = ptr[:type].to_s.split('_').last.capitalize 12 | klass = GPhoto2.const_get("#{type}CameraWidget") 13 | klass.new(ptr, parent) 14 | end 15 | 16 | # @param [FFI::GPhoto2::CameraWidget] ptr 17 | # @param [GPhoto2::CameraWidget] parent 18 | def initialize(ptr, parent = nil) 19 | @ptr = ptr 20 | @parent = parent 21 | end 22 | 23 | # @return [void] 24 | def finalize 25 | free 26 | end 27 | alias_method :close, :finalize 28 | 29 | # @return [String] 30 | def label 31 | get_label 32 | end 33 | 34 | # @return [String] 35 | def name 36 | get_name 37 | end 38 | 39 | # @return [Object] 40 | def value 41 | get_value 42 | end 43 | 44 | # @return [Object] 45 | def value=(value) 46 | set_value(value) 47 | value 48 | end 49 | 50 | # @return [CameraWidgetType] 51 | def type 52 | get_type 53 | end 54 | 55 | # @return [Array] 56 | def children 57 | count_children.times.map { |i| get_child(i) } 58 | end 59 | 60 | # @param [Hash] map 61 | # @return [Hash] 62 | def flatten(map = {}) 63 | case type 64 | when :window, :section 65 | children.each { |child| child.flatten(map) } 66 | else 67 | map[name] = self 68 | end 69 | 70 | map 71 | end 72 | 73 | # @return [String] 74 | def to_s 75 | value.to_s 76 | end 77 | 78 | protected 79 | 80 | def get_value 81 | raise NotImplementedError 82 | end 83 | 84 | def set_value(value) 85 | raise NotImplementedError 86 | end 87 | 88 | private 89 | 90 | def free 91 | rc = gp_widget_free(ptr) 92 | GPhoto2.check!(rc) 93 | end 94 | 95 | def get_name 96 | str_ptr = FFI::MemoryPointer.new(:pointer) 97 | 98 | rc = gp_widget_get_name(ptr, str_ptr) 99 | GPhoto2.check!(rc) 100 | 101 | str_ptr = str_ptr.read_pointer 102 | str_ptr.null? ? nil : str_ptr.read_string 103 | end 104 | 105 | def get_type 106 | # assume CameraWidgetType is an int 107 | type = FFI::MemoryPointer.new(:int) 108 | rc = gp_widget_get_type(ptr, type) 109 | GPhoto2.check!(rc) 110 | CameraWidgetType[type.read_int] 111 | end 112 | 113 | def get_label 114 | str_ptr = FFI::MemoryPointer.new(:pointer) 115 | 116 | rc = gp_widget_get_label(ptr, str_ptr) 117 | GPhoto2.check!(rc) 118 | 119 | str_ptr = str_ptr.read_pointer 120 | str_ptr.null? ? nil : str_ptr.read_string 121 | end 122 | 123 | def count_children 124 | count = rc = gp_widget_count_children(ptr) 125 | GPhoto2.check!(rc) 126 | count 127 | end 128 | 129 | def get_child(index) 130 | widget_ptr = FFI::MemoryPointer.new(:pointer) 131 | rc = gp_widget_get_child(ptr, index, widget_ptr) 132 | GPhoto2.check!(rc) 133 | widget = FFI::GPhoto2::CameraWidget.new(widget_ptr.read_pointer) 134 | CameraWidget.factory(widget, self) 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/gphoto2/camera/configuration.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class Camera 3 | module Configuration 4 | # @param [String] model 5 | # @param [String] port 6 | def initialize(model, port) 7 | reset 8 | end 9 | 10 | # @return [WindowCameraWidget] 11 | def window 12 | @window ||= get_config 13 | end 14 | 15 | # @example 16 | # # List camera configuration keys. 17 | # camera.config.keys 18 | # # => ['autofocusdrive', 'manualfocusdrive', 'controlmode', ...] 19 | # 20 | # @return [Hash] a flat map of camera 21 | # configuration widgets 22 | # @see #[] 23 | # @see #[]= 24 | def config 25 | @config ||= window.flatten 26 | end 27 | 28 | # Reloads the camera configuration. 29 | # 30 | # All unsaved changes will be lost. 31 | # 32 | # @example 33 | # camera['iso'] 34 | # # => 800 35 | # 36 | # camera['iso'] = 200 37 | # camera.reload 38 | # 39 | # camera['iso'] 40 | # # => 800 41 | # 42 | # @return [void] 43 | def reload 44 | @window.finalize if @window 45 | reset 46 | config 47 | end 48 | 49 | # @example 50 | # camera['whitebalance'].to_s 51 | # # => "Automatic" 52 | # 53 | # @param [#to_s] key 54 | # @return [GPhoto2::CameraWidget] the widget identified by `key` 55 | def [](key) 56 | config[key.to_s] 57 | end 58 | 59 | # Updates the attribute identified by `key` with the specified `value`. 60 | # 61 | # This marks the configuration as "dirty", meaning a call to {#save} is 62 | # needed to actually update the configuration on the camera. 63 | # 64 | # @example 65 | # camera['iso'] = 800 66 | # camera['f-number'] = 'f/2.8' 67 | # camera['shutterspeed2'] = '1/60' 68 | # 69 | # @param [#to_s] key 70 | # @param [Object] value 71 | # @return [Object] 72 | def []=(key, value) 73 | raise ArgumentError, "invalid key: #{key}" unless self[key] 74 | self[key].value = value 75 | @dirty = true 76 | value 77 | end 78 | 79 | # Updates the configuration on the camera. 80 | # 81 | # @example 82 | # camera['iso'] = 800 83 | # camera.save 84 | # # => true 85 | # camera.save 86 | # # => false (nothing to update) 87 | # 88 | # @return [Boolean] whether setting the configuration was attempted 89 | def save 90 | return false unless dirty? 91 | set_config 92 | @dirty = false 93 | true 94 | end 95 | 96 | # Updates the attributes of the camera from the given Hash and saves the 97 | # configuration. 98 | # 99 | # @example 100 | # camera['iso'] # => 800 101 | # camera['shutterspeed2'] # => "1/30" 102 | # 103 | # camera.update(iso: 400, shutterspeed2: '1/60') 104 | # 105 | # camera['iso'] # => 400 106 | # camera['shutterspeed2'] # => "1/60" 107 | # 108 | # @param [Hash] attributes 109 | # @return [Boolean] whether the configuration saved 110 | def update(attributes = {}) 111 | attributes.each do |key, value| 112 | self[key] = value 113 | end 114 | 115 | save 116 | end 117 | 118 | # @example 119 | # camera.dirty? 120 | # # => false 121 | # 122 | # camera['iso'] = 400 123 | # 124 | # camera.dirty? 125 | # # => true 126 | # 127 | # @return [Boolean] whether attributes have been changed 128 | def dirty? 129 | @dirty 130 | end 131 | 132 | private 133 | 134 | def reset 135 | @window = nil 136 | @config = nil 137 | @dirty = false 138 | end 139 | 140 | def get_config 141 | widget_ptr = FFI::MemoryPointer.new(:pointer) 142 | rc = gp_camera_get_config(ptr, widget_ptr, context.ptr) 143 | GPhoto2.check!(rc) 144 | widget = FFI::GPhoto2::CameraWidget.new(widget_ptr.read_pointer) 145 | CameraWidget.factory(widget) 146 | end 147 | 148 | def set_config 149 | rc = gp_camera_set_config(ptr, window.ptr, context.ptr) 150 | GPhoto2.check!(rc) 151 | end 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /spec/gphoto2/camera_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module GPhoto2 4 | describe Camera do 5 | let(:model) { 'Nikon DSC D5100 (PTP mode)' } 6 | let(:port) { 'usb:250,006' } 7 | 8 | it_behaves_like Camera::Capture 9 | it_behaves_like Camera::Configuration 10 | it_behaves_like Camera::Event 11 | it_behaves_like Camera::Filesystem 12 | 13 | describe '.all' do 14 | let(:abilities_list) { double('camera_abilities_list') } 15 | let(:camera_list) { double('camera_list') } 16 | let(:camera) { Camera.new(model, port) } 17 | 18 | before do 19 | allow(CameraAbilitiesList).to receive(:new).and_return(abilities_list) 20 | allow(abilities_list).to receive(:detect).and_return(camera_list) 21 | allow(camera_list).to receive_message_chain(:to_a, :map).and_return([camera]) 22 | end 23 | 24 | it 'returns a list of device entries' do 25 | list = Camera.all 26 | expect(list).to be_kind_of(Array) 27 | expect(list.first).to be_kind_of(Camera) 28 | end 29 | end 30 | 31 | describe '.first' do 32 | context 'when devices are automatically detected' do 33 | let(:camera) { Camera.new(model, port) } 34 | 35 | before do 36 | allow(Camera).to receive(:all).and_return([camera]) 37 | end 38 | 39 | context 'when a block is given' do 40 | it 'yeilds the first detected camera' do 41 | expect(Camera).to receive(:first).and_yield(camera) 42 | Camera.first { |c| } 43 | end 44 | 45 | it 'finalizes the camera when the block terminates' do 46 | expect(camera).to receive(:finalize) 47 | Camera.first { |c| } 48 | end 49 | end 50 | 51 | context 'when no block is given' do 52 | it 'returns a new Camera using the first entry' do 53 | expect(Camera.first).to be_kind_of(Camera) 54 | end 55 | end 56 | end 57 | 58 | context 'when no devices are detected' do 59 | it 'raises a RuntimeError' do 60 | allow(Camera).to receive(:all).and_return([]) 61 | expect { Camera.first }.to raise_error(RuntimeError) 62 | end 63 | end 64 | end 65 | 66 | describe '.open' do 67 | let(:camera) { double('camera') } 68 | before { allow(Camera).to receive(:new).and_return(camera) } 69 | 70 | context 'when a block is given' do 71 | it 'yeilds a new camera instance' do 72 | expect(Camera).to receive(:open).and_yield(camera) 73 | Camera.open(model, port) { |c| } 74 | end 75 | 76 | it 'finalizes the camera when the block terminates' do 77 | expect(camera).to receive(:finalize) 78 | Camera.open(model, port) { |c| } 79 | end 80 | end 81 | 82 | context 'when no block is given' do 83 | it 'returns a new camera instance' do 84 | expect(Camera.open(model, port)).to eq(camera) 85 | end 86 | end 87 | end 88 | 89 | describe '.where' do 90 | let(:cameras) do 91 | cameras = [] 92 | cameras << double('camera', model: 'Canon EOS 5D Mark III', port: 'usb:250,004') 93 | cameras << double('camera', model: 'Nikon DSC D800', port: 'usb:250,005') 94 | cameras << double('camera', model: 'Nikon DSC D5100', port: 'usb:250,006') 95 | cameras 96 | end 97 | 98 | before do 99 | allow(Camera).to receive(:all).and_return(cameras) 100 | end 101 | 102 | it 'filters all detected cameras by model' do 103 | actual = Camera.where(model: /nikon/i) 104 | expected = [cameras[1], cameras[2]] 105 | expect(actual).to match_array(expected) 106 | end 107 | 108 | it 'filters all detected cameras by port' do 109 | actual = Camera.where(port: 'usb:250,004') 110 | expected = [cameras[0]] 111 | expect(actual).to match_array(expected) 112 | end 113 | end 114 | 115 | describe '#exit' do 116 | it 'closes the camera connection' do 117 | camera = Camera.new(model, port) 118 | allow(camera).to receive(:_exit) 119 | expect(camera).to receive(:_exit) 120 | camera.exit 121 | end 122 | end 123 | 124 | describe '#can?' do 125 | let(:camera) { Camera.new(model, port) } 126 | let(:operations) { FFI::GPhoto2::CameraOperation[:capture_image] } 127 | 128 | before do 129 | allow(camera).to receive_message_chain(:abilities, :operations).and_return(operations) 130 | end 131 | 132 | context 'when the camera has the ability to perform an operation' do 133 | it 'returns true' do 134 | expect(camera.can?(:capture_image)).to be(true) 135 | end 136 | end 137 | 138 | context 'when the camera does not have the ability perform an operation' do 139 | it 'returns false' do 140 | expect(camera.can?(:capture_audio)).to be(false) 141 | end 142 | end 143 | 144 | context 'an invalid operation is given' do 145 | it 'returns false' do 146 | expect(camera.can?(:dance)).to be(false) 147 | end 148 | end 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /spec/support/shared_examples/camera/configuration_examples.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | shared_examples_for Camera::Configuration do 3 | describe '#window' do 4 | let(:camera) { Camera.new(model, port) } 5 | let(:window) { double('camera_widget') } 6 | 7 | it 'always returns the same CameraWidget instance' do 8 | allow(camera).to receive(:get_config).and_return(window) 9 | expect(camera.window).to eq(window) 10 | expect(camera.window).to eq(window) 11 | end 12 | end 13 | 14 | describe '#config' do 15 | it 'returns a map of configuration widgets' do 16 | camera = Camera.new(model, port) 17 | window = double('camera_widget') 18 | allow(camera).to receive(:window).and_return(window) 19 | allow(window).to receive(:flatten).and_return({ 'iso' => window }) 20 | 21 | expect(camera.config).to eq({ 'iso' => window }) 22 | end 23 | end 24 | 25 | describe '#reload' do 26 | let(:camera) do 27 | camera = Camera.new(model, port) 28 | window = double('camera_widget') 29 | allow(window).to receive(:flatten) 30 | allow(camera).to receive(:get_config).and_return(window) 31 | camera 32 | end 33 | 34 | it 'reloads the configuration from the camera' do 35 | expect(camera).to receive(:get_config).once 36 | camera.reload 37 | end 38 | 39 | it 'marks the camera as not dirty' do 40 | camera.reload 41 | expect(camera.dirty?).to be(false) 42 | end 43 | end 44 | 45 | describe '#[]' do 46 | let(:window) { double('camera_widget') } 47 | 48 | let(:camera) do 49 | camera = Camera.new(model, port) 50 | allow(camera).to receive(:config).and_return({ 'iso' => window }) 51 | camera 52 | end 53 | 54 | context 'when the specified key exists' do 55 | it 'returns a CameraWidget' do 56 | expect(camera['iso']).to eq(window) 57 | end 58 | end 59 | 60 | context 'when the specified key does not exist' do 61 | it 'returns nil' do 62 | expect(camera['shutterspeed2']).to be_nil 63 | end 64 | end 65 | end 66 | 67 | describe '#[]=' do 68 | let(:camera) { Camera.new(model, port) } 69 | 70 | context 'when the specified key is valid' do 71 | let(:window) { double('camera_widget', :value= => 400) } 72 | 73 | before do 74 | allow(camera).to receive(:[]).and_return(window) 75 | end 76 | 77 | it "sets a widget's value" do 78 | camera['iso'] = 400 79 | end 80 | 81 | it 'marks the camera as dirty' do 82 | camera['iso'] = 400 83 | expect(camera).to be_dirty 84 | end 85 | 86 | it 'returns the passed value' do 87 | expect(camera['iso'] = 400).to eq(400) 88 | end 89 | end 90 | 91 | context 'when the specified key is invalid' do 92 | before do 93 | allow(camera).to receive(:[]).and_return(nil) 94 | end 95 | 96 | it 'raises an ArgumentError' do 97 | expect { camera['flavor'] = 'strawberry' }.to raise_error(ArgumentError) 98 | end 99 | end 100 | end 101 | 102 | describe '#update' do 103 | let(:camera) do 104 | camera = Camera.new(model, port) 105 | allow(camera).to receive(:[]=) 106 | allow(camera).to receive(:save) 107 | camera 108 | end 109 | 110 | it 'sets one or more camera setting' do 111 | expect(camera).to receive(:[]=).exactly(2).times 112 | camera.update('iso' => 400, 'shutterspeed2' => '1/30') 113 | end 114 | 115 | it 'immediately saves the settings to the camera' do 116 | expect(camera).to receive(:save) 117 | camera.update 118 | end 119 | end 120 | 121 | describe '#save' do 122 | let(:camera) do 123 | camera = Camera.new(model, port) 124 | allow(camera).to receive_message_chain(:[], :value=) 125 | allow(camera).to receive(:set_config) 126 | camera 127 | end 128 | 129 | context 'when the camera is marked as dirty' do 130 | it 'returns true' do 131 | camera['iso'] = 400 132 | expect(camera.save).to be(true) 133 | end 134 | 135 | it 'marks the camera as not dirty' do 136 | camera['iso'] = 400 137 | camera.save 138 | expect(camera).not_to be_dirty 139 | end 140 | end 141 | 142 | context 'when the camera is not marked as dirty' do 143 | it 'returns false' do 144 | expect(camera.save).to be(false) 145 | end 146 | end 147 | end 148 | 149 | describe '#dirty?' do 150 | context 'when the configuration changed' do 151 | it 'returns true' do 152 | camera = Camera.new(model, port) 153 | allow(camera).to receive_message_chain(:[], :value=) 154 | 155 | camera['iso'] = 400 156 | 157 | expect(camera).to be_dirty 158 | end 159 | end 160 | 161 | context 'when the configuration has not changed' do 162 | it 'returns false' do 163 | camera = Camera.new(model, port) 164 | expect(camera).not_to be_dirty 165 | end 166 | end 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /lib/ffi/gphoto2.rb: -------------------------------------------------------------------------------- 1 | require 'ffi' 2 | require 'ffi/gphoto2_port' 3 | 4 | module FFI 5 | module GPhoto2 6 | extend FFI::Library 7 | 8 | ffi_lib 'libgphoto2' 9 | 10 | # enums 11 | require 'ffi/gphoto2/camera_capture_type' 12 | require 'ffi/gphoto2/camera_driver_status' 13 | require 'ffi/gphoto2/camera_event_type' 14 | require 'ffi/gphoto2/camera_file_access_type' 15 | require 'ffi/gphoto2/camera_file_info_fields' 16 | require 'ffi/gphoto2/camera_file_operation' 17 | require 'ffi/gphoto2/camera_file_permissions' 18 | require 'ffi/gphoto2/camera_file_status' 19 | require 'ffi/gphoto2/camera_file_type' 20 | require 'ffi/gphoto2/camera_folder_operation' 21 | require 'ffi/gphoto2/camera_operation' 22 | require 'ffi/gphoto2/camera_widget_type' 23 | require 'ffi/gphoto2/gphoto_device_type' 24 | 25 | # structs 26 | require 'ffi/gphoto2/camera' 27 | require 'ffi/gphoto2/camera_abilities' 28 | require 'ffi/gphoto2/camera_abilities_list' 29 | require 'ffi/gphoto2/camera_file' 30 | require 'ffi/gphoto2/camera_file_info_audio' 31 | require 'ffi/gphoto2/camera_file_info_file' 32 | require 'ffi/gphoto2/camera_file_info_preview' 33 | require 'ffi/gphoto2/camera_file_info' 34 | require 'ffi/gphoto2/camera_file_path' 35 | require 'ffi/gphoto2/entry' 36 | require 'ffi/gphoto2/camera_list' 37 | require 'ffi/gphoto2/camera_widget' 38 | require 'ffi/gphoto2/gp_context' 39 | 40 | # gphoto2/gphoto2-abilities-list.h 41 | attach_function :gp_abilities_list_new, [:pointer], :int 42 | attach_function :gp_abilities_list_free, [:pointer], :int 43 | attach_function :gp_abilities_list_load, [CameraAbilitiesList.by_ref, GPContext.by_ref], :int 44 | attach_function :gp_abilities_list_detect, [CameraAbilitiesList.by_ref, GPhoto2Port::GPPortInfoList.by_ref, CameraList.by_ref, GPContext.by_ref], :int 45 | attach_function :gp_abilities_list_lookup_model, [CameraAbilitiesList.by_ref, :string], :int 46 | attach_function :gp_abilities_list_get_abilities, [CameraAbilitiesList.by_ref, :int, CameraAbilities.by_ref], :int 47 | 48 | # gphoto2/gphoto2-camera.h 49 | attach_function :gp_camera_new, [:pointer], :int 50 | attach_function :gp_camera_set_abilities, [Camera.by_ref, CameraAbilities.by_value], :int 51 | attach_function :gp_camera_set_port_info, [Camera.by_ref, GPhoto2Port::GPPortInfo], :int 52 | attach_function :gp_camera_exit, [Camera.by_ref, GPContext.by_ref], :int 53 | attach_function :gp_camera_ref, [Camera.by_ref], :int 54 | attach_function :gp_camera_unref, [Camera.by_ref], :int 55 | attach_function :gp_camera_get_config, [Camera.by_ref, :pointer, GPContext.by_ref], :int 56 | attach_function :gp_camera_set_config, [Camera.by_ref, CameraWidget.by_ref, GPContext.by_ref], :int 57 | attach_function :gp_camera_capture, [Camera.by_ref, CameraCaptureType, CameraFilePath.by_ref, GPContext.by_ref], :int, blocking: true 58 | attach_function :gp_camera_trigger_capture, [Camera.by_ref, GPContext.by_ref], :int, blocking: true 59 | attach_function :gp_camera_capture_preview, [Camera.by_ref, CameraFile.by_ref, GPContext.by_ref], :int, blocking: true 60 | attach_function :gp_camera_wait_for_event, [Camera.by_ref, :int, :pointer, :pointer, GPContext.by_ref], :int, blocking: true 61 | attach_function :gp_camera_folder_list_files, [Camera.by_ref, :string, CameraList.by_ref, GPContext.by_ref], :int 62 | attach_function :gp_camera_folder_list_folders, [Camera.by_ref, :string, CameraList.by_ref, GPContext.by_ref], :int 63 | attach_function :gp_camera_file_get_info, [Camera.by_ref, :string, :string, CameraFileInfo.by_ref, GPContext.by_ref], :int 64 | attach_function :gp_camera_file_get, [Camera.by_ref, :string, :string, CameraFileType, CameraFile.by_ref, GPContext.by_ref], :int, blocking: true 65 | attach_function :gp_camera_file_delete, [Camera.by_ref, :string, :string, GPContext.by_ref], :int 66 | 67 | # gphoto2/gphoto2-context.h 68 | attach_function :gp_context_new, [], :pointer 69 | attach_function :gp_context_ref, [GPContext.by_ref], :void 70 | attach_function :gp_context_unref, [GPContext.by_ref], :void 71 | 72 | # gphoto2/gphoto2-file.h 73 | attach_function :gp_file_new, [:pointer], :int 74 | attach_function :gp_file_free, [:pointer], :int 75 | attach_function :gp_file_get_data_and_size, [CameraFile.by_ref, :pointer, :pointer], :int 76 | 77 | # gphoto2/gphoto2-list.h 78 | attach_function :gp_list_new, [:pointer], :int 79 | attach_function :gp_list_free, [:pointer], :int 80 | attach_function :gp_list_count, [CameraList.by_ref], :int 81 | attach_function :gp_list_get_name, [CameraList.by_ref, :int, :pointer], :int 82 | attach_function :gp_list_get_value, [CameraList.by_ref, :int, :pointer], :int 83 | 84 | # gphoto2/gphoto2-widget.h 85 | attach_function :gp_widget_free, [CameraWidget.by_ref], :int 86 | attach_function :gp_widget_count_children, [CameraWidget.by_ref], :int 87 | attach_function :gp_widget_get_child, [CameraWidget.by_ref, :int, :pointer], :int 88 | attach_function :gp_widget_set_value, [CameraWidget.by_ref, :pointer], :int 89 | attach_function :gp_widget_get_value, [CameraWidget.by_ref, :pointer], :int 90 | attach_function :gp_widget_get_name, [CameraWidget.by_ref, :pointer], :int 91 | attach_function :gp_widget_get_type, [CameraWidget.by_ref, :pointer], :int 92 | attach_function :gp_widget_get_label, [CameraWidget.by_ref, :pointer], :int 93 | attach_function :gp_widget_get_range, [CameraWidget.by_ref, :pointer, :pointer, :pointer], :int 94 | attach_function :gp_widget_count_choices, [CameraWidget.by_ref], :int 95 | attach_function :gp_widget_get_choice, [CameraWidget.by_ref, :int, :pointer], :int 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/gphoto2/camera.rb: -------------------------------------------------------------------------------- 1 | module GPhoto2 2 | class Camera 3 | include FFI::GPhoto2 4 | include GPhoto2::Struct 5 | 6 | include Capture 7 | include Configuration 8 | include Event 9 | include Filesystem 10 | 11 | # @return [String] 12 | attr_reader :model 13 | 14 | # @return [String] 15 | attr_reader :port 16 | 17 | # @example 18 | # cameras = GPhoto2::Camera.all 19 | # # => [#, #, ...] 20 | # 21 | # @return [Array] a list of all available devices 22 | def self.all 23 | context = Context.new 24 | 25 | abilities = CameraAbilitiesList.new(context) 26 | cameras = abilities.detect 27 | 28 | entries = cameras.to_a.map do |entry| 29 | model, port = entry.name, entry.value 30 | Camera.new(model, port) 31 | end 32 | 33 | context.finalize 34 | 35 | entries 36 | end 37 | 38 | # @example 39 | # camera = GPhoto2::Camera.first 40 | # 41 | # begin 42 | # # ... 43 | # ensure 44 | # camera.finalize 45 | # end 46 | # 47 | # # Alternatively, pass a block, which will automatically close the camera. 48 | # GPhoto2::Camera.first do |camera| 49 | # # ... 50 | # end 51 | # 52 | # @return [GPhoto2::Camera] the first detected camera 53 | # @raise [RuntimeError] when no devices are detected 54 | def self.first(&blk) 55 | entries = all 56 | raise RuntimeError, 'no devices detected' if entries.empty? 57 | camera = entries.first 58 | autorelease(camera, &blk) 59 | end 60 | 61 | # @example 62 | # model = 'Nikon DSC D5100 (PTP mode)' 63 | # port = 'usb:250,006' 64 | # 65 | # camera = GPhoto2::Camera.open(model, port) 66 | # 67 | # begin 68 | # # ... 69 | # ensure 70 | # camera.finalize 71 | # end 72 | # 73 | # # Alternatively, pass a block, which will automatically close the camera. 74 | # GPhoto2::Camera.open(model, port) do |camera| 75 | # # ... 76 | # end 77 | # 78 | # @param [String] model 79 | # @param [String] port 80 | # @return [GPhoto2::Camera] 81 | def self.open(model, port, &blk) 82 | camera = new(model, port) 83 | autorelease(camera, &blk) 84 | end 85 | 86 | # Filters devices by a given condition. 87 | # 88 | # Filter keys can be either `model` or `port`. Only the first filter is 89 | # used. 90 | # 91 | # @example 92 | # # Find the cameras whose model names contain Nikon. 93 | # cameras = GPhoto2::Camera.where(model: /nikon/i) 94 | # 95 | # # Select a camera by its port. 96 | # camera = GPhoto2::Camera.where(port: 'usb:250,004').first 97 | # 98 | # @param [Hash] condition 99 | # @return [Array] 100 | def self.where(condition) 101 | name = condition.keys.first 102 | pattern = condition.values.first 103 | all.select { |c| c.send(name).match(pattern) } 104 | end 105 | 106 | # @param [String] model 107 | # @param [String] port 108 | def initialize(model, port) 109 | super 110 | @model, @port = model, port 111 | end 112 | 113 | # @return [void] 114 | def finalize 115 | @context.finalize if @context 116 | @window.finalize if @window 117 | unref if @ptr 118 | end 119 | alias_method :close, :finalize 120 | 121 | # @return [void] 122 | def exit 123 | _exit 124 | end 125 | 126 | # @return [FFI::GPhoto::Camera] 127 | def ptr 128 | @ptr || (init && @ptr) 129 | end 130 | 131 | # @return [GPhoto2::CameraAbilities] 132 | def abilities 133 | @abilities || (init && @abilities) 134 | end 135 | 136 | # @return [GPhoto2::PortInfo] 137 | def port_info 138 | @port_info || (init && @port_info) 139 | end 140 | 141 | # @return [GPhoto2::Context] 142 | def context 143 | @context ||= Context.new 144 | end 145 | 146 | # @example 147 | # camera.can? :capture_image 148 | # # => true 149 | # 150 | # @see FFI::GPhoto2::CameraOperation 151 | # @param [CameraOperation] operation 152 | # @return [Boolean] 153 | def can?(operation) 154 | (abilities.operations & (CameraOperation[operation] || 0)) != 0 155 | end 156 | 157 | private 158 | 159 | # Ensures the given camera is finalized when passed a block. 160 | # 161 | # If no block is given, the camera is returned and the caller must must 162 | # manually close it. 163 | def self.autorelease(camera) 164 | if block_given? 165 | begin 166 | yield camera 167 | ensure 168 | camera.finalize 169 | end 170 | else 171 | camera 172 | end 173 | end 174 | 175 | def init 176 | new 177 | set_abilities(CameraAbilities.find(@model)) 178 | set_port_info(PortInfo.find(@port)) 179 | end 180 | 181 | def new 182 | ptr = FFI::MemoryPointer.new(:pointer) 183 | rc = gp_camera_new(ptr) 184 | GPhoto2.check!(rc) 185 | @ptr = FFI::GPhoto2::Camera.new(ptr.read_pointer) 186 | end 187 | 188 | def _exit 189 | rc = gp_camera_exit(ptr, context.ptr) 190 | GPhoto2.check!(rc) 191 | end 192 | 193 | def set_port_info(port_info) 194 | rc = gp_camera_set_port_info(ptr, port_info.ptr) 195 | GPhoto2.check!(rc) 196 | @port_info = port_info 197 | end 198 | 199 | def set_abilities(abilities) 200 | rc = gp_camera_set_abilities(ptr, abilities.ptr) 201 | GPhoto2.check!(rc) 202 | @abilities = abilities 203 | end 204 | 205 | def unref 206 | rc = gp_camera_unref(ptr) 207 | GPhoto2.check!(rc) 208 | end 209 | end 210 | end 211 | --------------------------------------------------------------------------------