Module: Roby::Models::Task
Defined Under Namespace
Classes: AsPlanProxy, Template, TemplateEventGenerator
Constant Summary collapse
- @@exception_handler_id =
0
Constants included from Arguments
Arguments::NO_DEFAULT_ARGUMENT
Event Relations collapse
-
#causal_link(mappings) ⇒ Object
Establish model-level causal links between events of that task.
-
#forward(mappings) ⇒ Object
Establish model-level forwarding between events of that task.
-
#signal(mappings) ⇒ Object
Establish model-level signals between events of that task.
Class Method Summary collapse
-
.model_attribute_list(name) ⇒ Object
Declares an attribute set which follows the task models inheritance hierarchy.
- .model_relation(name) ⇒ Object
Instance Method Summary collapse
-
#abstract ⇒ Object
Declare that this task model defines abstract tasks.
- #all_models ⇒ Object deprecated Deprecated.
-
#as_plan(arguments = Hash.new) ⇒ Object
Default implementation of the #as_plan method.
- #can_merge?(target_model) ⇒ Boolean
-
#clear_model ⇒ Object
Clears all definitions saved in this model.
- #compute_terminal_events(events) ⇒ Object
-
#define_command_method(event_name, block) ⇒ Object
private
Define the method that will be used as command for the given event.
-
#define_event_methods(event_name) ⇒ Object
private
Define support methods for a task event.
- #discover_terminal_events(events, terminal_set, set, root) ⇒ Object
-
#enum_events ⇒ Object
:nodoc.
-
#event(event_name, options = Hash.new, &block) ⇒ Hash<Symbol,TaskEvent>
The events defined by the task model.
-
#event_model(model_def) ⇒ Model<TaskEvent>
Accesses an event model.
-
#find_event_model(name) ⇒ Object
(also: #has_event?)
Find the event class for
event, or nil ifeventis not an event name for this model. -
#from(object) ⇒ Object
Helper method to define delayed arguments from related objects.
-
#from_state(state_object = State) ⇒ Object
Helper method to define delayed arguments from the State object.
- #fullfills?(models) ⇒ Boolean
- #instantiate_event_relations(template) ⇒ Object
-
#interruptible ⇒ Object
Declare that tasks of this model can be interrupted by calling the command of Task#failed_event.
- #invalidate_template ⇒ Object
-
#match(*args) ⇒ Object
Returns a TaskMatcher object that matches this task model.
-
#on(*event_names) {|context| ... } ⇒ Object
Adds an event handler for the given event model.
-
#on_exception(matcher) {|exception| ... } ⇒ Object
Defines an exception handler.
-
#poll(&block) ⇒ Object
Declares that the given block should be called at each execution cycle, when the task is running.
- #precondition(event, reason, &block) ⇒ Object
-
#provided_services ⇒ Object
Returns the lists of tags this model fullfills.
- #query(*args) ⇒ Object
-
#template ⇒ Object
The plan that is used to instantiate this task model.
-
#terminal_events ⇒ Object
Get the list of terminal events for this task model.
-
#terminates ⇒ Object
Declare that tasks of this model can finish by simply emitting stop, i.e.
- #to_coordination_task(task_model) ⇒ Object
-
#to_execution_exception_matcher ⇒ Queries::ExecutionExceptionMatcher
An exception match object that matches exceptions originating from this task.
-
#update_terminal_flag ⇒ Object
private
Update the terminal flag for the event models that are defined in this task model.
-
#with_arguments(arguments = Hash.new) ⇒ Object
If this class model has an ‘as_plan’, this specifies what arguments should be passed to as_plan.
Methods included from Arguments
#argument, #arguments, #default_argument, #meaningful_arguments
Class Method Details
.model_attribute_list(name) ⇒ Object
Declares an attribute set which follows the task models inheritance hierarchy. Define the corresponding enumeration methods as well.
For instance,
model_attribute_list 'signal'
defines the model-level signals, which can be accessed through
.each_signal(model)
.signals(model)
#each_signal(model)
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 |
# File 'lib/roby/models/task.rb', line 253 def self.model_attribute_list(name) # :nodoc: class_eval <<-EOD, __FILE__, __LINE__+1 inherited_attribute("#{name}_set", "#{name}_sets", map: true) { Hash.new { |h, k| h[k] = Set.new } } def each_#{name}(model) for obj in #{name}s(model) yield(obj) end self end def #{name}s(model) result = Set.new each_#{name}_set(model, false) do |set| result.merge set end result end def all_#{name}s if @all_#{name}s @all_#{name}s else result = Hash.new each_#{name}_set do |from, targets| result[from] ||= Set.new result[from].merge(targets) end @all_#{name}s = result end end EOD end |
.model_relation(name) ⇒ Object
285 286 287 |
# File 'lib/roby/models/task.rb', line 285 def self.model_relation(name) model_attribute_list(name) end |
Instance Method Details
#abstract ⇒ Object
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.
461 462 463 |
# File 'lib/roby/models/task.rb', line 461 def abstract @abstract = true end |
#all_models ⇒ Object
Use #each_submodel instead
221 222 223 |
# File 'lib/roby/models/task.rb', line 221 def all_models submodels end |
#as_plan(arguments = Hash.new) ⇒ Object
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))
208 209 210 211 212 213 214 215 216 |
# 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 |
#can_merge?(target_model) ⇒ Boolean
826 827 828 |
# File 'lib/roby/models/task.rb', line 826 def can_merge?(target_model) fullfills?(target_model) end |
#causal_link(mappings) ⇒ Object
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
353 354 355 356 357 358 359 |
# 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 |
#clear_model ⇒ Object
Clears all definitions saved in this model. This is to be used by the reloading code
227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
# 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 |
#compute_terminal_events(events) ⇒ Object
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# 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 |
#define_command_method(event_name, block) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Define the method that will be used as command for the given event
565 566 567 568 569 570 571 572 |
# 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 |
#define_event_methods(event_name) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Define support methods for a task event
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 |
# 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 |
#discover_terminal_events(events, terminal_set, set, root) ⇒ Object
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# 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 |
#enum_events ⇒ Object
:nodoc
640 641 642 643 |
# 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 |
#event(event_name, options = Hash.new, &block) ⇒ Hash<Symbol,TaskEvent>
The events defined by the task model
524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 |
# File 'lib/roby/models/task.rb', line 524 def event(event_name, = Hash.new, &block) event_name = event_name.to_sym = , controlable: nil, command: nil, terminal: nil, model: find_event_model(event_name) || Roby::TaskEvent if .has_key?(:controlable) [:command] = [:controlable] elsif !.has_key?(:command) && block [:command] = define_command_method(event_name, block) end validate_event_definition_request(event_name, ) # Define the event class new_event = [:model].new_submodel task_model: self, terminal: [:terminal], symbol: event_name, command: [:command] new_event.permanent_model = self.permanent_model? setup_terminal_handler = false old_model = find_event_model(event_name) if new_event.symbol != :stop && [: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 |
#event_model(model_def) ⇒ Model<TaskEvent>
Accesses an event model
This method gives access to this task’s event models. If given a name, it returns the corresponding event model. If given an event model, it verifies that the model is part of the events of self and returns it.
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 |
# File 'lib/roby/models/task.rb', line 666 def event_model(model_def) #:nodoc: if model_def.respond_to?(:to_sym) ev_model = find_event_model(model_def.to_sym) unless ev_model all_events = each_event.map { |name, _| name } raise ArgumentError, "#{model_def} is not an event of #{name}: #{all_events}" unless ev_model end elsif model_def.respond_to?(:has_ancestor?) && model_def.has_ancestor?(Roby::TaskEvent) # Check that model_def is an event class for us ev_model = find_event_model(model_def.symbol) if !ev_model raise ArgumentError, "no #{model_def.symbol} event in #{name}" elsif ev_model != model_def raise ArgumentError, "the event model #{model_def} is not a model for #{name} (found #{ev_model} with the same name)" end else raise ArgumentError, "wanted either a symbol or an event class, got #{model_def}" end ev_model end |
#find_event_model(name) ⇒ Object Also known as: has_event?
Find the event class for event, or nil if event is not an event name for this model
652 653 654 |
# File 'lib/roby/models/task.rb', line 652 def find_event_model(name) find_event(name.to_sym) end |
#forward(mappings) ⇒ Object
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.
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 |
# 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 |
#from(object) ⇒ Object
Helper method to define delayed arguments from related objects
401 402 403 404 405 406 407 |
# 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 |
#from_state(state_object = State) ⇒ Object
Helper method to define delayed arguments from the State object
413 414 415 |
# File 'lib/roby/models/task.rb', line 413 def from_state(state_object = State) Roby.from_state(state_object) end |
#fullfills?(models) ⇒ Boolean
812 813 814 815 816 817 818 819 820 821 822 823 824 |
# 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 |
#instantiate_event_relations(template) ⇒ Object
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# 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 |
#interruptible ⇒ Object
Declare that tasks of this model can be interrupted by calling the command of Task#failed_event
434 435 436 437 438 439 440 441 442 443 444 445 446 |
# 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 |
#invalidate_template ⇒ Object
40 41 42 |
# File 'lib/roby/models/task.rb', line 40 def invalidate_template @template = nil end |
#match(*args) ⇒ Object
Returns a TaskMatcher object that matches this task model
796 797 798 799 800 801 802 803 804 |
# 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 |
#on(*event_names) {|context| ... } ⇒ Object
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.
703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 |
# 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 |
#on_exception(matcher) {|exception| ... } ⇒ Object
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 Task#pass_exception
775 776 777 778 779 780 781 |
# 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 |
#poll(&block) ⇒ Object
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.
743 744 745 746 747 748 749 |
# 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 |
#precondition(event, reason, &block) ⇒ Object
722 723 724 725 |
# File 'lib/roby/models/task.rb', line 722 def precondition(event, reason, &block) event = event_model(event) precondition_sets[event.symbol] << [reason, block] end |
#provided_services ⇒ Object
Returns the lists of tags this model fullfills.
728 729 730 |
# File 'lib/roby/models/task.rb', line 728 def provided_services ancestors.find_all { |m| m.kind_of?(Models::TaskServiceModel) } end |
#query(*args) ⇒ Object
785 786 787 788 789 790 791 792 793 |
# 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 |
#signal(mappings) ⇒ Object
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.
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 |
# 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 |
#template ⇒ Object
The plan that is used to instantiate this task model
45 46 47 48 49 50 51 52 53 54 55 56 |
# 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 |
#terminal_events ⇒ Object
Get the list of terminal events for this task model
646 647 648 649 |
# File 'lib/roby/models/task.rb', line 646 def terminal_events each_event.find_all { |_, e| e.terminal? }. map { |_, e| e } end |
#terminates ⇒ Object
Declare that tasks of this model can finish by simply emitting stop, i.e. with no specific action.
425 426 427 428 |
# File 'lib/roby/models/task.rb', line 425 def terminates event :failed, command: true, terminal: true interruptible end |
#to_coordination_task(task_model) ⇒ Object
830 831 832 |
# File 'lib/roby/models/task.rb', line 830 def to_coordination_task(task_model) Roby::Coordination::Models::TaskFromAsPlan.new(self, self) end |
#to_execution_exception_matcher ⇒ Queries::ExecutionExceptionMatcher
Returns an exception match object that matches exceptions originating from this task.
808 809 810 |
# File 'lib/roby/models/task.rb', line 808 def to_execution_exception_matcher Queries::ExecutionExceptionMatcher.new.with_origin(self) end |
#update_terminal_flag ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Update the terminal flag for the event models that are defined in this task model. The event is terminal if model-level signals (#signal) or forwards (#forward) lead to the emission of #stop_event
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 |
# File 'lib/roby/models/task.rb', line 471 def update_terminal_flag # :nodoc: events = each_event.map { |name, _| name } terminal_events = [:stop] events.delete(:stop) loop do old_size = terminal_events.size events.delete_if do |ev| if signals(ev).any? { |sig_ev| terminal_events.include?(sig_ev) } || forwardings(ev).any? { |sig_ev| terminal_events.include?(sig_ev) } terminal_events << ev true end end break if old_size == terminal_events.size end terminal_events.each do |sym| if ev = self.events[sym] ev.terminal = true else ev = superclass.event_model(sym) unless ev.terminal? event sym, model: ev, terminal: true, command: (ev.method(:call) rescue nil) end end end end |
#with_arguments(arguments = Hash.new) ⇒ Object
If this class model has an ‘as_plan’, this specifies what arguments should be passed to as_plan
171 172 173 174 175 176 177 |
# 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 |