Are expectations callbacks and clarifications of Ardsdk DSL

Hello guys,

  1. Question

I have some points that I would be glad if somebody helped clarify:

In the user manual it is written

The “>>” operator is used to combine two expressions with an “and then” semantic.

Does this mean that I can replace this operator with a traditional python if clause?

  1. List item

Are the expectations a kind of look back callback mechanism? I ask because the following code is described:

In the above example we are using a compound expectation expression to send a taking off command when the drone has a GPS fix and when it is not already in the hovering state.
Emphasis mine.

TakeOff(_no_expect=True)
             & FlyingStateChanged(state="hovering", _policy="wait", _timeout=5)

If this is a kind of callback does this mean that the expression is evaluated left to right?

  1. List item

Another question I have is if it is possible to have a sdk user supplying a callback to the recv_cmd_cb where the user’s callback would be threaded off the main context and passed the newly received event? This would be useful, as I would not need to query all the messages for changes. Am i missi

I find the overloading of operators extremely confusing, as it seems to create a Domain Specific Language inside Python. Would it be possible to achieve most of the functionality without the operators?

If I am not clear let me know and I will try to reformulate my questions.

The Olympe DSL is an asynchronous API so you can build complex expressions and wait for the expectations asynchronously. Doing the same thing with a traditional if clause would require the user to spread the program logic in multiple callbacks. It brings similar (but different) advantages/drawbacks as asynchronous coroutines.

I don’t understand what you mean by “look back callback mechanism” but I’ll try to shed some light on the Olympe DSL below.

The Olympe DSL defines a kind of program that you can execute later. You can choose to execute an Olympe DSL expression with or without waiting for the end of its execution, just call .wait() on the expectation object to block the current thread until the expectation is done.

You can see an Olympe DSL expression as a succession of messages to be sent/received. For example

CmdA() >> EvtB() >> CmdC()

The above DSL expression is a program that will send CmdA then waits for EvtB and then sends CmdC.
To execute this DSL expression/program, you have to submit it to a Drone object.

drone(CmdA() >> EvtB() >> CmdC())

This will execute the expression. First, CmdA is sent to the drone, then Olympe waits for EvtB and then sends CmdC. If EvtB is not received, the execution stops and CmdC is not sent. This is all done asynchronously. So it means that in the following code, the print('Hello there!') will most probably be executed while CmdA has not been sent yet :

drone(CmdA() >> EvtB() >> CmdC())
print('Hello there!')

If you want to wait for the expression to be executed, you have to .wait() :

drone(CmdA() >> EvtB() >> CmdC()).wait(10)
print('Hello there!')

If you want to execute some code only if an expression has successfully been executed, you can test the expression with the .success() expectation method :

if drone(CmdA() >> EvtB() >> CmdC()).wait(10).success():
    print('Hello there!')

If you want to wait for two events in parallel, you can use the & (AND) or | (OR) operators. For example :

EvtA() & EvtB()

is an expression that expects an EvtA and an EvtB but without specifying a specific order i.e. either EvtA or EvtB may be received first but in the end EvtA and EvtB must be received for the entire expectation to be successful.

drone(EvtA() & EvtB()).wait(10).success()  # is True only if EvtA and EvtB have been received with 10 seconds

Conversely:

drone(EvtA() | EvtB()).wait(10).success()  # is True if either EvtA or EvtB (or both events) have been received within 10 seconds

Olympe evaluates a DSL expressions from left to right and try to evaluate any subexpression synchronously whenever possible. If a subexpression is synchronously evaluated, the next subexpression to the right might not be evaluated. For example :

drone(FlyingStateChanged(state="hovering") | TakeOff()).wait(10)

In the above example, if the drone is currently in hovering, the TakeOff command is not sent and the whole expression is successful. If the drone is not in hovering, the TakeOff command is sent and the whole expression is successful either if the drone eventually reaches the hovering state or if the TakeOff command built-in expectations (flying state “motor_raming” and then “takingoff”) are successful.

This is not currently implemented and I definitely see the use case for this. This should not be too difficult to do, I just didn’t had time to implement this feature.

Hello @ndessart

Amazing post! All my questions were thoroughly answered. Maybe consider adding a similar write-up to the docs as it add good depth to the capabilities of the SDK and it’s DSL.

Regarding the callback I did it locally. Indeed it was simple. I just pass the message to the callback. Maybe the diff is useful for somebody.

diff --git a/src/olympe/arsdkng/drone.py b/src/olympe/arsdkng/drone.py                                    
index 3f77b25..3af09a4 100644                                                                             
--- a/src/olympe/arsdkng/drone.py                                                                         
+++ b/src/olympe/arsdkng/drone.py                                                                         
@@ -35,7 +35,7 @@ from __future__ import print_function                                                   
 from __future__ import absolute_import                                                                   
 from future.builtins import str, bytes                                                                   
                                                                                                          
-                                                                                                         
+import threading                                                                                         
 import ctypes                                                                                            
 import datetime                                                                                          
 import functools                                                                                         
@@ -127,7 +127,8 @@ class Drone(object):                                                                  
                  mpp=False,                                                                              
                  loglevel=TraceLogger.level.info,                                                        
                  logfile=sys.stdout,                                                                     
-                 video_buffer_queue_size=2):                                                             
+                 video_buffer_queue_size=2,                                                              
+                 callbacks = []):                                                                        
         """                                                                                              
         :param ip_addr: the drone IP address                                                             
         :type ip_addr: str                                                                               
@@ -159,6 +160,7 @@ class Drone(object):                                                                  
         self.thread_loop = None                                                                          
         self.discovery_inst = None                                                                       
         self.stop_discov_steps = dict()                                                                  
+        self.event_message_callbacks = callbacks                                                         
                                                                                                          
         if dcport is not None and drone_type is not None:                                                
             self.rawdiscov_info = {                                                                      
@@ -436,9 +438,10 @@ class Drone(object):                                                                 
             (name.upper(), message_args[pos])                                                            
             for name, pos in args_pos.items()                                                            
         ))                                                                                               
-                                                                                                         
         if callback_type == messages.ArsdkMessageCallbackType.STANDARD:                                  
             self._controller_state.device_states.states[message_name] = message_args                     
+            for user_callback in self.event_message_callbacks:                                           
+                threading.Thread(target=user_callback, args=[message]).start()                           
         elif callback_type == messages.ArsdkMessageCallbackType.MAP:                                     
             key = message_args[message.key_name.upper()]                                                 
             self._controller_state.device_states.states[message_name][key] = message_args                
diff --git a/src/olympe/doc/examples/takeoff.py b/src/olympe/doc/examples/takeoff.py