├── .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 |
--------------------------------------------------------------------------------