Acting, Sensing & Control
This section covers how to interface with external processes. For this purpose the Scala common library offers several abstractions.
Actor: execute actions and monitor their status
Sensor: read information from external sources
Controller: given a goal, actor, and sensor, choose actions to achieve the goal
Dispatcher: manage executions of plans (e.g., sequential, partial-order) and deal with execution errors
All of these abstractions extend the Tickable trait whose purpose is to allow to control the frequency with which they update their internal states.
Actor
The actor's responsibility is to execute some external process and keep track of the status of the execution. Examples include running a program binary, sending a robot to a location, or even sending a task to another agent.
Typical usage of the Actor is:
Dispatch an action
Tick the actor regularly until it's done
Dispatch next action
The interface supports checking if action terms are supported, dispatching them, canceling and checking on the status.
supported(action: Term) : Booleanchecks if action is supporteddispatch(action: Term) : Option[ActionInstanceId]attempts to dispatch an action and returns an ID if successful orNoneotherwise.dispatchBlock(action: Term) : Option[ActionInstanceId]same as dispatch, but will block until action is completedcancel(id: ActionInstanceId)attempt to cancel action given its ID (may or may not be supported)status(id: ActionInstanceId)read the current execution status of an actionidle: Booleancheck if the actor is currently executing any actionregisterCallback(f: (ActionInstanceId, Term, Status) => Unit ): Unitregister callback function that takes an ID, the action term, and a status as arguments and will be called when the status changes.tick: Unitchecks on the external process (needs to be overwritten when Actor is implemented)
The status of a dispatched action is one of the following:
Pendingdispatched but not startedActivecurrently runningSucceededsuccessfully completedError(code: Term, msg: String)something went wrongRecallingcancelling action that has not started yetRecalledcancelled action that never startedPreemptingcancelling action while its runningPreemptedcancelled action while it was running
This set of states is based on the Robot Operating System 1 (ROS1) and might be simplified in the future to match the more recent ROS2 actions which merge recall and preempt into cancel.
Sensor
A sensor value consists of three elements:
a value term: what was sensed
a sequence ID
a nanosecond timestamp (taken when
performSenseAndUpdateis called)
Sensors have three modes:
OnDemandmode performs a sense operation whensenseis calledFrequencymode performs a sense operation whentickis called and returns the most recent read onsenseMixedperforms sense in both cases
To implement a sensor, extend the Sensor class and overwrite the performSense method to return the sensed vale. Make sure to set the sensor mode when creating concrete instances of your new sensor.
Example:
Controller
A controller uses a sensor to read the current state, then calls a decide method to generate a list of instructions which are then forwarded to its actor. Callbacks can be registered to read signals.
setGoal(goal: Term): UnitcurrentGoal: Termdecide(state: Term): List[Instruction]enable: Unitdisable: UnitregisterCallback(cb: Signal => Unit): UnitremoveCallback(cb: Signal => Unit): Unittick: Unit
To create your own controller, override the decide method and instantiate with a sensor and actor. The following controller senses a number and returns a + action when it is too low and a - action when it is too large. When the goal is reached it emits the corresponding signal and returns an empty list.
See /test/scala/org/aiddl/common/scala/execution/ControllerSuite.scala for the full example including the implementation of NumberSimulator.
Controller Instructions
Instructions consist of an optional ID term and the action term. The latter will be dispatched while the former can be used to get a unique handle on an action. This is useful in cases where actions may be repeated, but the ordering needs to be tracked (e.g., an action load-robot may happen many times, so a partial-order planner may assign some form of ID to keep track of partial-orders)
Controller Signals
Enabled: Controller has been enabled
Disabled: Controller has been disabled
Skipped: Controller skipped tick because it is disabled
GoalReached(goal: Term): Controller has reached a goal
GoalUpdated(goal: Term): Controller goal has been updated
Dispatched(instanceId: ActionInstanceId, actionId: Term, action: Term): Controller has dispatched an action
UnexpectedActionResult(actionId: Term, action: Term): Action succeeded, but sensed state was not expected
ActorFailure(instanceId: ActionInstanceId, actionId: Term, action: Term, code: Term, message: String): Actor failed to execute an action
DispatchFailure(id: Option[Term], action: Term): An action could not be dispatched
GoalUnreachable(goal: Term): Controller cannot reach goal from sensed state
Dispatcher
Plans come in a variety of forms with different expectations of when the next action is to be executed.
A Sequential plan is a list of actions executed in total order.
A partially-ordered is a set of actions with a partial order between them. Each action can be executed once all its predecessors in the partial order have been executed.
The purpose of the dispatcher trait is to have a common interface regardless of the type of plan we want to execute. The trait should be implemented for each concrete type of plan.
Currently, we have two implementations for sequential plans and partially-ordered plans.
See Also
External Libraries -> Services via Protobuf & gRPC