In the previous sections, we have shown how to make a simple behavior thanks to the
Robot abstraction. But how to combine those elementary behaviors into something more complex? You could use threads and do it manually, but we provide the
Primitive to abstract most of the work for you.
What do we call “Primitive”?¶
Primitive any simple or complex behavior applied to a
Robot. A primitive can access all sensors and effectors in the robot. A primitive is supposed to be independent of other primitives. In particular, a primitive is not aware of the other primitives running on the robot at the same time. We imagine those primitives as elementary blocks that can be combined to create more complex blocks in a hierarchical manner.
The independence of primitives is really important when you create complex behaviors - such as balance - where many primitives are needed. Adding another primitive - such as walking - should be direct and not force you to rewrite everything. Furthermore, the balance primitive could also be combined with another behavior - such as shoot a ball - without modifying it.
To ensure this independence, the primitive is running in a sort of sandbox. More precisely, this means that the primitive has not direct access to the robot. It can only request commands (e.g. set a new goal position of a motor) to a
PrimitiveManager which transmits them to the “real” robot. As multiple primitives can run on the robot at the same time, their request orders are combined by the manager.
The primitives all share the same manager. In further versions, we would like to move from this linear combination of all primitives to a hierarchical structure and have different layer of managers.
The manager uses a filter function to combine all orders sent by primitives. By default, this filter function is a simple mean but you can choose your own specific filter (e.g. add function).
You should not mix control through primitives and direct control through the
Robot. Indeed, the primitive manager will overwrite your orders at its refresh frequency: i.e. it will look like only the commands send through primitives will be taken into account.
Writing your own primitive¶
To write you own primitive, you have to subclass the
Primitive class. It provides you with basic mechanisms (e.g. connection to the manager, setup of the thread) to allow you to directly “plug” your primitive to your robot and run it.
You should always call the super constructor if you override the
As an example, let’s write a simple primitive that recreate the dance behavior written in the Controlling your Ergo-Robot section. Notice that to pass arguments to your primitive, you have to override the
import time import pypot.primitive class DancePrimitive(pypot.primitive.Primitive): def __init__(self, robot, amp=30, freq=0.5): self.robot = robot self.amp = amp self.freq = freq pypot.primitive.Primitive.__init__(self, robot) def run(self): amp = self.amp freq = self.freq # self.elapsed_time gives you the time (in s) since the primitive has been running while self.elapsed_time < 30: x = amp * numpy.sin(2 * numpy.pi * freq * self.elapsed_time) self.robot.base_pan.goal_position = x self.robot.head_pan.goal_position = -x time.sleep(0.02)
To run this primitive on your robot, you simply have to do:
ergo_robot = pypot.robot.from_config(...) dance = DancePrimitive(ergo_robot,amp=60, freq=0.6) dance.start()
If you want to make the dance primitive infinite you can use the
class LoopDancePrimitive(pypot.primitive.LoopPrimitive): def __init__(self, robot, refresh_freq, amp=30, freq=0.5): self.robot = robot self.amp = amp self.freq = freq LoopPrimitive.__init__(self, robot, refresh_freq) # The update function is automatically called at the frequency given on the constructor def update(self): amp = self.amp freq = self.freq x = amp * numpy.sin(2 * numpy.pi * freq * self.elapsed_time) self.robot.base_pan.goal_position = x self.robot.head_pan.goal_position = -x
And then runs it with:
ergo_robot = pypot.robot.from_config(...) dance = LoopDancePrimitive(ergo_robot, 50, amp = 40, freq = 0.3) # The robot will dance until you call dance.stop() dance.start()
When writing your own primitive, you should always keep in mind that you should never directly pass the robot or its motors as argument and access them directly. You have to access them through the self.robot and self.robot.motors properties. Indeed, at instantiation the
DxlMotor) instance is transformed into a
MockupMotor). Those class are used to intercept the orders sent and forward them to the
PrimitiveManager which will combine them. By directly accessing the “real” motor or robot you circumvent this mechanism and break the sandboxing. If you have to specify a list of motors to your primitive (e.g. apply the sinusoid primitive to the specified motors), you should either give the motors name and access the motors within the primitive or transform the list of
MockupMotor thanks to the
class MyDummyPrimitive(pypot.primitive.Primitive): def run(self, motors_name): motors = [getattr(self.robot, name) for name in motors_name] while True: for m in fake_motors: ...
class MyDummyPrimitive(pypot.primitive.Primitive): def run(self, motors): fake_motors = [self.get_mockup_motor(m) for m in motors] while True: for m in fake_motors: ...
Start, Stop, Pause, and Resume¶
When overriding the
Primitive, you are responsible for correctly handling those events. For instance, the stop method will only trigger the should stop event that you should watch in your run loop and break it when the event is set. In particular, you should check the
should_pause() in your run loop. You can also use the
wait_to_resume() to wait until the commands have really been executed.
You can refer to the source code of the
LoopPrimitive for an example of how to correctly handle all these events.
Attaching a primitive to the robot¶
In the previous section, we explain that the primitives run in a sandbox in the sense that they are not aware of the other primitives running at the same time. In fact, this is not exactly true. More precisely, a primitive can access everything attached to the robot: e.g. motors, sensors. But you can also attach a primitive to the robot.
Let’s go back on our DancePrimitive example. You can write:
ergo_robot = pypot.robot.from_config(...) ergo_robot.attach_primitive(DancePrimitive(ergo_robot), 'dance') ergo_robot.dance.start()
By attaching a primitive to the robot, you make it accessible from within other primitive.
For instance you could then write:
class SelectorPrimitive(pypot.primitive.Primitive): def run(self): if song == 'my_favorite_song_to_dance' and not self.robot.dance.is_alive(): self.robot.dance.start()
In this case, instantiating the DancePrimitive within the SelectorPrimitive would be another solution.