16 июня 2009, 21:07
Управление фотокамерой с помощью руби
Темы: ruby, extension
Введение
По заказу neq4 в рамках одного из проектов сделал библиотеку для работы с камерами. Точнее, это оболочка вокруг библиотеки на C (libgphoto2) для использования в руби.
Пришлось, конечно, изучать тему расширения руби с помощью C, но сегодня я хотел написать не об этом, а наоборот о том, как пользоваться библиотекой на руби. Что называется, программировал на C, чтобы другим не пришлось :)
Оболочка собрана в виде gem и работает на Linux и Mac OS. То есть везде, где работает gphoto2.
Установка
Для того, чтобы собрать gem, кроме средств сборки и версии руби для разработчиков, нужна установденная версия libgphoto2 для разработчиков:
sudo apt-get install ruby-dev build-essential libgphoto2-dev
Теперь можно ставить gem:
sudo gem i gphoto4ruby
Инициализация и настройки камеры
О том, какие камеры совместимы, и как их подключать для работы достаточно много написано у создателей gphoto2. Скажу лишь, что те камеры, с которыми я работал, для подключения нужно было переводить в PTP-mode. После подключения:
require "rubygems"
require "gphoto4ruby"
c = GPhoto2::Camera.new
Чтобы получить текущее значение настройки камеры и управлять состоянием конкретной настройки, нужно проделать примерно следующее.
cfg = c.config #{"capturemode" => "Single Shot",
# "exptime" => "0.125",
# "f-number" => "f/3.2", ...}
cfg.keys # возможные настройки
c["f-number", :all] # ["f/2.8", "f/3.2", "f/3.5", ...]
# возможные значения
c["f-number"] # текущее значение
c["f-number"] = "f/8" # вуаля! настройка на камере изменена
Не все настройки, имеющие несколько возможных значений, можно переключить с компьютера. Нельзя переключить то, для чего нужно что-то поворачивать или открывать на камере. Если вы что-то поменяли вручную на камере, то чтобы увидеть изменения параметра в объекте программы, нужно написать, например:
c["focusmode", :no_cache] # на Nikon D80 меняется переключателем
Съёмка
c.capture
c.capture.save.delete # сохранить снятую фотографию в текущую
# папку и удалить её с камеры
c.save :file => :last, :to_folder => "/home/sweet/home"
# сохранить последний из файлов в папку
c.save :file => c.files[3], :type => :preview, :new_name => "PREVIEW.JPG"
# загрузить превью фотографии под номером 4
# в списке файлов на карточке
При сохранении, если вы не делали съемку, могут быть тонкости связанные с тем, что нужно перейти в камере в нужную папку. Для этого есть ряд методов. Они хорошо описаны в документации по ссылке ниже.
Материалы для самостоятельного изучения
Библиотека libgphoto2 и инструмент для работы с камерами из командной строки gphoto2: есть FAQ.
Документация оболочки gphoto4ruby: полный список классов и методов с примерами.
Исходный код gphoto4ruby
Расширение руби с помощью C
09 июня 2009, 00:51
Ruby daemon, или как сделать демона на руби
Темы: daemon, ruby
Задача
Иногда в процессе работы с разными сторонними библиотеками, которые содержат тяжелые блокирующие методы, не обойтись обычным Thread. В таком случае на помощь приходят демоны :) Вот несколько приемов, которые я освоил в работе с нимим.
Базовый механизм
Запустить демона можно с помощью метода Kernel.fork, передав ему блок. Метод возвращает pid процесса, который можно записать в файл и просто использовать в дальнейшем.
pid = fork do
puts "from daemon"
exit!(1)
end
Определение статуса
Я не нашёл удобного способа определения методами руби, запущен ли процесс. Есть возможность фильтровать вывод ps ax, ища в нём pid процесса. Но есть метод Process.waitpid, который можно использовать хитрым образом. Так же для будущих задач, упакуем наш код в класс:
require 'timeout'
class Daemon
class << self
def start
@pid = fork do
puts "from daemon"
sleep 1
exit!(1)
end
end
def running?
if @pid
begin
Timeout::timeout(0.01) do
Process.waitpid(@pid)
if $?.exited?
return false
end
end
rescue Timeout::Error
end
return true
else
return false
end
end
end
end
Daemon.start
puts "running: #{Daemon.running?}"
sleep 1.5
puts "running: #{Daemon.running?}"
Сообщение об ошибках
Предположим, что у нашего демона есть некоторый процесс инициализации, и мы хотим знать, завершился ли он или произошла ошибка до того, как покинем метод Daemon.start.
Для передачи подобных сообщений хорошо подходит IO.pipe. Для двустороннего общения нужно создавать два канала, но нам хватит и одного:
require 'timeout'
class Daemon
def initialize
puts "from daemon: initializing"
end
class << self
def start
@rd, @wr = IO.pipe
@pid = fork do
@rd.close
begin
dmn = new
@wr.write "ok"
@wr.close
sleep 1
rescue Exception => e
@wr.write e.to_s
@wr.close
ensure
exit!(1)
end
end
@wr.close
str = @rd.read
if str == "ok"
puts "daemon started ok"
else
puts "error while initializing daemon: #{str}"
end
@rd.close
end
def running?
if @pid
begin
Timeout::timeout(0.01) do
Process.waitpid(@pid)
if $?.exited?
return false
end
end
rescue Timeout::Error
end
return true
else
return false
end
end
end
end
Daemon.start
puts "running: #{Daemon.running?}"
sleep 1.5
puts "running: #{Daemon.running?}"
Остановка
У каждого демона (для того они обычно и создаются) есть циклическая часть. Нам хотелось бы запускать и останавливать процесс тогда, когда нам нужно. Если с запуском всё понятно, то для остановки потребуется ещё одна вещь. Ведь в тот момент, когда мы создали демона, он создает копии всех переменных и дальнейшее их изменение внутри и снаружи демона становится независимым. Ещё один способ общения с демоном сигнал:
require 'timeout'
class Daemon
def initialize
puts "from daemon: initializing"
@cnt = 0
end
def main_loop
@cnt += 1
puts "from daemon: running loop ##{@cnt}"
sleep 0.1
end
class << self
def start
@rd, @wr = IO.pipe
@pid = fork do
@rd.close
running = true
Signal.trap("TERM") do
running = false
end
begin
dmn = new
@wr.write "ok"
@wr.close
while running
dmn.main_loop
end
rescue Exception => e
@wr.write e.to_s
@wr.close
ensure
exit!(1)
end
end
@wr.close
str = @rd.read
if str == "ok"
puts "daemon started ok"
else
puts "error while initializing daemon: #{str}"
end
@rd.close
end
def stop
unless @pid.nil?
Process.kill("TERM", @pid)
@pid = nil
end
end
def running?
if @pid
begin
Timeout::timeout(0.01) do
Process.waitpid(@pid)
if $?.exited?
return false
end
end
rescue Timeout::Error
end
return true
else
return false
end
end
end
end
Daemon.start
puts "running: #{Daemon.running?}"
sleep 1
Daemon.stop
puts "running: #{Daemon.running?}"
Теперь, если наследовать от этого класса свой класс и переопределить методы initialize и main_loop, получится вполне себе демон :)
Для самостоятельного изучения
Есть, конечно, ещё недостатки. Например:- Если ошибка возникает в main_loop, то канал вывода уже закрыт. А если не закрывать канал, то метод IO#read не позволит нам выйти из метода start. Что делать?
- Если нужно периодически общаться с демоном общирными объемами информации, что делать? (я в своей задаче использовал TCPSocket, но это, ведь, не панацея)
- Хорошо бы хранить pid в pid-файле на случай нашествия зомби. И, соответственно, обрабатывать возникающие зомбо-проблемы.
Но это я оставлю на самостоятельное решение пытливым читателям.