lib/launchpad/interaction.rb in launchpad-0.2.2 vs lib/launchpad/interaction.rb in launchpad-0.3.0
- old
+ new
@@ -1,14 +1,14 @@
require 'launchpad/device'
+require 'launchpad/logging'
module Launchpad
# This class provides advanced interaction features.
#
# Example:
#
- # require 'rubygems'
# require 'launchpad'
#
# interaction = Launchpad::Interaction.new
# interaction.response_to(:grid, :down) do |interaction, action|
# interaction.device.change(:grid, action.merge(:red => :high))
@@ -17,10 +17,12 @@
# interaction.stop
# end
#
# interaction.start
class Interaction
+
+ include Logging
# Returns the Launchpad::Device the Launchpad::Interaction acts on.
attr_reader :device
# Returns whether the Launchpad::Interaction is active or not.
@@ -38,29 +40,50 @@
# optional, <tt>:device_name</tt> will be used if omitted
# [<tt>:device_name</tt>] Name of the MIDI device to use,
# optional, defaults to "Launchpad"
# [<tt>:latency</tt>] delay (in s, fractions allowed) between MIDI pulls,
# optional, defaults to 0.001 (1ms)
+ # [<tt>:logger</tt>] [Logger] to be used by this interaction instance, can be changed afterwards
#
# Errors raised:
#
# [Launchpad::NoSuchDeviceError] when device with ID or name specified does not exist
# [Launchpad::DeviceBusyError] when device with ID or name specified is busy
def initialize(opts = nil)
opts ||= {}
- @device = opts[:device] || Device.new(opts.merge(:input => true, :output => true))
+
+ self.logger = opts[:logger]
+ logger.debug "initializing Launchpad::Interaction##{object_id} with #{opts.inspect}"
+
+ @device = opts[:device]
+ @device ||= Device.new(opts.merge(
+ :input => true,
+ :output => true,
+ :logger => opts[:logger]
+ ))
@latency = (opts[:latency] || 0.001).to_f.abs
@active = false
+
+ @action_threads = ThreadGroup.new
end
-
+
+ # Sets the logger to be used by the current instance and the device.
+ #
+ # [+logger+] the [Logger] instance
+ def logger=(logger)
+ @logger = logger
+ @device.logger = logger if @device
+ end
+
# Closes the interaction's device - nothing can be done with the interaction/device afterwards.
#
# Errors raised:
#
# [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device
# [Launchpad::CommunicationError] when anything unexpected happens while communicating with the
def close
+ logger.debug "closing Launchpad::Interaction##{object_id}"
stop
@device.close
end
# Determines whether this interaction's device has been closed.
@@ -81,22 +104,35 @@
#
# [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device
# [Launchpad::NoOutputAllowedError] when output is not enabled on the interaction's device
# [Launchpad::CommunicationError] when anything unexpected happens while communicating with the launchpad
def start(opts = nil)
+ logger.debug "starting Launchpad::Interaction##{object_id}"
+
opts = {
:detached => false
}.merge(opts || {})
+
@active = true
+
@reader_thread ||= Thread.new do
begin
while @active do
- @device.read_pending_actions.each {|action| Thread.new {respond_to_action(action)}}
- sleep @latency unless @latency <= 0
+ @device.read_pending_actions.each do |action|
+ action_thread = Thread.new(action) do |action|
+ respond_to_action(action)
+ end
+ @action_threads.add(action_thread)
+ end
+ sleep @latency# if @latency > 0.0
end
rescue Portmidi::DeviceError => e
+ logger.fatal "could not read from device, stopping to read actions"
raise CommunicationError.new(e)
+ rescue Exception => e
+ logger.fatal "error causing action reading to stop: #{e.inspect}"
+ raise e
ensure
@device.reset
end
end
@reader_thread.join unless opts[:detached]
@@ -107,17 +143,27 @@
# Errors raised:
#
# [Launchpad::NoInputAllowedError] when input is not enabled on the interaction's device
# [Launchpad::CommunicationError] when anything unexpected happens while communicating with the
def stop
+ logger.debug "stopping Launchpad::Interaction##{object_id}"
@active = false
if @reader_thread
# run (resume from sleep) and wait for @reader_thread to end
@reader_thread.run if @reader_thread.alive?
@reader_thread.join
@reader_thread = nil
end
+ ensure
+ @action_threads.list.each do |thread|
+ begin
+ thread.kill
+ thread.join
+ rescue Exception => e
+ logger.error "error when killing action thread: #{e.inspect}"
+ end
+ end
nil
end
# Registers a response to one or more actions.
#
@@ -131,23 +177,32 @@
# Optional options hash:
#
# [<tt>:exclusive</tt>] <tt>true/false</tt>,
# whether to deregister all other responses to the specified actions,
# optional, defaults to +false+
+ # [<tt>:x</tt>] x coordinate(s), can contain arrays and ranges, when specified
+ # without y coordinate, it's interpreted as a whole column
+ # [<tt>:y</tt>] y coordinate(s), can contain arrays and ranges, when specified
+ # without x coordinate, it's interpreted as a whole row
#
# Takes a block which will be called when an action matching the parameters occurs.
#
# Block parameters:
#
# [+interaction+] the interaction object that received the action
# [+action+] the action received from Launchpad::Device.read_pending_actions
def response_to(types = :all, state = :both, opts = nil, &block)
+ logger.debug "setting response to #{types.inspect} for state #{state.inspect} with #{opts.inspect}"
types = Array(types)
opts ||= {}
no_response_to(types, state) if opts[:exclusive] == true
Array(state == :both ? %w(down up) : state).each do |state|
- types.each {|type| responses[type.to_sym][state.to_sym] << block}
+ types.each do |type|
+ combined_types(type, opts).each do |combined_type|
+ responses[combined_type][state.to_sym] << block
+ end
+ end
end
nil
end
# Deregisters all responses to one or more actions.
@@ -158,14 +213,26 @@
# additional value <tt>:all</tt> for actions on all buttons
# (but not meaning "all responses"),
# optional, defaults to +nil+, meaning "all responses"
# [+state+] button state to respond to,
# additional value <tt>:both</tt>
- def no_response_to(types = nil, state = :both)
+ #
+ # Optional options hash:
+ #
+ # [<tt>:x</tt>] x coordinate(s), can contain arrays and ranges, when specified
+ # without y coordinate, it's interpreted as a whole column
+ # [<tt>:y</tt>] y coordinate(s), can contain arrays and ranges, when specified
+ # without x coordinate, it's interpreted as a whole row
+ def no_response_to(types = nil, state = :both, opts = nil)
+ logger.debug "removing response to #{types.inspect} for state #{state.inspect}"
types = Array(types)
Array(state == :both ? %w(down up) : state).each do |state|
- types.each {|type| responses[type.to_sym][state.to_sym].clear}
+ types.each do |type|
+ combined_types(type, opts).each do |combined_type|
+ responses[combined_type][state.to_sym].clear
+ end
+ end
end
nil
end
# Responds to an action by executing all matching responses, effectively simulating
@@ -189,20 +256,77 @@
# Returns the hash storing all responses. Keys are button types, values are
# hashes themselves, keys are <tt>:down/:up</tt>, values are arrays of responses.
def responses
@responses ||= Hash.new {|hash, key| hash[key] = {:down => [], :up => []}}
end
+
+ # Returns an array of grid positions for a range.
+ #
+ # Parameters:
+ #
+ # [+range+] the range definitions, can be
+ # * a Fixnum
+ # * a Range
+ # * an Array of Fixnum, Range or Array objects
+ def grid_range(range)
+ return nil if range.nil?
+ Array(range).flatten.map do |pos|
+ pos.respond_to?(:to_a) ? pos.to_a : pos
+ end.flatten.uniq
+ end
+
+ # Returns a list of combined types for the type and opts specified. Combined
+ # types are just the type, except for grid, where the opts are interpreted
+ # and all combinations of x and y coordinates are added as a position suffix.
+ #
+ # Example:
+ #
+ # combined_types(:grid, :x => 1..2, y => 2) => [:grid12, :grid22]
+ #
+ # Parameters (see Launchpad for values):
+ #
+ # [+type+] type of the button
+ #
+ # Optional options hash:
+ #
+ # [<tt>:x</tt>] x coordinate(s), can contain arrays and ranges, when specified
+ # without y coordinate, it's interpreted as a whole column
+ # [<tt>:y</tt>] y coordinate(s), can contain arrays and ranges, when specified
+ # without x coordinate, it's interpreted as a whole row
+ def combined_types(type, opts = nil)
+ if type.to_sym == :grid && opts
+ x = grid_range(opts[:x])
+ y = grid_range(opts[:y])
+ return [:grid] if x.nil? && y.nil? # whole grid
+ x ||= ['-'] # whole row
+ y ||= ['-'] # whole column
+ x.product(y).map {|x, y| :"grid#{x}#{y}"}
+ else
+ [type.to_sym]
+ end
+ end
# Reponds to an action by executing all matching responses.
#
# Parameters:
#
# [+action+] hash containing an action from Launchpad::Device.read_pending_actions
def respond_to_action(action)
type = action[:type].to_sym
state = action[:state].to_sym
- (responses[type][state] + responses[:all][state]).each {|block| block.call(self, action)}
+ actions = []
+ if type == :grid
+ actions += responses[:"grid#{action[:x]}#{action[:y]}"][state]
+ actions += responses[:"grid#{action[:x]}-"][state]
+ actions += responses[:"grid-#{action[:y]}"][state]
+ end
+ actions += responses[type][state]
+ actions += responses[:all][state]
+ actions.compact.each {|block| block.call(self, action)}
nil
+ rescue Exception => e
+ logger.error "error when responding to action #{action.inspect}: #{e.inspect}"
+ raise e
end
end
end
\ No newline at end of file