module Roby::Models::Task
Public Class Methods
# File lib/roby/models/task.rb, line 285 def self.model_relation(name) model_attribute_list(name) end
Public Instance Methods
Declare that this task model defines abstract tasks. Abstract tasks can be used to represent an action, without specifically representing how this action should be done.
Instances of abstract task models are not executable, i.e. they cannot be started.
@see abstract? executable?
# File lib/roby/models/task.rb, line 461 def abstract @abstract = true end
@deprecated
Use each_submodel instead
# File lib/roby/models/task.rb, line 221 def all_models submodels end
Default implementation of the as_plan
method
The as_plan
method is used to use task models as representation of abstract actions. For instance, if an as_plan
method is available on a particular MoveTo task model, one can do
root.depends_on(MoveTo)
This default implementation looks for planning methods declared in the main Roby
application planners that return the required task type or one of its subclasses. If one is found, it is using it to generate the action. Otherwise, it falls back to returning a new instance of this task model, unless the model is abstract in which case it raises ArgumentError.
It can be used with
class TaskModel < Roby::Task end root = Roby::Task.new child = root.depends_on(TaskModel)
If arguments need to be given, the with_arguments
method should be used:
root = Roby::Task.new child = root.depends_on(TaskModel.with_arguments(id: 200))
# File lib/roby/models/task.rb, line 208 def as_plan(arguments = Hash.new) Roby.app.prepare_action(self, **arguments).first rescue Application::ActionResolutionError if abstract? raise Application::ActionResolutionError, "#{self} is abstract and no planning method exists that returns it" else new(arguments) end end
# File lib/roby/models/task.rb, line 826 def can_merge?(target_model) fullfills?(target_model) end
Establish model-level causal links between events of that task. These signals will be established on all the instances of this task model (and its subclasses).
Causal links are used during event propagation to order the propagation properly. Establish a causal link when e.g. an event handler might call or emit on another of this task's event
@param [Hash<Symbol,Array<Symbol>>,Hash<Symbol,Symbol>] mappings the source-to-target mappings
@example when establishing multiple relations from the same source use name-to-arrays
signal start: [:one, :two]
# File lib/roby/models/task.rb, line 353 def causal_link(mappings) mappings.each do |from, to| from = event_model(from).symbol causal_link_sets[from].merge Array[*to].map { |ev| event_model(ev).symbol } end update_terminal_flag end
Clears all definitions saved in this model. This is to be used by the reloading code
# File lib/roby/models/task.rb, line 227 def clear_model class_eval do # Remove event models events.each_key do |ev_symbol| remove_const ev_symbol.to_s.camelcase(:upper) end [@events, @signal_sets, @forwarding_sets, @causal_link_sets, @arguments, @handler_sets, @precondition_sets].each do |set| set.clear if set end end super end
# File lib/roby/models/task.rb, line 147 def compute_terminal_events(events) success_events, failure_events, terminal_events = [events[:success]].to_set, [events[:failed]].to_set, [events[:stop], events[:success], events[:failed]].to_set event_set = events.values.to_set discover_terminal_events(event_set, terminal_events, success_events, events[:success]) discover_terminal_events(event_set, terminal_events, failure_events, events[:failed]) discover_terminal_events(event_set, terminal_events, nil, events[:stop]) events.each_value do |ev| if ev.event_model.terminal? if !success_events.include?(ev) && !failure_events.include?(ev) terminal_events << ev end end end return terminal_events, success_events, failure_events end
@api private
Define the method that will be used as command for the given event
@param [Symbol] event_name the event name
# File lib/roby/models/task.rb, line 565 def define_command_method(event_name, block) check_arity(block, 1, strict: true) define_method("event_command_#{event_name}", &block) method = instance_method("event_command_#{event_name}") lambda do |dst_task, *event_context| method.bind(dst_task).call(*event_context) end end
@api private
Define support methods for a task event
@param [Symbol] event_name the event name
# File lib/roby/models/task.rb, line 579 def define_event_methods(event_name) event_name = event_name.to_sym if !method_defined?("#{event_name}_event") define_method("#{event_name}_event") do @bound_events[event_name] || event(event_name) end end if !method_defined?("#{event_name}?") define_method("#{event_name}?") do (@bound_events[event_name] || event(event_name)).emitted? end end if !method_defined?("#{event_name}!") define_method("#{event_name}!") do |*context| (@bound_events[event_name] || event(event_name)).call(*context) end end if !respond_to?("#{event_name}_event") singleton_class.class_eval do define_method("#{event_name}_event") do find_event_model(event_name) end end end end
# File lib/roby/models/task.rb, line 127 def discover_terminal_events(events, terminal_set, set, root) stack = [root] while !stack.empty? vertex = stack.shift for relation in [EventStructure::Signal, EventStructure::Forwarding] for parent in vertex.parent_objects(relation) if !events.include?(parent) next elsif parent[vertex, relation] next elsif !terminal_set.include?(parent) terminal_set << parent set << parent if set stack << parent end end end end end
# File lib/roby/models/task.rb, line 640 def enum_events # :nodoc Roby.warn_deprecated "#enum_events is deprecated, use #each_event without a block instead" each_event end
Defines a new event on this task.
@param [Symbol] event_name the event name @param [Hash] options an option hash @option options [Boolean] :controllable if true, the event is
controllable and will use the default command of emitting directly in the command
@option options [Boolean] :terminal if true, the event is marked as
terminal, i.e. it will terminate the task upon emission. Giving this flag is required to redeclare an existing terminal event in a subclass. Otherwise, it is determined automatically by checking whether the event is forwarded to :stop
@option options [Class] :model the base class used to create the
model for this event. This class is going to be used to generate the event. Defaults to TaskEvent.
When a task event (for instance start
) is emitted, a Roby::TaskEvent
object is created to describe the information related to this emission (time, sources, context information, …). Task.event
defines a specific event model MyTask::MyEvent for each task event with name :my_event. This specific model is by default a subclass of Roby::TaskEvent
, but it is possible to override that by using the model
option.
# File lib/roby/models/task.rb, line 524 def event(event_name, options = Hash.new, &block) event_name = event_name.to_sym options = validate_options options, controlable: nil, command: nil, terminal: nil, model: find_event_model(event_name) || Roby::TaskEvent if options.has_key?(:controlable) options[:command] = options[:controlable] elsif !options.has_key?(:command) && block options[:command] = define_command_method(event_name, block) end validate_event_definition_request(event_name, options) # Define the event class new_event = options[:model].new_submodel task_model: self, terminal: options[:terminal], symbol: event_name, command: options[:command] new_event.permanent_model = self.permanent_model? setup_terminal_handler = false old_model = find_event_model(event_name) if new_event.symbol != :stop && options[:terminal] && (!old_model || !old_model.terminal?) setup_terminal_handler = true end events[new_event.symbol] = new_event if setup_terminal_handler forward(new_event => :stop) end const_set(event_name.to_s.camelcase(:upper), new_event) define_event_methods(event_name) new_event end
Find the event class for event
, or nil if event
is not an event name for this model
# File lib/roby/models/task.rb, line 652 def find_event_model(name) find_event(name.to_sym) end
Establish model-level forwarding between events of that task. These relations will be established on all the instances of this task model (and its subclasses).
Forwarding is used to cause the target event to be emitted when the source event is.
@param [Hash<Symbol,Array<Symbol>>,Hash<Symbol,Symbol>] mappings the source-to-target mappings @example
# A task that is stopped as soon as it is started class MyTask < Roby::Task forward start: :stop end
@see Task#forward
@see EventGenerator#forward
. @see Roby::EventStructure::Forward the forwarding relation.
# File lib/roby/models/task.rb, line 378 def forward(mappings) mappings.each do |from, to| from = event_model(from).symbol targets = Array[*to].map { |ev| event_model(ev).symbol } if event_model(from).terminal? non_terminal = targets.find_all { |name| !event_model(name).terminal? } if !non_terminal.empty? raise ArgumentError, "trying to establish a forwarding relation from the terminal event #{from} to the non-terminal event(s) #{targets}" end end forwarding_sets[from].merge targets end update_terminal_flag end
Helper method to define delayed arguments from related objects
@example propagate an argument from a parent task
argument :target, default: from(:parent).target
# File lib/roby/models/task.rb, line 401 def from(object) if object.kind_of?(Symbol) Roby.from(nil).send(object) else Roby.from(object) end end
Helper method to define delayed arguments from the State object
@example get an argument from the State object
argument :initial_pose, default: from_state.pose
# File lib/roby/models/task.rb, line 413 def from_state(state_object = State) Roby.from_state(state_object) end
# File lib/roby/models/task.rb, line 812 def fullfills?(models) if models.respond_to?(:each) models = models.to_a else models = [models] end models.each do |m| m.each_fullfilled_model do |test_m| return false if !has_ancestor?(test_m) end end return true end
# File lib/roby/models/task.rb, line 58 def instantiate_event_relations(template) events = template.events_by_name all_signals.each do |generator, signalled_events| next if signalled_events.empty? generator = events[generator] for signalled in signalled_events signalled = events[signalled] generator.signals signalled end end all_forwardings.each do |generator, signalled_events| next if signalled_events.empty? generator = events[generator] for signalled in signalled_events signalled = events[signalled] generator.forward_to signalled end end all_causal_links.each do |generator, signalled_events| next if signalled_events.empty? generator = events[generator] for signalled in signalled_events signalled = events[signalled] generator.add_causal_link signalled end end # Add a link from internal_event to stop if stop is controllable if events[:stop].controlable? events[:internal_error].signals events[:stop] end terminal_events, success_events, failure_events = compute_terminal_events(events) template.terminal_events = terminal_events template.success_events = success_events template.failure_events = failure_events start_event = events[:start] # WARN: the start event CAN be terminal: it can be a signal from # :start to a terminal event # # Create the precedence relations between 'normal' events and the terminal events root_terminal_events = terminal_events.find_all do |ev| (ev != start_event) && ev.root?(Roby::EventStructure::Precedence) end events.each_value do |ev| next if ev == start_event if !terminal_events.include?(ev) if ev.root?(Roby::EventStructure::Precedence) start_event.add_precedence(ev) end if ev.leaf?(Roby::EventStructure::Precedence) for terminal in root_terminal_events ev.add_precedence(terminal) end end end end end
Declare that tasks of this model can be interrupted by calling the command of {Roby::Task#failed_event}
@raise [ArgumentError] if {Roby::Task#failed_event} is not controlable.
# File lib/roby/models/task.rb, line 434 def interruptible if !has_event?(:failed) || !event_model(:failed).controlable? raise ArgumentError, "failed is not controlable" end event(:stop) do |context| if starting? start_event.signals stop_event return end failed!(context) end end
# File lib/roby/models/task.rb, line 40 def invalidate_template @template = nil end
Returns a TaskMatcher object that matches this task model
# File lib/roby/models/task.rb, line 796 def match(*args) matcher = Queries::TaskMatcher.new if args.empty? && self != Task matcher.which_fullfills(self) else matcher.which_fullfills(*args) end matcher end
Adds an event handler for the given event model. The block is going to be called whenever some events are emitted.
Unlike a block given to {EventGenerator#on}, the block is evaluated in the context of the task instance.
@param [Array<Symbol>] event_names the name of the events on which
to install the handler
@yieldparam [Object] context the arguments passed to {Roby::Task#emit}
when the event was emitted
# File lib/roby/models/task.rb, line 703 def on(*event_names, &user_handler) if !user_handler raise ArgumentError, "#on called without a block" end check_arity(user_handler, 1, strict: true) event_names.each do |from| from = event_model(from).symbol if user_handler method_name = "event_handler_#{from}_#{Object.address_from_id(user_handler.object_id).to_s(16)}" define_method(method_name, &user_handler) handler = lambda { |event| event.task.send(method_name, event) } handler_sets[from] << EventGenerator::EventHandler.new(handler, false, false) end end end
Defines an exception handler.
When propagating exceptions, {ExecutionException} goes up in the task hierarchy and calls matching handlers on the tasks it finds, and on their planning task. The first matching handler is called, and the exception propagation assumes that it handled the exception (i.e. won't look for new handlers) unless it calls {Roby::Task#pass_exception}
@param [#to_execution_exception_matcher] matcher object for
exceptions. Subclasses of {LocalizedError} have it (matching the exception class) as well as {Task} (matches exception origin). See {Roby::Queries} for more advanced exception matchers.
@yieldparam [ExecutionException] exception the exception that is
being handled
@example install a handler for a TaskModelViolation exception
on_exception(TaskModelViolation, ...) do |task, exception_object| if cannot_handle task.pass_exception # send to the next handler end do_handle end
# File lib/roby/models/task.rb, line 775 def on_exception(matcher, &handler) check_arity(handler, 1, strict: true) matcher = matcher.to_execution_exception_matcher id = (@@exception_handler_id += 1) define_method("exception_handler_#{id}", &handler) exception_handlers.unshift [matcher, instance_method("exception_handler_#{id}")] end
Declares that the given block should be called at each execution cycle, when the task is running. Use it that way:
class MyTask < Roby::Task poll do ... do something ... end end
If the given polling block raises an exception, the task will be terminated by emitting its failed
event.
# File lib/roby/models/task.rb, line 743 def poll(&block) if !block_given? raise ArgumentError, "no block given" end define_method(:poll_handler, &block) end
# File lib/roby/models/task.rb, line 722 def precondition(event, reason, &block) event = event_model(event) precondition_sets[event.symbol] << [reason, block] end
Returns the lists of tags this model fullfills.
# File lib/roby/models/task.rb, line 728 def provided_services ancestors.find_all { |m| m.kind_of?(Models::TaskServiceModel) } end
# File lib/roby/models/task.rb, line 785 def query(*args) q = Queries::Query.new if args.empty? && self != Task q.which_fullfills(self) else q.which_fullfills(*args) end q end
Establish model-level signals between events of that task. These signals will be established on all the instances of this task model (and its subclasses).
Signals cause the target event(s) command to be called when the source event is emitted.
@param [Hash<Symbol,Array<Symbol>>,Hash<Symbol,Symbol>] mappings the source-to-target mappings @raise [ArgumentError] if the target event is not controlable,
i.e. not have a command
@example when establishing multiple relations from the same source use name-to-arrays
signal start: [:one, :two]
# File lib/roby/models/task.rb, line 320 def signal(mappings) mappings.each do |from, to| from = event_model(from) targets = Array[*to].map { |ev| event_model(ev) } if from.terminal? non_terminal = targets.find_all { |ev| !ev.terminal? } if !non_terminal.empty? raise ArgumentError, "trying to establish a signal from the terminal event #{from} to the non-terminal events #{non_terminal}" end end non_controlable = targets.find_all { |ev| !ev.controlable? } if !non_controlable.empty? raise ArgumentError, "trying to signal #{non_controlable.join(" ")} which is/are not controlable" end signal_sets[from.symbol].merge targets.map { |ev| ev.symbol } end update_terminal_flag end
The plan that is used to instantiate this task model
# File lib/roby/models/task.rb, line 45 def template return @template if @template template = Template.new each_event do |event_name, event_model| template.add(event = TemplateEventGenerator.new(event_model.controlable?, event_model, plan: template)) template.events_by_name[event_name] = event end instantiate_event_relations(template) @template = template end
Get the list of terminal events for this task model
# File lib/roby/models/task.rb, line 646 def terminal_events each_event.find_all { |_, e| e.terminal? }. map { |_, e| e } end
Declare that tasks of this model can finish by simply emitting stop, i.e. with no specific action.
@example
class MyTask < Roby::Task terminates end
# File lib/roby/models/task.rb, line 425 def terminates event :failed, command: true, terminal: true interruptible end
# File lib/roby/models/task.rb, line 830 def to_coordination_task(task_model) Roby::Coordination::Models::TaskFromAsPlan.new(self, self) end
@return [Queries::ExecutionExceptionMatcher] an exception match
object that matches exceptions originating from this task
# File lib/roby/models/task.rb, line 808 def to_execution_exception_matcher Queries::ExecutionExceptionMatcher.new.with_origin(self) end
If this class model has an 'as_plan', this specifies what arguments should be passed to as_plan
# File lib/roby/models/task.rb, line 171 def with_arguments(arguments = Hash.new) if respond_to?(:as_plan) AsPlanProxy.new(self, arguments) else raise NoMethodError, "#with_arguments is invalid on #self, as #self does not have an #as_plan method" end end