├── .gitignore ├── COPYING.txt ├── README ├── doc ├── README.md ├── js_robotutils.md ├── stk_events.md ├── stk_logging.md ├── stk_runner.md └── stk_services.md ├── javascript ├── robotutils.js └── robotutils.qim1.js ├── python ├── samples │ ├── sample_1_helloworld.py │ ├── sample_2_servicecache.py │ ├── sample_3_activity.py │ ├── sample_4_service.py │ ├── sample_5_logging.py │ ├── sample_6_exceptions.py │ ├── sample_7_events.py │ ├── sample_8_decorators.py │ └── sample_9_coroutines.py ├── stk │ ├── __init__.py │ ├── coroutines.py │ ├── events.py │ ├── logging.py │ ├── runner.py │ └── services.py └── tests │ ├── conftest.py │ └── test_async.py └── qiproject.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.komodoproject 3 | *.cache 4 | *~ 5 | -------------------------------------------------------------------------------- /COPYING.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017, SoftBank Robotics Europe SAS 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the SoftBank Robotics Europe SAS nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL SoftBank Robotics Europe SAS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ############################################## 2 | # # 3 | # Studio Tool Kit # 4 | # # 5 | # Created : November 2nd, 2015 # 6 | # Author : Aldebaran Studio # 7 | # Copyright: Aldebaran # 8 | ############################################## 9 | 10 | 11 | Summary: 12 | Studio tool kit is a set of libraries and tools used by Studio Team to develop awesome applications 13 | 14 | 15 | Content : 16 | studiotoolkit/ 17 | README - this file 18 | python/ - the python libraries 19 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | 2 | STK Python Modules 3 | ==================== 4 | 5 | The stk libraries provide helpers for common functionalities. They don't depend on each other, and can be integrated in applications. 6 | 7 | Modules: 8 | 9 | * [stk.runner](stk_runner.md), for running an application 10 | * [stk.services](stk_services.md), for easy access to services 11 | * [stk.logging](stk_logging.md) for logging 12 | * [stk.events](stk_events.md), for ALMemory events, and signals 13 | 14 | You can also see sample usage of these in the python/samples/ folder. 15 | 16 | Javascript libraries 17 | ==================== 18 | 19 | Here are also some helper javascript libraries for common functionality. 20 | 21 | * [robotutils.js](js_robotutils.md), a helper for using NAOqi services. 22 | -------------------------------------------------------------------------------- /doc/js_robotutils.md: -------------------------------------------------------------------------------- 1 | Studio lib: **`robotutils.js`** 2 | 3 | A utility library for qimessaging.js. 4 | 5 | Includes: 6 | 7 | * Support for remote debugging by running pages from your local computer. 8 | * Syntactic sugar, see below 9 | 10 | If you use this library, you don't need to link to qimessaging.js in your html, just package it in your html folder and include: 11 | 12 | ```html 13 | 14 | ``` 15 | 16 | 17 | API reference 18 | ==== 19 | 20 | * **RobotUtils.onServices(servicesCallback, errorCallback)**, for getting services 21 | * **RobotUtils.subscribeToALMemoryEvent(event, eventCallback, subscribeDoneCallback)**, for subscribing to ALMemory 22 | * **RobotUtils.robotIp** - a member variable containing the address of the robot you're connecting to 23 | 24 | Functions you probably will not need (used internally, or for advance things): 25 | 26 | * **RobotUtils.connect(connectedCallback, failureCallback)**, for creating a session 27 | * **RobotUtils.session**, contains the current qimessaging.js session. 28 | 29 | Connecting to services 30 | ==== 31 | 32 | Just use `RobotUtils.onServices` with a function whose parameters are the names of the services you want; it will be called once they are all ready: 33 | 34 | ```javascript 35 | RobotUtils.onServices(function(ALLeds, ALTextToSpeech) { 36 | ALLeds.randomEyes(2.0); 37 | ALTextToSpeech.say("I can speak"); 38 | }); 39 | ``` 40 | 41 | Optionally, you can pass a failure callback as a second parameter: 42 | 43 | ```javascript 44 | RobotUtils.onServices(function(ALTabletService) { 45 | // TODO: do something cool 46 | }, function(error) { 47 | alert("oh dear, I don't have a tablet ... wait, then where is this code running?"); 48 | }); 49 | ``` 50 | 51 | Failure can be because you're not connecting to a robot (e.g. just opened the page in your web browser, see the "remote debugging" section below), or NAOqi is not running, or one of the services is not available, etc. 52 | 53 | 54 | Subscribing to ALMemory 55 | ==== 56 | 57 | Use `RobotUtils.subscribeToALMemoryEvent`, passing the name of your key, and a callback to be called whenever that event is raised: 58 | 59 | ```javascript 60 | RobotUtils.subscribeToALMemoryEvent("FrontTactilTouched", function(value) { 61 | alert("Head touched: " + value); 62 | }); 63 | ``` 64 | 65 | Optionally, you can pass as second parameter a callback to be called when the subscription is done. 66 | 67 | `RobotUtils.subscribeToALMemoryEvent` returns a `MemoryEventSubscription` object, on which you can call .unsubscribe(), which takes as optional parameter a callback to be called when the ubsubscription is done. 68 | 69 | Remote debugging 70 | ==== 71 | 72 | `robotutils.js` makes it easy to test your webpage locally, without needing to install anything to the robot: just open your page in a browser with an extra `?robot=my-robots-ip-address` after the URL. 73 | 74 | There are three general way of using a webpage that connects to NAOqi: 75 | 76 | * **On Pepper's tablet**, as in the vast majority of Pepper applications 77 | * **Hosted on Pepper, but opened with a web browser** (e.g. by opening http://my-robot/apps/my-app/) 78 | * **Hosted on your computer** 79 | 80 | Opening the page with a web browser is useful with debugging, as you can use your browser's debug tools (element inspection, javascript console...). Directly using the page hosted on your computer is especially useful during dev, as you can edit the files and test again without needing to copy anything on the robot - this is where you need to use the query robot parameter. 81 | 82 | In addition, if you need to dynamically fetch web content from the robot in Javascript, you can use the `RobotUtils.robotIp` variable. For example if you want to fetch an image from another app, you could do: 83 | 84 | ```javascript 85 | var imageUrl = "/apps/another-app/images/image.gif"; 86 | if (RobotUtils.robotIp) { 87 | imageUrl = "http://" + RobotUtils.robotIp + imageUrl; 88 | } 89 | // ... do something with imageUrl; 90 | ``` 91 | 92 | That way the image will be fetched from the right location regardless of how you are accessing your page. 93 | 94 | Older versions of NAOqi 95 | ==== 96 | 97 | robotutils.js does not work on NAOqi version 2.1 and below - typically found on NAO. robotutils.qim1.js provides the same API but with compatibility with older versions of NAOqi: 98 | 99 | ```html 100 | 101 | ``` 102 | -------------------------------------------------------------------------------- /doc/stk_events.md: -------------------------------------------------------------------------------- 1 | Studio lib: **`stk/events.py`** 2 | 3 | 4 | Basic usage 5 | ================== 6 | 7 | Create a **`EventsHelper`** object, that will serve as a simple interface for ALMemory events and signals using a common, simple syntax. 8 | 9 | Here is how you would block until a sensor is touched. 10 | 11 | ```python 12 | import stk.events 13 | 14 | events = stk.events.EventHelper(qiapp.session) 15 | 16 | events.wait_for("FrontTactilTouched") 17 | print "The front tactile sensor was touched!" 18 | 19 | ``` 20 | ... and here's how one would subscribe to an event, to print something each time that sensor is touched: 21 | 22 | ```python 23 | import stk.runner 24 | import stk.events 25 | 26 | class TouchDemo(object): 27 | def __init__(self, qiapp): 28 | self.qiapp = qiapp 29 | self.events = stk.events.EventHelper(qiapp.session) 30 | 31 | def on_touched(self, value): 32 | if value: 33 | print "It tickles!" 34 | 35 | def on_start(self): 36 | self.events.connect("FrontTactilTouched", self.on_touched) 37 | 38 | stk.runner.run_activity(TouchDemo) 39 | ``` 40 | 41 | using decorators 42 | ================== 43 | 44 | The above example can be rewritten like this: 45 | 46 | ```python 47 | import stk.runner 48 | import stk.events 49 | 50 | class DecoratorsDemo(object): 51 | def __init__(self, qiapp): 52 | self.events = stk.events.EventHelper(qiapp.session) 53 | 54 | @stk.events.on("FrontTactilTouched") 55 | def on_touched(self, value): 56 | if value: 57 | print "It tickles!" 58 | 59 | def on_start(self): 60 | self.events.connect_decorators(self) 61 | 62 | stk.runner.run_activity(DecoratorsDemo) 63 | ``` 64 | 65 | This makes it easier to keep track of your logic, especially if you have many subscriptions. 66 | 67 | Events vs. Signals 68 | ================== 69 | 70 | NAOqi has two related contepts: 71 | * ALMemory events (like `FrontTactilTouched`), handled by the ALMemory module 72 | * Signals (like `ALTabletService.onTouch`, handled by indivisual services 73 | 74 | NAOqi uses a different syntax for both, but `stk.events` wraps the two with the same syntax, so all the examples above would work the same with keys such as `ALTabletService.onTouch`. 75 | 76 | API details 77 | ===== 78 | 79 | **`on(*keys)`** : a decorator for connecting the decorated method to a callback. 80 | 81 | Methods of **`EventHelper`**: 82 | 83 | `EventHelper` **`.__init__(session=None)`** : constructor. If you don't specify a NAOqi session, you can do so later with `.init` 84 | 85 | `EventHelper` **`.init(session)`** : defines the NAOqi session if it wasn't done at construction. 86 | 87 | `EventHelper` **`.connect_decorators(object)`** : Connects all decorator methods on an object. 88 | 89 | `EventHelper` **`.connect(event, callback)`** : connect a function to an event, so that the function will be called every time the event is raised. "event" can be either an ALMemory key, or in the form signal.service. Returns a connection ID. 90 | 91 | 92 | `EventHelper` **`.disconnect(self, event, connection_id=None)`** : if a connection ID is given, disconnect that connection to the given event. Otherwise, disconnect all connections to the given event. 93 | 94 | `EventHelper` **`.clear()`** : Disconnects all event subscriptions.. 95 | 96 | `EventHelper` **`.get(key)`** : get a given ALMemory key. 97 | 98 | `EventHelper` **`.set(key, value)`** : set an ALMemory key. 99 | 100 | `EventHelper` **`.remove(key)`** : remove an ALMemory key. 101 | 102 | `EventHelper` **`.wait_for(event)`** : Blocks until the given event is raised, and returns its value. Will raise an exception if the wait is cancelled, or if wait_for() is called again. This blocks a thread, so avoid using it too much! 103 | 104 | `EventHelper` **`.cancel_wait()`** : Cancels the current wait, if there is one. 105 | -------------------------------------------------------------------------------- /doc/stk_logging.md: -------------------------------------------------------------------------------- 1 | Studio lib: **`stk/logging.py`** 2 | 3 | This library provides utilities to make it easier to handle logging. 4 | 5 | API reference 6 | ==== 7 | 8 | For usage recommendations, see below 9 | 10 | * **`get_logger()`** : returns a Qi Logger object, with some debug facilities (see "Basic Usage" below) 11 | * **`log_exceptions`** : A method decorator (on an object that must have a "logger" member) for logging exceptions raised (see "Exceptions" below) 12 | * **`log_exceptions_and_return(value)`** : A method decorator that logs exceptions and returns a default value 13 | 14 | 15 | Basic usage 16 | ==== 17 | 18 | 19 | Example 20 | 21 | ```python 22 | """ 23 | Sample 5: Logging 24 | 25 | Demonstrates stk.logging (only prints to log) 26 | """ 27 | 28 | import time 29 | 30 | import stk.runner 31 | import stk.logging 32 | 33 | class ActivityWithLogging(object): 34 | "Simple activity, demonstrating logging" 35 | APP_ID = "com.aldebaran.example5" 36 | def __init__(self, qiapp): 37 | self.qiapp = qiapp 38 | self.logger = stk.logging.get_logger(qiapp.session, self.APP_ID) 39 | 40 | def on_start(self): 41 | "Called at the start of the application." 42 | self.logger.info("I'm writing to the log.") 43 | time.sleep(4) # you can try stopping the process while it sleeps. 44 | self.logger.info("Okay I'll stop now.") 45 | self.stop() 46 | 47 | def stop(self): 48 | "Standard way of stopping the activity." 49 | self.logger.warning("I've been stopped with .stop().") 50 | self.qiapp.stop() 51 | 52 | def on_stop(self): 53 | "Called after the app is stopped." 54 | self.logger.error("My process is dyyyyyiiiiinnggggg ...") 55 | 56 | if __name__ == "__main__": 57 | stk.runner.run_activity(ActivityWithLogging) 58 | ``` 59 | 60 | Running this example from Python IDE should produce something like: 61 | 62 | ```ini 63 | no --qi-url parameter given; interactively getting debug robot. 64 | connect to which robot? (default is citadelle.local) 65 | [I] 5736 com.aldebaran.example5: I'm writing to the log. 66 | [I] 5736 com.aldebaran.example5: Okay I'll stop now. 67 | [W] 5736 com.aldebaran.example5: I've been stopped with .stop(). 68 | [E] 5731 com.aldebaran.example5: My process is dyyyyyiiiiinnggggg ... 69 | ``` 70 | Running it on the robot (in NAOqi 2.4) should produce those logs in /var/log/naoqi/servicemanager/(your service's name). 71 | 72 | You will also be able to see these logs in choregraphe or monitor (where you can filter them). 73 | 74 | 75 | Handling Exceptions 76 | ==================== 77 | 78 | A common annoyance with working with NAOqi is that if an exeption happens in your method, exceptions raised may be silently ignored. this happens in these cases: 79 | 80 | * With qi.async(my_function) (though you can check on the future returned by that call whether it has an error) 81 | * With calls to service methods ALMyService.myMethod() (however the exception will be raised on the caller's side) 82 | * With callbacks to ALMemory events and signals 83 | 84 | This is sometimes what you want, but it often means you'll be in a situation where your code is failing for stupid reasons, but none of that appears in your logs. 85 | 86 | Here's a service that logs it's exceptions: 87 | 88 | ```python 89 | """ 90 | Sample 6: Exceptions 91 | 92 | Demonstrates decorators for exception handling 93 | """ 94 | 95 | import stk.runner 96 | import stk.logging 97 | 98 | class ALLoggerDemo(object): 99 | "Simple activity, demonstrating logging and exceptions" 100 | APP_ID = "com.aldebaran.example6" 101 | def __init__(self, qiapp): 102 | self.qiapp = qiapp 103 | self.logger = stk.logging.get_logger(qiapp.session, self.APP_ID) 104 | 105 | @stk.logging.log_exceptions 106 | def compute_arithmetic_quotient(self, num_a, num_b): 107 | "Do a complicated operation on the given numbers." 108 | return num_a / num_b 109 | 110 | @stk.logging.log_exceptions_and_return(False) 111 | def is_lucky(self, number): 112 | "Is this number lucky?" 113 | return (1.0 / number) < 0.5 114 | 115 | def stop(self): 116 | "Standard way of stopping the activity." 117 | self.qiapp.stop() 118 | 119 | if __name__ == "__main__": 120 | stk.runner.run_service(ALLoggerDemo) 121 | ``` 122 | 123 | So there are two decorators: 124 | 125 | **@stk.logging.log_exceptions** Just prints the exception to the log (in this case, divide by zero): 126 | 127 | 128 | ```ini 129 | [E] 6607 com.aldebaran.example6: Traceback (most recent call last): 130 | File "/home/ekroeger/dev/studiotoolkit/python/stk/logging.py", line 37, in wrapped 131 | return func(self, *args) 132 | File "/home/ekroeger/dev/studiotoolkit/python/samples/sample_6_exceptions.py", line 20, in compute_arithmetic_quotient 133 | return num_a / num_b 134 | ZeroDivisionError: long division or modulo by zero 135 | ``` 136 | 137 | The caller still gets the exceptions (as if it was not decorated): 138 | 139 | ```ini 140 | citadelle [0] ~ $ qicli call ALLoggerDemo.compute_arithmetic_quotient 1 0 141 | ERROR: ZeroDivisionError: long division or modulo by zero 142 | ``` 143 | 144 | **@stk.logging.log_exceptions_and_return** allows you to specify a default value to return when an exception is raised, so that the caller never sees the exceptions: 145 | 146 | ```ini 147 | citadelle [err 1] ~ $ qicli call ALLoggerDemo.is_lucky 3 148 | true 149 | citadelle [0] ~ $ qicli call ALLoggerDemo.is_lucky 0 150 | false 151 | ``` 152 | 153 | These must be attached by an object with a "logging" member (from the helper above). 154 | 155 | **/!\ ** Be careful not to overuse `@log_exceptions_and_return`; it can be a convenient, but it amounts to catching *all* exceptions and hiding them (from the caller), which is discouraged in Python (and other languages) because it makes it harder to find mistakes in your code - see the discussion of Exceptions in [The Programming Recommendations of PEP 8](https://www.python.org/dev/peps/pep-0008/#programming-recommendations). 156 | 157 | It is usually better to either: 158 | 159 | * handle exceptions yourself in your methods (if they are "expected" e.g. you're running on the wrong robot, your robot doesn't have the internet...) in which case you don't need to print a full stack trace in the log, or 160 | * raise the exception on the caller side (as happens with no decorator, or with `@log_exceptions`), so that if he is the cause of the problem (e.g. passing you malformed data), he can be aware of it, and solve the problem or handle that case. 161 | 162 | There's the usual tradeoff between development - where you want your code to crash as soon as something goes even slightly wrong, and give you as much information as possible - and production - where you want a robust system that gracefully hides errors from the person interacting with the robot. -------------------------------------------------------------------------------- /doc/stk_runner.md: -------------------------------------------------------------------------------- 1 | Studio lib: **`stk/runner.py`** 2 | 3 | This is a collection of small utilities for running a Qi Application, while making it easy to debug without installing anything on the robot. 4 | 5 | API reference 6 | ==== 7 | 8 | For usage recommendations, see below 9 | 10 | * **`init()`** : returns a Qi.Application object, with some debug facilities (see below) 11 | * **`run_activity(activity_class)`** : instantiates the activity and runs it (see below) 12 | * **`run_service(service_class)`** : instantiates the service, registers it and runs it (see below) 13 | 14 | 15 | A simple script 16 | ==== 17 | 18 | The most useful function is init() 19 | 20 | ```python 21 | import stk.runner 22 | 23 | if __name__ == "__main__": 24 | qiapp = stk.runner.init() 25 | 26 | tts = qiapp.session.service("ALTextToSpeech") 27 | tts.say("Hello, World!") 28 | ``` 29 | 30 | What it does on the robot 31 | ----- 32 | 33 | Once on the robot, this is mostly equivalent to: 34 | 35 | ```python 36 | import qi 37 | 38 | qiapp = qi.Application() 39 | ... 40 | ``` 41 | 42 | ... provided the script has been called with a --qi-url argument in command line (which should be the case if it's packaged properly). 43 | 44 | When debugging locally 45 | ----- 46 | 47 | However, installing the application on the robot takes time, and slows down iteration time. 48 | 49 | So when using stk.runner.init(), you can also execute the application locally, in which case it will interactively ask you for your robot's IP address: 50 | 51 | no --qi-url parameter given; interactively getting debug robot. 52 | connect to which robot? (default is citadelle.local) 53 | 54 | 55 | If you have qiq installed (as in this case), it will suggest that as a default, and otherwise, you can specify on which robot you want to run. 56 | 57 | 58 | A more complete application 59 | ==== 60 | 61 | The above script is pretty simple, but you might want a more complete app, that you can start and stop. There is a helper for that, `stk.runner`**`.run_activity()`**, that expects an "activity" class. For example: 62 | 63 | 64 | ```python 65 | import time 66 | 67 | import stk.runner 68 | import stk.services 69 | 70 | class Activity(object): 71 | def __init__(self, qiapp): 72 | "Necessary: __init__ must take a qiapplication as parameter" 73 | self.qiapp = qiapp 74 | self.services = stk.services.ServiceCache(qiapp.session) 75 | 76 | def on_start(self): 77 | "Optional: Called at the start of the application." 78 | self.services.ALTextToSpeech.say("Let me think ...") 79 | time.sleep(2) 80 | self.services.ALTextToSpeech.say("No, nothing.") 81 | self.stop() 82 | 83 | def stop(self): 84 | "Optional: Standard way of stopping the activity." 85 | self.qiapp.stop() 86 | 87 | def on_stop(self): 88 | "Optional: Automatically called before exit (from .stop() or SIGTERM)." 89 | pass 90 | 91 | if __name__ == "__main__": 92 | stk.runner.run_activity(Activity) 93 | ``` 94 | 95 | The class must define a **`__init__`** that takes a qiapplication as a parameter, 96 | and may also define **`on_start`** and **`on_stop`**. 97 | The application will then run until qiapplication.stop() is called. 98 | 99 | 100 | A Service 101 | ==== 102 | 103 | This mostly works the same as for an activity: 104 | 105 | ```python 106 | import qi 107 | import stk.runner 108 | 109 | class ALAddition(object): 110 | def __init__(self, qiapp): 111 | self.qiapp = qiapp 112 | 113 | @qi.bind(qi.Int32, [qi.Int32, qi.Int32]) 114 | def add(self, a, b): 115 | "Returns the sum of two numbers" 116 | return a + b 117 | 118 | def stop(self): 119 | "Stops the service" 120 | self.qiapp.stop() 121 | 122 | if __name__ == "__main__": 123 | stk.runner.run_service(ALAddition) 124 | ``` 125 | 126 | Once run (even remotely), this service is available; for example with qicli: 127 | 128 | ```bash 129 | citadelle [0] ~ $ qicli info ALAddition --show-doc 130 | 145 [ALAddition] 131 | * Info: 132 | machine 8828e3e3-dcee-46f4-abff-5a456ada9dcb 133 | process 9523 134 | endpoints tcp://10.0.132.19:47057 135 | tcp://127.0.0.1:47057 136 | * Methods: 137 | 100 add Int32 (Int32,Int32) 138 | Returns the sum of two numbers. 139 | 101 stop Value () 140 | Stops the service. 141 | citadelle [0] ~ $ qicli call ALAddition.add 1 2 142 | 3 143 | ``` 144 | -------------------------------------------------------------------------------- /doc/stk_services.md: -------------------------------------------------------------------------------- 1 | Studio lib: **`stk/services.py`** 2 | 3 | 4 | Basic usage 5 | ================== 6 | 7 | Create a **`ServiceCache`** object, and it will behave as if it had all NAOqi services as members. 8 | 9 | For example: 10 | 11 | ```python 12 | import stk.services 13 | 14 | services = stk.services.ServiceCache(qiapp.session) 15 | 16 | if services.ALAddition: 17 | result = services.ALAddition.add(2, 2) 18 | services.ALTextToSpeech.say("2 and 2 are " + str(result)) 19 | else: 20 | services.ALTextToSpeech.say("You don't have ALAddition, so watch my eyes") 21 | services.ALLeds.rasta(2.0) 22 | ``` 23 | 24 | This is equivalent to: 25 | 26 | ```python 27 | try: 28 | addition = session.service("ALAddition") 29 | except RuntimeError: # service is not registered 30 | addition = None 31 | 32 | if addition: 33 | addition.showWebview() 34 | else: 35 | session.service("ALTextToSpeech").say("I don't have a webview") 36 | session.service("ALLeds").rasta(2.0) 37 | ``` 38 | 39 | So it allows you to keep your code simple and readable. 40 | 41 | 42 | API details 43 | ===== 44 | 45 | 46 | Methods of **`ServiceCache`**: 47 | 48 | `ServiceCache` **`.__init__(session=None)`** : constructor. If you don't specify a session, you can do so later with `.init` 49 | 50 | `ServiceCache` **`.init(session)`** : defines the session if it wasn't done at construction. 51 | 52 | `ServiceCache` **`.unregister(service_name)`** : unregisters the service, if it exists. 53 | 54 | `ServiceCache` **`.(any NAOqi module name)`** : will return the NAOqi module, or `None` if it doesn't exist. 55 | 56 | -------------------------------------------------------------------------------- /javascript/robotutils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * robotutils.js version 0.3 3 | * 4 | * A utility library for naoqi; 5 | * 6 | * This library is a wrapper over qimessaging.js. Some advantages: 7 | * - debugging and iterating are made easier by support for a 8 | * ?robot= query parameter in the URL, that allows 9 | * you to open a local file and connect it to a remote robot. 10 | * - there is some syntactic sugar over common calls that 11 | * allows you to keep your logic simple without too much nesting 12 | * 13 | * You can of course directly use qimessaging.js instead. 14 | * 15 | * See the method documentations below for sample usage. 16 | * 17 | * Copyright Aldebaran Robotics 18 | * Authors: ekroeger@aldebaran.com, jjeannin@aldebaran.com 19 | */ 20 | 21 | RobotUtils = (function(self) { 22 | 23 | /*--------------------------------------------- 24 | * Public API 25 | */ 26 | 27 | /* RobotUtils.onServices(servicesCallback, errorCallback) 28 | * 29 | * A function for using NAOqi services. 30 | * 31 | * "servicesCallback" should be a function whose arguments are the 32 | * names of NAOqi services; the callback will be called 33 | * with those services as parameters (or the errorCallback 34 | * will be called with a reason). 35 | * 36 | * Sample usage: 37 | * 38 | * RobotUtils.onServices(function(ALLeds, ALTextToSpeech) { 39 | * ALLeds.randomEyes(2.0); 40 | * ALTextToSpeech.say("I can speak"); 41 | * }); 42 | * 43 | * This is actually syntactic sugar over RobotUtils.connect() and 44 | * some basic QiSession functions, so that the code stays simple. 45 | */ 46 | self.onServices = function(servicesCallback, errorCallback) { 47 | self.connect(function(session) { 48 | var wantedServices = getParamNames(servicesCallback); 49 | var pendingServices = wantedServices.length; 50 | var services = new Array(wantedServices.length); 51 | var i; 52 | wantedServices.forEach(function(serviceName, i) { 53 | getService(session, serviceName, function(service) { 54 | services[i] = service; 55 | pendingServices -= 1; 56 | if (pendingServices == 0) { 57 | servicesCallback.apply(undefined, services); 58 | } 59 | }, function() { 60 | var reason = "Failed getting a NaoQi Service: " + 61 | serviceName; 62 | console.log(reason); 63 | if (errorCallback) { 64 | errorCallback(reason); 65 | } 66 | }); 67 | }); 68 | }, errorCallback); 69 | } 70 | 71 | // alias, so that the code looks natural when there is only one service. 72 | self.onService = self.onServices; 73 | 74 | /* Helper to get services, and eventually retry if required. 75 | * 76 | */ 77 | function getService(session, serviceName, onSuccess, onFailure) { 78 | session.service(serviceName).then( 79 | function(service) { 80 | onSuccess(service); 81 | }, 82 | function() { 83 | // Failure: the service wasn't there 84 | if ( waitableServices[serviceName] ) { 85 | // It might be normal, try again in 200 ms. 86 | console.log("Waiting for service " + serviceName); 87 | setTimeout(function(){ getService(session, 88 | serviceName, onSuccess, onFailure) }, 200); 89 | } 90 | else { 91 | onFailure(); 92 | } 93 | } 94 | ); 95 | } 96 | 97 | // services we want to wait when onServices is called 98 | var waitableServices = {}; 99 | 100 | /* RobotUtils.setWaitableServices(serviceA, serviceB, ...) 101 | * 102 | * Flag some services as "to be awaited" - this means that if they 103 | * are missing when RobotUtils.onServices(...) is called, if a 104 | * service is missing then we will wait for it instead of failing. 105 | * 106 | * This is typically useful if you packaged your own service in your 107 | * application and have launched it in parallel to showing a 108 | * webpage, to handle the case where the page is ready before the 109 | * service finished registering. 110 | */ 111 | self.setWaitableServices = function() 112 | { 113 | Array.prototype.slice.call(arguments).forEach(function(serviceName) { 114 | waitableServices[serviceName] = true; 115 | }); 116 | } 117 | 118 | /* RobotUtils.subscribeToALMemoryEvent(event, eventCallback, subscribeDoneCallback) 119 | * 120 | * connects a callback to an ALMemory event. Returns a MemoryEventSubscription. 121 | * 122 | * This is just syntactic sugar over calls to the ALMemory service, which you can 123 | * do yourself if you want finer control. 124 | */ 125 | self.subscribeToALMemoryEvent = function(event, eventCallback, subscribeDoneCallback) { 126 | var evt = new MemoryEventSubscription(event); 127 | self.onServices(function(ALMemory) { 128 | ALMemory.subscriber(event).then(function (sub) { 129 | evt.setSubscriber(sub) 130 | sub.signal.connect(eventCallback).then(function(id) { 131 | evt.setId(id); 132 | if (subscribeDoneCallback) subscribeDoneCallback(id) 133 | }); 134 | }, 135 | onALMemoryError); 136 | }); 137 | return evt; 138 | } 139 | 140 | /* RobotUtils.connect(connectedCallback, failureCallback) 141 | * 142 | * connectedCallback should take a single argument, a NAOqi session object 143 | * 144 | * This function is mostly meant for internal use, for your app you 145 | * should probably use the more specific RobotUtils.onServices or 146 | * RobotUtils.subscribeToALMemoryEvent. 147 | * 148 | * There can be several calls to .connect() in parallel, only one 149 | * session will be created. 150 | */ 151 | self.connect = function(connectedCallback, failureCallback) { 152 | if (self.session) { 153 | // We already have a session, don't create a new one 154 | connectedCallback(self.session); 155 | return; 156 | } 157 | else if (pendingConnectionCallbacks.length > 0) { 158 | // A connection attempt is in progress, just add this callback to the queue 159 | pendingConnectionCallbacks.push(connectedCallback); 160 | return; 161 | } 162 | else { 163 | // Add self to the queue, but create a new connection. 164 | pendingConnectionCallbacks.push(connectedCallback); 165 | } 166 | 167 | var qimAddress = null; 168 | var robotlibs = '/libs/'; 169 | if (self.robotIp) { 170 | // Special case: we're doing remote debugging on a robot. 171 | robotlibs = "http://" + self.robotIp + "/libs/"; 172 | qimAddress = self.robotIp + ":80"; 173 | } 174 | 175 | function onConnected(session) { 176 | self.session = session; 177 | var numCallbacks = pendingConnectionCallbacks.length; 178 | for (var i = 0; i < numCallbacks; i++) { 179 | pendingConnectionCallbacks[i](session); 180 | } 181 | } 182 | 183 | getScript(robotlibs + 'qimessaging/2/qimessaging.js', function() { 184 | QiSession( 185 | onConnected, 186 | failureCallback, 187 | qimAddress 188 | ) 189 | }, function() { 190 | if (self.robotIp) { 191 | console.error("Failed to get qimessaging.js from robot: " + self.robotIp); 192 | } else { 193 | console.error("Failed to get qimessaging.js from this domain; host this app on a robot or add a ?robot=MY-ROBOT-IP to the URL."); 194 | } 195 | failureCallback(); 196 | }); 197 | } 198 | 199 | // public variables that can be useful. 200 | self.robotIp = _getRobotIp(); 201 | self.session = null; 202 | 203 | /*--------------------------------------------- 204 | * Internal helper functions 205 | */ 206 | 207 | // Replacement for jQuery's getScript function 208 | function getScript(source, successCallback, failureCallback) { 209 | var script = document.createElement('script'); 210 | var prior = document.getElementsByTagName('script')[0]; 211 | script.async = 1; 212 | prior.parentNode.insertBefore(script, prior); 213 | 214 | script.onload = script.onreadystatechange = function( _, isAbort ) { 215 | if(isAbort || !script.readyState || /loaded|complete/.test(script.readyState) ) { 216 | script.onload = script.onreadystatechange = null; 217 | script = undefined; 218 | 219 | if(isAbort) { 220 | if (failureCallback) failureCallback(); 221 | } else { 222 | // Success! 223 | if (successCallback) successCallback(); 224 | } 225 | } 226 | }; 227 | script.src = source; 228 | } 229 | 230 | function _getRobotIp() { 231 | var regex = new RegExp("[\\?&]robot=([^&#]*)"); 232 | var results = regex.exec(location.search); 233 | return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ").replace("/", "")); 234 | } 235 | 236 | // Helper for getting the parameters from a function. 237 | var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; 238 | function getParamNames(func) { 239 | var fnStr = func.toString().replace(STRIP_COMMENTS, ''); 240 | var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g); 241 | if(result === null) 242 | result = []; 243 | return result; 244 | }; 245 | 246 | // ALMemory helpers (event subscription requires a lot of boilerplate) 247 | 248 | function MemoryEventSubscription(event) { 249 | this._event = event; 250 | this._internalId = null; 251 | this._sub = null; 252 | this._unsubscribe = false; 253 | } 254 | 255 | MemoryEventSubscription.prototype.setId = function(id) { 256 | this._internalId = id; 257 | // as id can be receveid after unsubscribe call, defere 258 | if (this._unsubscribe) this.unsubscribe(this._unsubscribeCallback); 259 | } 260 | 261 | MemoryEventSubscription.prototype.setSubscriber = function(sub) { 262 | this._sub = sub; 263 | // as sub can be receveid after unsubscribe call, defere 264 | if (this._unsubscribe) this.unsubscribe(this._unsubscribeCallback); 265 | } 266 | 267 | MemoryEventSubscription.prototype.unsubscribe = function(unsubscribeDoneCallback) 268 | { 269 | if (this._internalId != null && this._sub != null) { 270 | evtSubscription = this; 271 | evtSubscription._sub.signal.disconnect(evtSubscription._internalId).then(function() { 272 | if (unsubscribeDoneCallback) unsubscribeDoneCallback(); 273 | }).fail(onALMemoryError); 274 | } 275 | else 276 | { 277 | this._unsubscribe = true; 278 | this._unsubscribeCallback = unsubscribeDoneCallback; 279 | } 280 | } 281 | 282 | var onALMemoryError = function(errMsg) { 283 | console.log("ALMemory error: " + errMsg); 284 | } 285 | 286 | var pendingConnectionCallbacks = []; 287 | 288 | return self; 289 | 290 | })(window.RobotUtils || {}); 291 | -------------------------------------------------------------------------------- /javascript/robotutils.qim1.js: -------------------------------------------------------------------------------- 1 | /* 2 | * robotutils.qim1.js version 0.2 3 | * 4 | * A utility library for naoqi; requires jQuery. 5 | * 6 | * This library is a wrapper over qimessaging.js. Some advantages: 7 | * - debugging and iterating are made easier by support for a 8 | * ?robot= query parameter in the URL, that allows 9 | * you to open a local file and connect it to a remote robot. 10 | * - there is some syntactic sugar over common calls that 11 | * allows you to keep your logic simple without too much nesting 12 | * 13 | * You can of course directly use qimessaging.js instead. 14 | * 15 | * This uses qimessaging 1.0 (qimessaging 2 is not available on NAOqi 16 | * 2.1, which is on NAO) 17 | * 18 | * See the method documentations below for sample usage. 19 | * 20 | * Copyright Aldebaran Robotics 21 | * Authors: ekroeger@aldebaran.com, jjeannin@aldebaran.com 22 | */ 23 | 24 | RobotUtils = (function(self) { 25 | 26 | /*--------------------------------------------- 27 | * Public API 28 | */ 29 | 30 | /* RobotUtils.onServices(servicesCallback, errorCallback) 31 | * 32 | * A function for using NAOqi services. 33 | * 34 | * "servicesCallback" should be a function whose arguments are the 35 | * names of NAOqi services; the callback will be called 36 | * with those services as parameters (or the errorCallback 37 | * will be called with a reason). 38 | * 39 | * Sample usage: 40 | * 41 | * RobotUtils.onServices(function(ALLeds, ALTextToSpeech) { 42 | * ALLeds.randomEyes(2.0); 43 | * ALTextToSpeech.say("I can speak"); 44 | * }); 45 | * 46 | * This is actually syntactic sugar over RobotUtils.connect() and 47 | * some basic QiSession functions, so that the code stays simple. 48 | */ 49 | self.onServices = function(servicesCallback, errorCallback) { 50 | self.connect(function(session) { 51 | var wantedServices = getParamNames(servicesCallback); 52 | var pendingServices = wantedServices.length; 53 | var services = new Array(wantedServices.length); 54 | var i; 55 | for (i = 0; i < wantedServices.length; i++) { 56 | (function (i){ 57 | session.service(wantedServices[i]).done(function(service) { 58 | services[i] = service; 59 | pendingServices -= 1; 60 | if (pendingServices == 0) { 61 | servicesCallback.apply(undefined, services); 62 | } 63 | }).fail(function() { 64 | var reason = "Failed getting a NaoQi Module: " + wantedServices[i] 65 | console.log(reason); 66 | if (errorCallback) { 67 | errorCallback(reason); 68 | } 69 | }); 70 | })(i); 71 | } 72 | }, errorCallback); 73 | } 74 | 75 | // alias, so that the code looks natural when there is only one service. 76 | self.onService = self.onServices; 77 | 78 | /* RobotUtils.subscribeToALMemoryEvent(event, eventCallback, subscribeDoneCallback) 79 | * 80 | * connects a callback to an ALMemory event. Returns a MemoryEventSubscription. 81 | * 82 | * This is just syntactic sugar over calls to the ALMemory service, which you can 83 | * do yourself if you want finer control. 84 | */ 85 | self.subscribeToALMemoryEvent = function(event, eventCallback, subscribeDoneCallback) { 86 | var evt = new MemoryEventSubscription(event); 87 | self.onServices(function(ALMemory) { 88 | ALMemory.subscriber(event).then(function (sub) { 89 | evt.setSubscriber(sub) 90 | sub.signal.connect(eventCallback).then(function(id) { 91 | evt.setId(id); 92 | if (subscribeDoneCallback) subscribeDoneCallback(id) 93 | }); 94 | }, 95 | onALMemoryError); 96 | }); 97 | return evt; 98 | } 99 | 100 | /* RobotUtils.connect(connectedCallback, failureCallback) 101 | * 102 | * connectedCallback should take a single argument, a NAOqi session object 103 | * 104 | * This function is mostly meant for intenral use, for your app you 105 | * should probably use the more specific RobotUtils.onServices or 106 | * RobotUtils.subscribeToALMemoryEvent. 107 | * 108 | * There can be several calls to .connect() in parallel, only one 109 | * session will be created. 110 | */ 111 | self.connect = function(connectedCallback, failureCallback) { 112 | if (self.session) { 113 | // We already have a session, don't create a new one 114 | connectedCallback(self.session); 115 | return; 116 | } 117 | else if (pendingConnectionCallbacks.length > 0) { 118 | // A connection attempt is in progress, just add this callback to the queue 119 | pendingConnectionCallbacks.push(connectedCallback); 120 | return; 121 | } 122 | else { 123 | // Add self to the queue, but create a new connection. 124 | pendingConnectionCallbacks.push(connectedCallback); 125 | } 126 | 127 | var qimAddress = null; 128 | var robotlibs = '/libs/'; 129 | if (self.robotIp) { 130 | // Special case: we're doing remote debugging on a robot. 131 | robotlibs = "http://" + self.robotIp + "/libs/"; 132 | qimAddress = self.robotIp + ":80"; 133 | } 134 | 135 | function onConnected() { 136 | var numCallbacks = pendingConnectionCallbacks.length; 137 | for (var i = 0; i < numCallbacks; i++) { 138 | pendingConnectionCallbacks[i](self.session); 139 | } 140 | } 141 | 142 | getScript(robotlibs + 'qimessaging/1.0/qimessaging.js', function() { 143 | self.session = new QiSession(qimAddress); 144 | self.session.socket().on('connect', onConnected); 145 | self.session.socket().on('disconnect', failureCallback); 146 | }, function() { 147 | if (self.robotIp) { 148 | console.error("Failed to get qimessaging.js from robot: " + self.robotIp); 149 | } else { 150 | console.error("Failed to get qimessaging.js from this domain; host this app on a robot or add a ?robot=MY-ROBOT-IP to the URL."); 151 | } 152 | failureCallback(); 153 | }); 154 | } 155 | 156 | // public variables that can be useful. 157 | self.robotIp = _getRobotIp(); 158 | self.session = null; 159 | 160 | /*--------------------------------------------- 161 | * Internal helper functions 162 | */ 163 | 164 | // Repalement for jQuery's getScript function 165 | function getScript(source, successCallback, failureCallback) { 166 | var script = document.createElement('script'); 167 | var prior = document.getElementsByTagName('script')[0]; 168 | script.async = 1; 169 | prior.parentNode.insertBefore(script, prior); 170 | 171 | script.onload = script.onreadystatechange = function( _, isAbort ) { 172 | if(isAbort || !script.readyState || /loaded|complete/.test(script.readyState) ) { 173 | script.onload = script.onreadystatechange = null; 174 | script = undefined; 175 | 176 | if(isAbort) { 177 | if (failureCallback) failureCallback(); 178 | } else { 179 | // Success! 180 | if (successCallback) successCallback(); 181 | } 182 | } 183 | }; 184 | 185 | script.src = source; 186 | } 187 | 188 | function _getRobotIp() { 189 | var regex = new RegExp("[\\?&]robot=([^&#]*)"); 190 | var results = regex.exec(location.search); 191 | return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ").replace("/", "")); 192 | } 193 | 194 | // Helper for getting the parameters from a function. 195 | var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; 196 | function getParamNames(func) { 197 | var fnStr = func.toString().replace(STRIP_COMMENTS, ''); 198 | var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g); 199 | if(result === null) 200 | result = []; 201 | return result; 202 | }; 203 | 204 | // ALMemory helpers (event subscription requires a lot of boilerplate) 205 | 206 | function MemoryEventSubscription(event) { 207 | this._event = event; 208 | this._internalId = null; 209 | this._sub = null; 210 | this._unsubscribe = false; 211 | } 212 | 213 | MemoryEventSubscription.prototype.setId = function(id) { 214 | this._internalId = id; 215 | // as id can be receveid after unsubscribe call, defere 216 | if (this._unsubscribe) this.unsubscribe(this._unsubscribeCallback); 217 | } 218 | 219 | MemoryEventSubscription.prototype.setSubscriber = function(sub) { 220 | this._sub = sub; 221 | // as sub can be receveid after unsubscribe call, defere 222 | if (this._unsubscribe) this.unsubscribe(this._unsubscribeCallback); 223 | } 224 | 225 | MemoryEventSubscription.prototype.unsubscribe = function(unsubscribeDoneCallback) 226 | { 227 | if (this._internalId != null && this._sub != null) { 228 | evtSubscription = this; 229 | evtSubscription._sub.signal.disconnect(evtSubscription._internalId).then(function() { 230 | if (unsubscribeDoneCallback) unsubscribeDoneCallback(); 231 | }).fail(onALMemoryError); 232 | } 233 | else 234 | { 235 | this._unsubscribe = true; 236 | this._unsubscribeCallback = unsubscribeDoneCallback; 237 | } 238 | } 239 | 240 | var onALMemoryError = function(errMsg) { 241 | console.log("ALMemory error: " + errMsg); 242 | } 243 | 244 | var pendingConnectionCallbacks = []; 245 | 246 | return self; 247 | 248 | })(window.RobotUtils || {}); 249 | -------------------------------------------------------------------------------- /python/samples/sample_1_helloworld.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sample 1: just using robot_app.init() to get a QiApplication object. 3 | 4 | You should be able to run this on your computer and have it connect to 5 | the robot remotely - you should be prompted to enter your robot's address. 6 | 7 | As an extra, if you use qiq, it will suggest using that by default. 8 | 9 | You can also add --qi-url as a command-line parameter (this should 10 | be the way it's configured when installed on the robot). 11 | """ 12 | 13 | import sys 14 | sys.path.append("..") # Add stk library to Python Path, if needed 15 | 16 | import stk.runner 17 | 18 | if __name__ == "__main__": 19 | qiapp = stk.runner.init() 20 | 21 | tts = qiapp.session.service("ALTextToSpeech") 22 | tts.say("Hello, World!") 23 | -------------------------------------------------------------------------------- /python/samples/sample_2_servicecache.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sample 2: using robot_services.ServiceCache for simply accessing services. 3 | 4 | ALAddition is created in example 4, you can try this with and without running 5 | it first. 6 | """ 7 | 8 | import sys 9 | sys.path.append("..") # Add stk library to Python Path, if needed 10 | 11 | import stk.runner 12 | import stk.services 13 | 14 | if __name__ == "__main__": 15 | qiapp = stk.runner.init() 16 | services = stk.services.ServiceCache(qiapp.session) 17 | 18 | if services.ALAddition: 19 | result = services.ALAddition.add(2, 2) 20 | services.ALTextToSpeech.say("2 and 2 are " + str(result)) 21 | else: 22 | services.ALTextToSpeech.say("ALAddition not found, so watch my eyes") 23 | services.ALLeds.rasta(2.0) 24 | -------------------------------------------------------------------------------- /python/samples/sample_3_activity.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sample 3 3 | 4 | A simple activity, whose structure roughly matches a Choregraphe box. 5 | 6 | See the docstrings below for details. 7 | """ 8 | 9 | import sys 10 | sys.path.append("..") # Add stk library to Python Path, if needed 11 | 12 | import time 13 | 14 | import stk.runner 15 | import stk.services 16 | 17 | class Activity(object): 18 | "Demonstrates a simple activity." 19 | def __init__(self, qiapplication): 20 | "Necessary: __init__ must take a qiapplication as parameter" 21 | self.qiapplication = qiapplication 22 | self.services = stk.services.ServiceCache(qiapplication.session) 23 | 24 | def on_start(self): 25 | "Optional: Called at the start of the application." 26 | self.services.ALTextToSpeech.say("Let me think ...") 27 | time.sleep(2) 28 | self.services.ALTextToSpeech.say("No, nothing.") 29 | self.stop() 30 | 31 | def stop(self): 32 | "Optional: Standard way of stopping the activity." 33 | self.qiapplication.stop() 34 | 35 | def on_stop(self): 36 | "Optional: Automatically called before exit (from .stop() or SIGTERM)." 37 | pass 38 | 39 | if __name__ == "__main__": 40 | stk.runner.run_activity(Activity) 41 | -------------------------------------------------------------------------------- /python/samples/sample_4_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sample 4: A simple NAOqi service. 3 | 4 | Once it's launched, you can call ALAddition.add(1, 2) on the robot 5 | (for example, with qicli). 6 | 7 | Docstrings will be shown in qicli info ALAddition --show-doc. 8 | """ 9 | 10 | import sys 11 | sys.path.append("..") # Add stk library to Python Path, if needed 12 | 13 | import qi 14 | import stk.runner 15 | 16 | class ALAddition(object): 17 | "Powerful arithmetic service." 18 | def __init__(self, qiapplication): 19 | self.qiapplication = qiapplication 20 | 21 | @qi.bind(qi.Int32, [qi.Int32, qi.Int32]) 22 | def add(self, num_a, num_b): 23 | "Returns the sum of two numbers" 24 | return num_a + num_b 25 | 26 | @qi.bind(qi.Void, []) 27 | def stop(self): 28 | "Stops the service" 29 | self.qiapplication.stop() 30 | 31 | if __name__ == "__main__": 32 | stk.runner.run_service(ALAddition) 33 | -------------------------------------------------------------------------------- /python/samples/sample_5_logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sample 5: Logging 3 | 4 | Demonstrates stk.logging (only prints to log) 5 | """ 6 | 7 | import sys 8 | sys.path.append("..") # Add stk library to Python Path, if needed 9 | 10 | import time 11 | 12 | import stk.runner 13 | import stk.logging 14 | 15 | class ActivityWithLogging(object): 16 | "Simple activity, demonstrating logging" 17 | APP_ID = "com.aldebaran.example5" 18 | def __init__(self, qiapp): 19 | self.qiapp = qiapp 20 | self.logger = stk.logging.get_logger(qiapp.session, self.APP_ID) 21 | 22 | def on_start(self): 23 | "Called at the start of the application." 24 | self.logger.info("I'm writing to the log.") 25 | time.sleep(4) # you can try stopping the process while it sleeps. 26 | self.logger.info("Okay I'll stop now.") 27 | self.stop() 28 | 29 | def stop(self): 30 | "Standard way of stopping the activity." 31 | self.logger.warning("I've been stopped with .stop().") 32 | self.qiapp.stop() 33 | 34 | def on_stop(self): 35 | "Called after the app is stopped." 36 | self.logger.error("My process is dyyyyyiiiiinnggggg ...") 37 | 38 | if __name__ == "__main__": 39 | stk.runner.run_activity(ActivityWithLogging) 40 | -------------------------------------------------------------------------------- /python/samples/sample_6_exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sample 6: Exceptions 3 | 4 | Demonstrates decorators for exception handling 5 | """ 6 | 7 | import sys 8 | sys.path.append("..") # Add stk library to Python Path, if needed 9 | 10 | import stk.runner 11 | import stk.logging 12 | 13 | class ALLoggerDemo(object): 14 | "Simple activity, demonstrating logging and exceptions" 15 | APP_ID = "com.aldebaran.example6" 16 | def __init__(self, qiapp): 17 | self.qiapp = qiapp 18 | self.logger = stk.logging.get_logger(qiapp.session, self.APP_ID) 19 | 20 | @stk.logging.log_exceptions 21 | def compute_arithmetic_quotient(self, num_a, num_b): 22 | "Do a complicated operation on the given numbers." 23 | return num_a / num_b 24 | 25 | @stk.logging.log_exceptions_and_return(False) 26 | def is_lucky(self, number): 27 | "Is this number lucky?" 28 | return (1.0 / number) < 0.5 29 | 30 | def stop(self): 31 | "Standard way of stopping the activity." 32 | self.qiapp.stop() 33 | 34 | if __name__ == "__main__": 35 | stk.runner.run_service(ALLoggerDemo) 36 | -------------------------------------------------------------------------------- /python/samples/sample_7_events.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sample 7: Listening for events 3 | 4 | Demonstrates a couple basic ways of using events. 5 | """ 6 | 7 | import sys 8 | sys.path.append("..") # Add stk library to Python Path, if needed 9 | 10 | import stk.runner 11 | import stk.services 12 | import stk.events 13 | 14 | class EventsDemo(object): 15 | "Simple activity, demonstrating simple ways to listen to events." 16 | def __init__(self, qiapp): 17 | self.qiapp = qiapp 18 | self.events = stk.events.EventHelper(qiapp.session) 19 | self.s = stk.services.ServiceCache(qiapp.session) 20 | 21 | def on_touched(self, *args): 22 | "Callback for tablet touched." 23 | if args: 24 | self.events.disconnect("ALTabletService.onTouchDown") 25 | self.s.ALTextToSpeech.say("Yay!") 26 | self.stop() 27 | 28 | def on_start(self): 29 | "Ask to be touched, waits, and exits." 30 | # Two ways of waiting for events 31 | # 1) block until it's called 32 | self.s.ALTextToSpeech.say("Touch my forehead.") 33 | self.events.wait_for("FrontTactilTouched") 34 | # 1) connect a callback 35 | if self.s.ALTabletService: 36 | self.events.connect("ALTabletService.onTouchDown", self.on_touched) 37 | self.s.ALTextToSpeech.say("okay, now touch my tablet.") 38 | else: 39 | self.s.ALTextToSpeech.say("oh, I don't have a tablet...") 40 | self.stop() 41 | 42 | def stop(self): 43 | "Standard way of stopping the application." 44 | self.qiapp.stop() 45 | 46 | def on_stop(self): 47 | "Cleanup" 48 | self.events.clear() 49 | 50 | if __name__ == "__main__": 51 | stk.runner.run_activity(EventsDemo) 52 | -------------------------------------------------------------------------------- /python/samples/sample_8_decorators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sample 8: Subscribing to events with a decorator 3 | 4 | """ 5 | 6 | import sys 7 | sys.path.append("..") # Add stk library to Python Path, if needed 8 | 9 | import stk.runner 10 | import stk.services 11 | import stk.events 12 | 13 | class DecoratorsDemo(object): 14 | "Simple activity, demonstrating connecting to events with decorators." 15 | def __init__(self, qiapp): 16 | self.qiapp = qiapp 17 | self.events = stk.events.EventHelper(qiapp.session) 18 | self.s = stk.services.ServiceCache(qiapp.session) 19 | 20 | @stk.events.on("FrontTactilTouched") 21 | def on_touched(self, value): 22 | "Callback for tablet touched." 23 | if value: 24 | self.s.ALTextToSpeech.say("Finished!") 25 | self.stop() 26 | 27 | @stk.events.on("LeftBumperPressed") 28 | def on_left_bumper(self, value): 29 | "Callback for tablet touched." 30 | if value: 31 | self.s.ALTextToSpeech.say("Bing!") 32 | 33 | @stk.events.on("RightBumperPressed") 34 | def on_right_bumper(self, value): 35 | "Callback for tablet touched." 36 | if value: 37 | self.s.ALTextToSpeech.say("Bong!") 38 | 39 | @stk.events.on("HandLeftBackTouched") 40 | def on_left_hand(self, value): 41 | "Callback for tablet touched." 42 | if value: 43 | self.s.ALTextToSpeech.say("Ping!") 44 | 45 | @stk.events.on("HandRightBackTouched") 46 | def on_right_hand(self, value): 47 | "Callback for tablet touched." 48 | if value: 49 | self.s.ALTextToSpeech.say("Pong!") 50 | 51 | def on_start(self): 52 | "Connects all touch events" 53 | self.s.ALTextToSpeech.say("Touch me!") 54 | # Until you call this, the decorators are not connected 55 | self.events.connect_decorators(self) 56 | print "(all connected)" 57 | 58 | def stop(self): 59 | "Standard way of stopping the application." 60 | self.qiapp.stop() 61 | 62 | def on_stop(self): 63 | "Cleanup" 64 | self.events.clear() # automatically disconnects all decorators 65 | 66 | if __name__ == "__main__": 67 | stk.runner.run_activity(DecoratorsDemo) 68 | -------------------------------------------------------------------------------- /python/samples/sample_9_coroutines.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sample 9: using coroutines to handle async tasks. 3 | """ 4 | 5 | import sys 6 | sys.path.append("..") # Add stk library to Python Path, if needed 7 | 8 | import time 9 | 10 | import stk.runner 11 | import stk.events 12 | import stk.services 13 | import stk.logging 14 | import stk.coroutines 15 | 16 | class Activity(object): 17 | "Demonstrates cororoutine async control" 18 | APP_ID = "com.aldebaran.coroutines-demo" 19 | def __init__(self, qiapp): 20 | self.qiapp = qiapp 21 | self.events = stk.events.EventHelper(qiapp.session) 22 | self.s = stk.services.ServiceCache(qiapp.session) 23 | self.logger = stk.logging.get_logger(qiapp.session, self.APP_ID) 24 | 25 | def on_start(self): 26 | "Start activity callback." 27 | self._run() 28 | 29 | @stk.coroutines.async_generator 30 | def _sub_run(self, thing): 31 | "Example sub-function" 32 | yield self.s.ALTextToSpeech.say("ready", _async=True) 33 | time.sleep(1) 34 | yield self.s.ALTextToSpeech.say("%s %s" % (thing, thing), _async=True) 35 | 36 | @stk.coroutines.async_generator 37 | def _run(self): 38 | "series of async calls turned into a future." 39 | try: 40 | reco = (yield self.s.ALMemory.getData("LastWordRecognizedErr", 41 | _async=True)) 42 | print "got?", reco 43 | except RuntimeError as exc: 44 | reco = "unknown" 45 | print "got runtime error on ALMemory", exc 46 | yield self.s.ALTextToSpeech.say("Last word is " + reco, _async=True) 47 | yield self._sub_run("dingo") 48 | yield self.s.ALLeds.rasta(0.5, _async=True) 49 | yield self.s.ALTextToSpeech.say("World", _async=True) 50 | self.stop() 51 | 52 | def stop(self): 53 | "Standard way of stopping the application." 54 | self.qiapp.stop() 55 | 56 | def on_stop(self): 57 | "Cleanup" 58 | self.logger.info("Application finished.") 59 | self.events.clear() 60 | print "cleanup finished" 61 | 62 | if __name__ == "__main__": 63 | stk.runner.run_activity(Activity) 64 | -------------------------------------------------------------------------------- /python/stk/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | STK - A collection of libraries useful for making apps with NAOqi. 3 | """ 4 | -------------------------------------------------------------------------------- /python/stk/coroutines.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper for easily doing async tasks with coroutines. 3 | 4 | It's mostly syntactic sugar that removes the need for .then and .andThen. 5 | 6 | Simply: 7 | - make a generator function that yields futures (e.g. from qi.async) 8 | - add the decorator async_generator 9 | 10 | For example: 11 | 12 | @stk.coroutines.async_generator 13 | def run_test(self): 14 | yield ALTextToSpeech.say("ready", _async=True) 15 | yield ALTextToSpeech.say("steady", _async=True) 16 | time.sleep(1) 17 | yield ALTextToSpeech.say("go", _async=True) 18 | 19 | ... this will turn run_test into a function that returns a future that is 20 | valid when the call is done - and that is still cancelable (your robot will 21 | start speaking). 22 | 23 | As your function now returns a future, it can be used in "yield run_test()" in 24 | another function wrapped with this decorator. 25 | """ 26 | 27 | __version__ = "0.1.2" 28 | 29 | __copyright__ = "Copyright 2017, Aldebaran Robotics / Softbank Robotics Europe" 30 | __author__ = 'ekroeger' 31 | __email__ = 'ekroeger@softbankrobotics.com' 32 | 33 | import functools 34 | import time 35 | import threading 36 | 37 | import qi 38 | 39 | class _MultiFuture(object): 40 | """Internal helper for handling lists of futures. 41 | 42 | The callback will only be called once, with either an exception or a 43 | list of the right type and size. 44 | """ 45 | def __init__(self, futures, callback, returntype): 46 | self.returntype = returntype 47 | self.callback = callback 48 | self.expecting = len(futures) 49 | self.values = [None] * self.expecting 50 | self.failed = False 51 | self.futures = futures 52 | for i, future in enumerate(futures): 53 | future.then(lambda fut: self.__handle_part_done(i, fut)) 54 | 55 | def __handle_part_done(self, index, future): 56 | "Internal callback for when a sub-function is done." 57 | if self.failed: 58 | # We already raised an exception, don't do anything else. 59 | return 60 | assert self.expecting, "Got more callbacks than expected!" 61 | try: 62 | self.values[index] = future.value() 63 | except Exception as exception: 64 | self.failed = True 65 | self.callback(exception=exception) 66 | return 67 | self.expecting -= 1 68 | if not self.expecting: 69 | # We have all the values 70 | self.callback(self.returntype(self.values)) 71 | 72 | def cancel(self): 73 | "Cancel all subfutures." 74 | for future in self.futures: 75 | future.cancel() 76 | 77 | class FutureWrapper(object): 78 | "Abstract base class for objects that pretend to be a future." 79 | def __init__(self): 80 | self.running = True 81 | self.promise = qi.Promise(self._on_future_cancelled) 82 | self.future = self.promise.future() 83 | self._exception = "" 84 | self.lock = threading.Lock() 85 | 86 | def _on_future_cancelled(self, promise): 87 | """If someone from outside cancelled our future - propagate.""" 88 | promise.setCanceled() 89 | 90 | def then(self, callback): 91 | """Add function to be called when the future is done; returns a future. 92 | 93 | The callback will be called with a (finished) future. 94 | """ 95 | if self.running: # We might want a mutex here... 96 | return self.future.then(callback) 97 | else: 98 | callback(self) 99 | # return something? (to see when we have a testcase for this...) 100 | 101 | def andThen(self, callback): 102 | """Add function to be called when the future is done; returns a future. 103 | 104 | The callback will be called with a return value (for now, None). 105 | """ 106 | if self.running: # We might want a mutex here... 107 | return self.future.andThen(callback) 108 | else: 109 | callback(self.future.value()) #? 110 | # return something? (to see when we have a testcase for this...) 111 | 112 | def hasError(self): 113 | "Was there an error in one of the generator calls?" 114 | return bool(self._exception) 115 | 116 | def wait(self): 117 | "Blocks the thread until everything is finished." 118 | self.future.wait() 119 | 120 | def isRunning(self): 121 | "Is the sequence of generators still running?" 122 | return self.future.isRunning() 123 | 124 | def value(self): 125 | """Blocks the thread, and returns the final generator return value. 126 | 127 | For now, always returns None.""" 128 | if self._exception: 129 | raise self._exception 130 | else: 131 | return self.future.value() 132 | 133 | def hasValue(self): 134 | "Tells us whether the generator 1) is finished and 2) has a value." 135 | # For some reason this doesn't do what I expected 136 | # self.future.hasValue() returns True even if we're not finished (?) 137 | if self.running: 138 | return False 139 | elif self._exception: 140 | return False 141 | else: 142 | return self.future.hasValue() 143 | 144 | def isFinished(self): 145 | "Is the generator finished?" 146 | return self.future.isFinished() 147 | 148 | def error(self): 149 | "Returns the error of the future." 150 | return self.future.error() 151 | 152 | def isCancelable(self): 153 | "Is this future cancelable? Yes, it always is." 154 | return True 155 | 156 | def cancel(self): 157 | "Cancel the future, and stop executing the sequence of actions." 158 | with self.lock: 159 | self.running = False 160 | self.promise.setCanceled() 161 | 162 | def isCanceled(self): 163 | "Has this already been cancelled?" 164 | return not self.running 165 | 166 | def addCallback(self, callback): 167 | "Add function to be called when the future is done." 168 | self.then(callback) 169 | 170 | # You know what? I'm not implementing unwrap() because I don't see a 171 | # use case. 172 | 173 | 174 | class GeneratorFuture(FutureWrapper): 175 | "Future-like object (same interface) made for wrapping a generator." 176 | def __init__(self, generator): 177 | FutureWrapper.__init__(self) 178 | self.generator = generator 179 | self.future.addCallback(self.__handle_finished) 180 | self.sub_future = None 181 | self.__ask_for_next() 182 | 183 | def __handle_finished(self, future): 184 | "Callback for when our future finished for any reason." 185 | if self.running: 186 | # promise was directly finished by someone else - cancel all! 187 | self.running = False 188 | if self.sub_future: 189 | self.sub_future.cancel() 190 | 191 | def __handle_done(self, future): 192 | "Internal callback for when the current sub-function is done." 193 | try: 194 | self.__ask_for_next(future.value()) 195 | except Exception as exception: 196 | self.__ask_for_next(exception=exception) 197 | 198 | def __finish(self, value): 199 | "Finish and return." 200 | with self.lock: 201 | self.running = False 202 | self.promise.setValue(value) 203 | 204 | def __ask_for_next(self, arg=None, exception=None): 205 | "Internal - get the next function in the generator." 206 | if self.running: 207 | try: 208 | self.sub_future = None 209 | if exception: 210 | future = self.generator.throw(exception) 211 | else: 212 | future = self.generator.send(arg) 213 | if isinstance(future, list): 214 | self.sub_future = _MultiFuture(future, self.__ask_for_next, 215 | list) 216 | elif isinstance(future, tuple): 217 | self.sub_future = _MultiFuture(future, self.__ask_for_next, 218 | tuple) 219 | elif isinstance(future, Return): 220 | # Special case: we returned a special "Return" object 221 | # in this case, stop execution. 222 | self.__finish(future.value) 223 | else: 224 | future.then(self.__handle_done) 225 | self.sub_future = future 226 | except StopIteration: 227 | self.__finish(None) 228 | except Exception as exc: 229 | with self.lock: 230 | self._exception = exc 231 | self.running = False 232 | self.promise.setError(str(exc)) 233 | # self.__finish(None) # May not be best way of finishing? 234 | 235 | def async_generator(func): 236 | """Decorator that turns a future-generator into a future. 237 | 238 | This allows having a function that does a bunch of async actions one 239 | after the other without awkward "then/andThen" syntax, returning a 240 | future-like object (actually a GeneratorFuture) that can be cancelled, etc. 241 | """ 242 | @functools.wraps(func) 243 | def function(*args, **kwargs): 244 | "Wrapped function" 245 | return GeneratorFuture(func(*args, **kwargs)) 246 | return function 247 | 248 | def public_async_generator(func): 249 | """Variant of async_generator that returns an actual future. 250 | 251 | This allows you to expose it through a qi interface (on a service), but 252 | that means cancel will not stop the whole chain. 253 | """ 254 | @functools.wraps(func) 255 | def function(*args, **kwargs): 256 | "Wrapped function" 257 | return GeneratorFuture(func(*args, **kwargs)).future 258 | return function 259 | 260 | class Return(object): 261 | "Use to wrap a return function " 262 | def __init__(self, value): 263 | self.value = value 264 | 265 | MICROSECONDS_PER_SECOND = 1000000 266 | 267 | class _Sleep(FutureWrapper): 268 | "Helper class that behaves like an async 'sleep' function" 269 | def __init__(self, time_in_secs): 270 | FutureWrapper.__init__(self) 271 | time_in_microseconds = int(MICROSECONDS_PER_SECOND * time_in_secs) 272 | self.fut = qi.async(self.set_finished, delay=time_in_microseconds) 273 | 274 | def set_finished(self): 275 | "Inner callback, finishes the future." 276 | with self.lock: 277 | self.promise.setValue(None) 278 | 279 | sleep = _Sleep 280 | -------------------------------------------------------------------------------- /python/stk/events.py: -------------------------------------------------------------------------------- 1 | """ 2 | stk.events.py 3 | 4 | Provides misc. wrappers for ALMemory and Signals (using the same syntax for 5 | handling both). 6 | """ 7 | 8 | __version__ = "0.1.1" 9 | 10 | __copyright__ = "Copyright 2015, Aldebaran Robotics" 11 | __author__ = 'ekroeger' 12 | __email__ = 'ekroeger@aldebaran.com' 13 | 14 | import qi 15 | 16 | 17 | def on(*keys): 18 | """Decorator for connecting a callback to one or several events. 19 | 20 | Usage: 21 | 22 | class O: 23 | @on("MyMemoryKey") 24 | def my_callback(self,value): 25 | print "I was called!", value 26 | 27 | o = O() 28 | events = EventHelper() 29 | events.connect_decorators(o) 30 | 31 | After that, whenever MyMemoryKey is raised, o.my_callback will be called 32 | with the value. 33 | """ 34 | def decorator(func): 35 | func.__event_keys__ = keys 36 | return func 37 | return decorator 38 | 39 | 40 | class EventHelper(object): 41 | "Helper for ALMemory; takes care of event connections so you don't have to" 42 | 43 | def __init__(self, session=None): 44 | self.session = None 45 | self.almemory = None 46 | if session: 47 | self.init(session) 48 | self.handlers = {} # a handler is (subscriber, connections) 49 | self.subscriber_names = {} 50 | self.wait_value = None 51 | self.wait_promise = None 52 | 53 | def init(self, session): 54 | "Sets the NAOqi session, if it wasn't passed to the constructor" 55 | self.session = session 56 | self.almemory = session.service("ALMemory") 57 | 58 | def connect_decorators(self, obj): 59 | "Connects all decorated methods of target object." 60 | for membername in dir(obj): 61 | member = getattr(obj, membername) 62 | if hasattr(member, "__event_keys__"): 63 | for event in member.__event_keys__: 64 | self.connect(event, member) 65 | 66 | def connect(self, event, callback): 67 | """Connects an ALMemory event or signal to a callback. 68 | 69 | Note that some events trigger side effects in services when someone 70 | subscribes to them (such as WordRecognized). Those will *not* be 71 | triggered by this function, for those, use .subscribe(). 72 | """ 73 | if event not in self.handlers: 74 | if "." in event: 75 | # if we have more than one ".": 76 | service_name, signal_name = event.split(".") 77 | service = self.session.service(service_name) 78 | self.handlers[event] = (getattr(service, signal_name), []) 79 | else: 80 | # It's a "normal" ALMemory event. 81 | self.handlers[event] = ( 82 | self.almemory.subscriber(event).signal, []) 83 | signal, connections = self.handlers[event] 84 | connection_id = signal.connect(callback) 85 | connections.append(connection_id) 86 | return connection_id 87 | 88 | def subscribe(self, event, attachedname, callback): 89 | """Subscribes to an ALMemory event so as to notify providers. 90 | 91 | This is necessary for things like WordRecognized.""" 92 | connection_id = self.connect(event, callback) 93 | dummyname = "on_" + event.replace("/", "") 94 | self.almemory.subscribeToEvent(event, attachedname, dummyname) 95 | self.subscriber_names[event] = attachedname 96 | return connection_id 97 | 98 | def disconnect(self, event, connection_id=None): 99 | "Disconnects a connection, or all if no connection is specified." 100 | if event in self.handlers: 101 | signal, connections = self.handlers[event] 102 | if connection_id: 103 | if connection_id in connections: 104 | signal.disconnect(connection_id) 105 | connections.remove(connection_id) 106 | else: 107 | # Didn't specify a connection ID: remove all 108 | for connection_id in connections: 109 | signal.disconnect(connection_id) 110 | del connections[:] 111 | if event in self.subscriber_names: 112 | name = self.subscriber_names[event] 113 | self.almemory.unsubscribeToEvent(event, name) 114 | del self.subscriber_names[event] 115 | 116 | def clear(self): 117 | "Disconnect all connections" 118 | for event in list(self.handlers): 119 | self.disconnect(event) 120 | 121 | def get(self, key): 122 | "Gets ALMemory value." 123 | return self.almemory.getData(key) 124 | 125 | def get_int(self, key): 126 | "Gets ALMemory value, cast as int." 127 | try: 128 | return int(self.get(key)) 129 | except RuntimeError: 130 | # Key doesn't exist 131 | return 0 132 | except ValueError: 133 | # Key exists, but can't be parsed to int 134 | return 0 135 | 136 | def set(self, key, value): 137 | "Sets value of ALMemory key." 138 | return self.almemory.raiseEvent(key, value) 139 | 140 | def remove(self, key): 141 | "Remove key from ALMemory." 142 | try: 143 | self.almemory.removeData(key) 144 | except RuntimeError: 145 | pass 146 | 147 | def _on_wait_event(self, value): 148 | "Internal - callback for an event." 149 | if self.wait_promise: 150 | self.wait_promise.setValue(value) 151 | self.wait_promise = None 152 | 153 | def _on_wait_signal(self, *args): 154 | "Internal - callback for a signal." 155 | if self.wait_promise: 156 | self.wait_promise.setValue(args) 157 | self.wait_promise = None 158 | 159 | def cancel_wait(self): 160 | "Cancel the current wait (raises an exception in the waiting thread)" 161 | if self.wait_promise: 162 | self.wait_promise.setCanceled() 163 | self.wait_promise = None 164 | 165 | def wait_for(self, event, subscribe=False): 166 | """Block until a certain event is raised, and returns it's value. 167 | 168 | If you pass subscribe=True, ALMemory.subscribeToEvent will be called 169 | (sometimes necessary for side effects, i.e. WordRecognized). 170 | 171 | This will block a thread so you should avoid doing this too often! 172 | """ 173 | if self.wait_promise: 174 | # there was already a wait in progress, cancel it! 175 | self.wait_promise.setCanceled() 176 | self.wait_promise = qi.Promise() 177 | if subscribe: 178 | connection_id = self.subscribe(event, "EVENTHELPER", 179 | self._on_wait_event) 180 | elif "." in event: # it's a signal 181 | connection_id = self.connect(event, self._on_wait_signal) 182 | else: 183 | connection_id = self.connect(event, self._on_wait_event) 184 | try: 185 | result = self.wait_promise.future().value() 186 | finally: 187 | self.disconnect(event, connection_id) 188 | return result 189 | -------------------------------------------------------------------------------- /python/stk/logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | stk.logging.py 3 | 4 | Utility library for logging with qi. 5 | """ 6 | 7 | __version__ = "0.1.2" 8 | 9 | __copyright__ = "Copyright 2015, Aldebaran Robotics" 10 | __author__ = 'ekroeger' 11 | __email__ = 'ekroeger@aldebaran.com' 12 | 13 | import functools 14 | import traceback 15 | 16 | import qi 17 | 18 | 19 | def get_logger(session, app_id): 20 | """Returns a qi logger object.""" 21 | logger = qi.logging.Logger(app_id) 22 | try: 23 | qicore = qi.module("qicore") 24 | log_manager = session.service("LogManager") 25 | provider = qicore.createObject("LogProvider", log_manager) 26 | log_manager.addProvider(provider) 27 | except RuntimeError: 28 | # no qicore, we're not running on a robot, it doesn't matter 29 | pass 30 | except AttributeError: 31 | # old version of NAOqi - logging will probably not work. 32 | pass 33 | return logger 34 | 35 | 36 | def log_exceptions(func): 37 | """Catches all exceptions in decorated method, and prints them. 38 | 39 | Attached function must be on an object with a "logger" member. 40 | """ 41 | @functools.wraps(func) 42 | def wrapped(self, *args): 43 | try: 44 | return func(self, *args) 45 | except Exception as exc: 46 | self.logger.error(traceback.format_exc()) 47 | raise exc 48 | return wrapped 49 | 50 | 51 | def log_exceptions_and_return(default_value): 52 | """If an exception occurs, print it and return default_value. 53 | 54 | Attached function must be on an object with a "logger" member. 55 | """ 56 | def decorator(func): 57 | @functools.wraps(func) 58 | def wrapped(self, *args): 59 | try: 60 | return func(self, *args) 61 | except Exception: 62 | self.logger.error(traceback.format_exc()) 63 | return default_value 64 | return wrapped 65 | return decorator 66 | -------------------------------------------------------------------------------- /python/stk/runner.py: -------------------------------------------------------------------------------- 1 | """ 2 | stk.runner.py 3 | 4 | A helper library for making simple standalone python scripts as apps. 5 | 6 | Wraps some NAOqi and system stuff, you could do all this by directly using the 7 | Python SDK, these helper functions just isolate some frequently used/hairy 8 | bits so you don't have them mixed in your logic. 9 | """ 10 | 11 | __version__ = "0.1.3" 12 | 13 | __copyright__ = "Copyright 2015, Aldebaran Robotics" 14 | __author__ = 'ekroeger' 15 | __email__ = 'ekroeger@aldebaran.com' 16 | 17 | import sys 18 | import qi 19 | from distutils.version import LooseVersion 20 | 21 | # 22 | # Helpers for making sure we have a robot to connect to 23 | # 24 | 25 | 26 | def check_commandline_args(description): 27 | "Checks whether command-line parameters are enough" 28 | import argparse 29 | parser = argparse.ArgumentParser(description=description) 30 | parser.add_argument('--qi-url', help='connect to specific NAOqi instance') 31 | 32 | args = parser.parse_args() 33 | return args 34 | 35 | 36 | def is_on_robot(): 37 | "Returns whether this is being executed on an Aldebaran robot." 38 | import platform 39 | return "aldebaran" in platform.platform() 40 | 41 | 42 | def get_debug_robot(): 43 | "Returns IP address of debug robot, complaining if not found" 44 | try: 45 | import qiq.config 46 | qiqrobot = qiq.config.defaultHost() 47 | if qiqrobot: 48 | robot = raw_input( 49 | "connect to which robot? (default is {0}) ".format(qiqrobot)) 50 | if robot: 51 | return robot 52 | else: 53 | return qiqrobot 54 | else: 55 | print "qiq found, but it has no default robot configured." 56 | except ImportError: 57 | # qiq not installed 58 | print "qiq not installed (you can use it to set a default robot)." 59 | return raw_input("connect to which robot? ") 60 | 61 | 62 | def init(qi_url=None): 63 | "Returns a QiApplication object, possibly with interactive input." 64 | if qi_url: 65 | sys.argv.extend(["--qi-url", qi_url]) 66 | else: 67 | args = check_commandline_args('Run the app.') 68 | if bool(args.qi_url): 69 | qi_url = args.qi_url 70 | elif not is_on_robot(): 71 | print "no --qi-url parameter given; interactively getting debug robot." 72 | debug_robot = get_debug_robot() 73 | if debug_robot: 74 | sys.argv.extend(["--qi-url", debug_robot]) 75 | qi_url = debug_robot 76 | else: 77 | raise RuntimeError("No robot, not running.") 78 | 79 | qiapp = None 80 | sys.argv[0] = str(sys.argv[0]) 81 | 82 | # In versions bellow 2.3, look for --qi-url in the arguemnts and call accordingly the Application 83 | if qi_url and hasattr(qi, "__version__") and LooseVersion(qi.__version__) < LooseVersion("2.3"): 84 | qiapp = qi.Application(url="tcp://"+qi_url+":9559") 85 | # In versions greater than 2.3 the ip can simply be passed through argv[0] 86 | else: 87 | # In some environments sys.argv[0] has unicode, which qi rejects 88 | qiapp = qi.Application() 89 | 90 | qiapp.start() 91 | return qiapp 92 | 93 | 94 | # Main runner 95 | 96 | def run_activity(activity_class, service_name=None): 97 | """Instantiate the given class, and runs it. 98 | 99 | The given class must take a qiapplication object as parameter, and may also 100 | have on_start and on_stop methods, that will be called before and after 101 | running it.""" 102 | qiapp = init() 103 | activity = activity_class(qiapp) 104 | service_id = None 105 | 106 | try: 107 | # if it's a service, register it 108 | if service_name: 109 | # Note: this will fail if there is already a service. Unregistering 110 | # it would not be a good practice, because it's process would still 111 | # be running. 112 | service_id = qiapp.session.registerService(service_name, activity) 113 | 114 | if hasattr(activity, "on_start"): 115 | def handle_on_start_done(on_start_future): 116 | "Custom callback, for checking errors" 117 | if on_start_future.hasError(): 118 | try: 119 | msg = "Error in on_start(), stopping application: %s" \ 120 | % on_start_future.error() 121 | if hasattr(activity, "logger"): 122 | activity.logger.error(msg) 123 | else: 124 | print msg 125 | finally: 126 | qiapp.stop() 127 | qi.async(activity.on_start).addCallback(handle_on_start_done) 128 | 129 | # Run the QiApplication, which runs until someone calls qiapp.stop() 130 | qiapp.run() 131 | 132 | finally: 133 | # Cleanup 134 | if hasattr(activity, "on_stop"): 135 | # We need a qi.async call so that if the class is single threaded, 136 | # it will wait for callbacks to be finished. 137 | qi.async(activity.on_stop).wait() 138 | if service_id: 139 | qiapp.session.unregisterService(service_id) 140 | 141 | 142 | def run_service(service_class, service_name=None): 143 | """Instantiate the given class, and registers it as a NAOqi service. 144 | 145 | The given class must take a qiapplication object as parameter, and may also 146 | have on_start and on_stop methods, that will be called before and after 147 | running it. 148 | 149 | If the service_name parameter is not given, the classes' name will be used. 150 | """ 151 | if not service_name: 152 | service_name = service_class.__name__ 153 | run_activity(service_class, service_name) 154 | -------------------------------------------------------------------------------- /python/stk/services.py: -------------------------------------------------------------------------------- 1 | """ 2 | stk.services.py 3 | 4 | Syntactic sugar for accessing NAOqi services. 5 | """ 6 | 7 | __version__ = "0.1.2" 8 | 9 | __copyright__ = "Copyright 2015, Aldebaran Robotics" 10 | __author__ = 'ekroeger' 11 | __email__ = 'ekroeger@aldebaran.com' 12 | 13 | 14 | class ServiceCache(object): 15 | "A helper for accessing NAOqi services." 16 | 17 | def __init__(self, session=None): 18 | self.session = None 19 | self.services = {} 20 | if session: 21 | self.init(session) 22 | 23 | def init(self, session): 24 | "Sets the session object, if it wasn't passed to constructor." 25 | self.session = session 26 | 27 | def __getattr__(self, servicename): 28 | "We overload this so (instance).ALMotion returns the service, or None." 29 | if (not servicename in self.services) or ( 30 | servicename == "ALTabletService"): 31 | # ugly hack: never cache ALtabletService, always ask for a new one 32 | if servicename.startswith("__"): 33 | # Behave like a normal python object for those 34 | raise AttributeError 35 | try: 36 | self.services[servicename] = self.session.service(servicename) 37 | except RuntimeError: # Cannot find service 38 | self.services[servicename] = None 39 | return self.services[servicename] 40 | -------------------------------------------------------------------------------- /python/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Jan 4 15:20:32 2017 4 | 5 | @author: ekroeger 6 | """ 7 | 8 | import stk.runner 9 | import stk.services 10 | 11 | import pytest 12 | 13 | def pytest_addoption(parser): 14 | parser.addoption("--qiurl", action="store", 15 | help="URL of the robot to connect to") 16 | 17 | g_qiapp = None 18 | 19 | @pytest.fixture 20 | def qiapp(request): 21 | global g_qiapp 22 | if not g_qiapp: 23 | qiurl = request.config.getoption("--qiurl") 24 | g_qiapp = stk.runner.init(qiurl) 25 | return g_qiapp 26 | 27 | @pytest.fixture 28 | def services(request): 29 | _qiapp = qiapp(request) 30 | return stk.services.ServiceCache(_qiapp.session) 31 | 32 | -------------------------------------------------------------------------------- /python/tests/test_async.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for stk.coroutines 3 | 4 | Created on Wed Jan 4 11:37:45 2017 5 | 6 | @author: ekroeger 7 | """ 8 | 9 | import time 10 | 11 | import pytest 12 | 13 | import stk.coroutines 14 | 15 | TEST_KEY = "TestAsync/TestMemKey" 16 | 17 | # PROMISE: 18 | ['future', 'isCancelRequested', 'setCanceled', 'setError', 'setValue'] 19 | 20 | # Parts of the future API 21 | TESTED = ['value', 'then', 'andThen', 'hasError', 'wait', 'isRunning', 22 | 'hasValue', 'isFinished', 'error', 'cancel', 'isCancelable', 23 | 'isCanceled', 'addCallback', ] 24 | 25 | # Not tested: 26 | UNWANTED = ['unwrap'] 27 | 28 | def test_memory(services): 29 | "The code in the generator is executed." 30 | services.ALMemory.raiseEvent(TEST_KEY, 0) 31 | @stk.coroutines.async_generator 32 | def run_mem(): 33 | yield services.ALMemory.raiseEvent(TEST_KEY, 1, _async=True) 34 | run_mem().wait() 35 | assert services.ALMemory.getData(TEST_KEY) == 1 36 | 37 | def test_memory_value(services): 38 | "The code in the generator is executed." 39 | services.ALMemory.raiseEvent(TEST_KEY, 0) 40 | @stk.coroutines.async_generator 41 | def run_mem(): 42 | yield services.ALMemory.raiseEvent(TEST_KEY, 1, _async=True) 43 | assert run_mem().value() == None 44 | assert services.ALMemory.getData(TEST_KEY) == 1 45 | 46 | def test_then(services): 47 | "The callback is called when it's over." 48 | services.ALMemory.raiseEvent(TEST_KEY, 0) 49 | @stk.coroutines.async_generator 50 | def run_mem(): 51 | yield services.ALMemory.raiseEvent(TEST_KEY, 1, _async=True) 52 | future = run_mem() 53 | cb_called = [False] 54 | def on_done(cb_future): 55 | assert not cb_future.hasError(False) 56 | cb_called[0] = True 57 | future.then(on_done) 58 | future.value() 59 | time.sleep(0.01) # Make sure everything is done 60 | assert cb_called[0] 61 | 62 | def test_callback(services): 63 | "The callback is called when it's over." 64 | services.ALMemory.raiseEvent(TEST_KEY, 0) 65 | @stk.coroutines.async_generator 66 | def run_mem(): 67 | yield services.ALMemory.raiseEvent(TEST_KEY, 1, _async=True) 68 | future = run_mem() 69 | cb_called = [False] 70 | def on_done(cb_future): 71 | assert not cb_future.hasError(False) 72 | cb_called[0] = True 73 | assert future.addCallback(on_done) == None 74 | future.value() 75 | time.sleep(0.01) # Make sure everything is done 76 | assert cb_called[0] 77 | 78 | def test_and_then(services): 79 | "The callback is called when it's over." 80 | services.ALMemory.raiseEvent(TEST_KEY, 0) 81 | @stk.coroutines.async_generator 82 | def run_mem(): 83 | yield services.ALMemory.raiseEvent(TEST_KEY, 1, _async=True) 84 | future = run_mem() 85 | cb_called = [False] 86 | def on_done(value): 87 | assert value == None 88 | cb_called[0] = True 89 | future.andThen(on_done) 90 | future.value() 91 | time.sleep(0.01) # Make sure everything is done 92 | assert cb_called[0] 93 | 94 | def test_memory2(services): 95 | "The code in the generator is executed asynchronously." 96 | services.ALMemory.raiseEvent(TEST_KEY, 0) 97 | @stk.coroutines.async_generator 98 | def run_mem(): 99 | yield services.ALMemory.raiseEvent(TEST_KEY, 1, _async=True) 100 | time.sleep(0.2) 101 | yield services.ALMemory.raiseEvent(TEST_KEY, 2, _async=True) 102 | future = run_mem() 103 | assert services.ALMemory.getData(TEST_KEY) == 1 104 | assert future.isRunning() 105 | assert not future.hasValue() 106 | assert not future.isFinished() 107 | future.wait() # wait 108 | assert future.isFinished() 109 | assert not future.isRunning() 110 | assert future.hasValue() 111 | assert services.ALMemory.getData(TEST_KEY) == 2 112 | 113 | def test_exception(qiapp): 114 | "Exceptions raised in the generator are propagated." 115 | services = stk.services.ServiceCache(qiapp.session) 116 | @stk.coroutines.async_generator 117 | def func(): 118 | assert False, "Nope" 119 | yield services.ALMemory.raiseEvent(TEST_KEY, 0, _async=True) 120 | with pytest.raises(AssertionError): 121 | func().value() 122 | 123 | def test_wrong_key(qiapp): 124 | "An exception is raised when the ALMemory key doesn't exist." 125 | services = stk.services.ServiceCache(qiapp.session) 126 | @stk.coroutines.async_generator 127 | def func(): 128 | yield services.ALMemory.getData("TestAsync/MemKeyThatDoesntExist", 129 | _async=True) 130 | with pytest.raises(RuntimeError): 131 | func().value() 132 | 133 | def test_wrong_function(qiapp): 134 | "An exception is raised when the function doesn't exist." 135 | services = stk.services.ServiceCache(qiapp.session) 136 | @stk.coroutines.async_generator 137 | def func(): 138 | yield services.ALMemory.doesntExist(_async=True) 139 | with pytest.raises(AttributeError): 140 | func().value() 141 | 142 | 143 | def test_exception_in_future(qiapp): 144 | "Exceptions raised in the generator are propagated." 145 | services = stk.services.ServiceCache(qiapp.session) 146 | @stk.coroutines.async_generator 147 | def func(): 148 | assert False, "Nope" 149 | yield services.ALMemory.raiseEvent(TEST_KEY, 0, _async=True) 150 | future = func() 151 | cb_called = [False] 152 | def on_done(cb_future): 153 | assert cb_future.hasError() 154 | cb_called[0] = True 155 | future.then(on_done) 156 | try: 157 | future.value() 158 | except AssertionError: 159 | pass 160 | assert future.hasError() 161 | assert "assert" in future.error() 162 | assert future.isFinished() 163 | assert not future.hasValue() 164 | time.sleep(0.01) 165 | assert cb_called[0] 166 | 167 | def test_cancel(services): 168 | "The code in the generator is executed asynchronously." 169 | services.ALMemory.raiseEvent(TEST_KEY, 0) 170 | @stk.coroutines.async_generator 171 | def run_mem(): 172 | yield services.ALMemory.raiseEvent(TEST_KEY, 1, _async=True) 173 | time.sleep(0.3) 174 | # Dummy, unfortunately necessary. 175 | yield services.ALMemory.getData(TEST_KEY, _async=True) 176 | yield services.ALMemory.raiseEvent(TEST_KEY, 2, _async=True) 177 | future = run_mem() 178 | time.sleep(0.1) 179 | assert future.isCancelable() 180 | assert not future.isCanceled() 181 | future.cancel() 182 | assert future.isCanceled() 183 | assert not future.isRunning() 184 | assert services.ALMemory.getData(TEST_KEY) == 1 185 | time.sleep(0.3) 186 | assert services.ALMemory.getData(TEST_KEY) == 1 187 | 188 | 189 | def test_cancel_public(services): 190 | "The code in the generator is executed asynchronously." 191 | services.ALMemory.raiseEvent(TEST_KEY, 100) 192 | @stk.coroutines.public_async_generator 193 | def run_mem(): 194 | yield services.ALMemory.raiseEvent(TEST_KEY, 101, _async=True) 195 | time.sleep(0.3) 196 | # Dummy, unfortunately necessary. 197 | yield services.ALMemory.getData(TEST_KEY, _async=True) 198 | yield services.ALMemory.raiseEvent(TEST_KEY, 102, _async=True) 199 | future = run_mem() 200 | time.sleep(0.1) 201 | #assert future.isCancelable() 202 | assert not future.isCanceled() 203 | future.cancel() 204 | # qi raw future is not canceled if you cancel it -_- 205 | #assert future.isCanceled() 206 | #assert future.isFinished() 207 | #assert not future.isRunning() 208 | assert services.ALMemory.getData(TEST_KEY) == 101 209 | time.sleep(0.3) 210 | assert services.ALMemory.getData(TEST_KEY) == 101 211 | 212 | 213 | def test_sleep(services): 214 | "Sleep works correctly." 215 | services.ALMemory.raiseEvent(TEST_KEY, 10) 216 | @stk.coroutines.async_generator 217 | def run_test(): 218 | yield services.ALMemory.raiseEvent(TEST_KEY, 11, _async=True) 219 | print "I'm gonna create my future" 220 | try: 221 | sleep_fut = stk.coroutines.sleep(0.2) 222 | except Exception as e: 223 | import traceback 224 | traceback.print_exc() 225 | print "um, now what?" 226 | yield services.ALMemory.raiseEvent(TEST_KEY, 12, _async=True) 227 | print "oh yeah wait for that future" 228 | yield sleep_fut 229 | print "did the promise finish like it should?" 230 | yield services.ALMemory.raiseEvent(TEST_KEY, 13, _async=True) 231 | fut = run_test() 232 | time.sleep(0.1) 233 | assert services.ALMemory.getData(TEST_KEY) == 12 234 | time.sleep(0.2) 235 | assert services.ALMemory.getData(TEST_KEY) == 13 236 | time.sleep(0.2) 237 | 238 | 239 | def test_set_promise(services): 240 | "You coroutine can prematurely finish a future by setting it's promise." 241 | services.ALMemory.raiseEvent(TEST_KEY, 0) 242 | 243 | global future_sub 244 | future_sub = None 245 | 246 | @stk.coroutines.async_generator 247 | def run_mem(): 248 | global future_sub 249 | yield services.ALMemory.raiseEvent(TEST_KEY, 21, _async=True) 250 | future_sub = run_sub() 251 | yield future_sub 252 | yield services.ALMemory.raiseEvent(TEST_KEY, 24, _async=True) 253 | 254 | @stk.coroutines.async_generator 255 | def run_sub(): 256 | yield services.ALMemory.raiseEvent(TEST_KEY, 22, _async=True) 257 | yield stk.coroutines.sleep(0.2) 258 | yield services.ALMemory.raiseEvent(TEST_KEY, 23, _async=True) 259 | 260 | future = run_mem() 261 | time.sleep(0.1) 262 | # WARNING: sometimes this test fails with value = 21; race condition? 263 | assert services.ALMemory.getData(TEST_KEY) == 22 264 | future_sub.promise.setValue("SUCCESS") 265 | time.sleep(0.2) 266 | assert services.ALMemory.getData(TEST_KEY) == 24 267 | time.sleep(0.2) 268 | 269 | def test_set_promise_multi(services): 270 | "You coroutine can prematurely finish a future by setting it's promise." 271 | services.ALMemory.raiseEvent(TEST_KEY, 0) 272 | 273 | global future_sub 274 | future_sub = None 275 | 276 | @stk.coroutines.async_generator 277 | def run_mem(): 278 | global future_sub 279 | yield services.ALMemory.raiseEvent(TEST_KEY, 31, _async=True) 280 | future_sub = run_sub() 281 | yield future_sub 282 | yield services.ALMemory.raiseEvent(TEST_KEY, 35, _async=True) 283 | 284 | @stk.coroutines.async_generator 285 | def run_sub_sub(): 286 | yield stk.coroutines.sleep(0.3) 287 | yield services.ALMemory.raiseEvent(TEST_KEY, 33, _async=True) 288 | 289 | @stk.coroutines.async_generator 290 | def run_sub(): 291 | yield services.ALMemory.raiseEvent(TEST_KEY, 32, _async=True) 292 | pair = [stk.coroutines.sleep(0.2), run_sub_sub()] 293 | yield pair 294 | yield services.ALMemory.raiseEvent(TEST_KEY, 34, _async=True) 295 | 296 | future = run_mem() 297 | time.sleep(0.1) 298 | # WARNING: sometimes this test fails with value = 31; race condition? 299 | assert services.ALMemory.getData(TEST_KEY) == 32 300 | future_sub.promise.setValue("SUCCESS") 301 | time.sleep(0.5) 302 | assert services.ALMemory.getData(TEST_KEY) == 35 303 | 304 | def test_return(services): 305 | "functions can return a value with coroutines.Return." 306 | @stk.coroutines.async_generator 307 | def run_return(): 308 | yield stk.coroutines.Return(42) 309 | assert run_return().value() == 42 310 | 311 | def test_return_stops_exec(services): 312 | "coroutines.Return acts like normal return, and stops execution." 313 | state = ["NOT_STARTED"] 314 | @stk.coroutines.async_generator 315 | def run_return(): 316 | state[0] = "STARTED" 317 | yield stk.coroutines.Return(42) 318 | state[0] = "WENT_TOO_FAR" 319 | assert run_return().value() == 42 320 | assert state[0] == "STARTED" 321 | time.sleep(0.1) 322 | assert state[0] == "STARTED" 323 | 324 | def test_yield_list(services): 325 | "A list of futures works like the future of a list." 326 | @stk.coroutines.async_generator 327 | def run_a(): 328 | yield stk.coroutines.Return("A") 329 | @stk.coroutines.async_generator 330 | def run_b(): 331 | yield stk.coroutines.Return("B") 332 | @stk.coroutines.async_generator 333 | def run_yield_list(): 334 | values = yield [run_a(), run_b()] 335 | assert values == ["A", "B"] 336 | yield stk.coroutines.Return("OK") 337 | assert run_yield_list().value() == "OK" 338 | 339 | def test_yield_tuple(services): 340 | "A tuple of futures works like the future of a tuple." 341 | @stk.coroutines.async_generator 342 | def run_a(): 343 | yield stk.coroutines.Return("A") 344 | @stk.coroutines.async_generator 345 | def run_b(): 346 | yield stk.coroutines.Return("B") 347 | @stk.coroutines.async_generator 348 | def run_yield_list(): 349 | values = yield (run_a(), run_b()) 350 | assert values == ("A", "B") 351 | yield stk.coroutines.Return("OK") 352 | assert run_yield_list().value() == "OK" 353 | 354 | 355 | def test_multisleep(services): 356 | "Sleep works correctly." 357 | services.ALMemory.raiseEvent(TEST_KEY, 0) 358 | @stk.coroutines.async_generator 359 | def run_test(): 360 | yield services.ALMemory.raiseEvent(TEST_KEY, 1, _async=True) 361 | yield [stk.coroutines.sleep(0.2), stk.coroutines.sleep(0.2)] 362 | yield services.ALMemory.raiseEvent(TEST_KEY, 2, _async=True) 363 | fut = run_test() 364 | time.sleep(0.1) 365 | assert services.ALMemory.getData(TEST_KEY) == 1 366 | time.sleep(0.2) 367 | assert services.ALMemory.getData(TEST_KEY) == 2 368 | 369 | def test_sleep_more(services): 370 | "Sleep works correctly." 371 | time_in_sec = 0.01 372 | @stk.coroutines.async_generator 373 | def run_test(): 374 | yield stk.coroutines.sleep(time_in_sec) 375 | 376 | cpt1 = 0 377 | 378 | while cpt1 < 20:# 2000 if you *really* want to be sure 379 | fut = run_test() 380 | time.sleep(time_in_sec) 381 | 382 | print "final bf", cpt1 383 | 384 | try: 385 | # print "async" 386 | fut.cancel() 387 | # qi.async(fut.cancel()) 388 | # print "after qi async" 389 | # if(not fut.isFinished()): 390 | # print "\n***************\n", cpt1 391 | # fut.cancel() 392 | except Exception as e: 393 | pass 394 | assert fut.isCanceled() 395 | assert not fut.isRunning() 396 | 397 | 398 | cpt1 +=1 399 | 400 | 401 | 402 | if __name__ == "__main__": 403 | pytest.main(['--qiurl', '10.0.204.255']) 404 | 405 | -------------------------------------------------------------------------------- /qiproject.xml: -------------------------------------------------------------------------------- 1 | 2 | Herman Kuntz 3 | Emile Kroeger 4 | Jerome Jeannin 5 | Jessica Eichberg 6 | Olivier Laugier 7 | Walid Bekhtaoui 8 | Thalia Cruz 9 | Erwan Pinault 10 | Pierre Fribourg 11 | Thibaut Marbache 12 | Alexandre Mazel 13 | Jonas Lerebours 14 | 15 | 16 | --------------------------------------------------------------------------------