############### Python Tutorial ############### .. default-domain:: py .. contents:: .. python API is derived from C++-API. .. maybe orient on ROS' turtle bot tutorial http://wiki.ros.org/Robots/TurtleBot This tutorial shows how to implement processes which use the Python API. It assumes that you have just read the preceding chapter on :doc:`tutorial_modules_and_configuration`, because we will continue the elevator example given there, sticking closely to it, and for brevity we will not repeat any explanation on it. (In case you really would like more to program in C++, you can just continue to :doc:`tutorial_cpp` without missing out on needed conceptual information). Setting up the Environment ========================== Before you can start, you possibly need to set up your environment so that the :program:`ln_manager` program as well as the Python libraries are found. This can be done by setting the environment variable :envvar:`PYTHONPATH` to the folder where LN was installed. For a local install using SCons, this will be something like :file:`/usr/local/lib/python....` . .. only:: rmc If you are using conan, you can use the following shell commands, which install the required libraries and programs, and set the environment accordingly: .. sourcecode:: bash conan install links_and_nodes_manager/[~2]@common/stable -if conan -g virtualenv -g virtualenv_python -g virtualrunenv for f in conan/activate*.sh; do source $f; done Here, you should replace ``links_and_nodes_manager/[~2]@common/stable`` by the current LN version, which is |DOCUMENTED_LN_MANAGER_VERSION|. Writing the LN Manager Configuration ==================================== Following our example, we can now write the main LN manager configuration, by starting with copying from the example given in :ref:`tutorial/grouping_processes`, in the tutorial introduction on process management: .. literalinclude:: examples/tutorial/lnm_configuration/hierarchical_grouping.lnc :language: lnc :linenos: It comes handy that we already have three processes. We just need to rename these and change the command lines so that they match our system written in Python: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/elevator-1.lnc :language: lnc :emphasize-lines: 12,16,23 :linenos: Here, we placed both the hardware and sensor control into the process group elevator_hardware, and let the UI depend on the elevator_hardware/control process, which in turn depends on the elevator_hardware/elevator process. That process is implemented by the elevator_simulation Python program. .. index:: pair: order of dependencies; tutorial (Python) pair: depends-on directive; tutorial (Python) .. _tutorial_python/designing-dependencies: .. rubric:: Among communicating processes, which process should depend on another? In respect to the ``depends_on`` directive, you might wonder which is the right order of dependencies in such cases, when one process is a service client and another is a service provider? The general rule is: Low-level processes like hardware controllers which provide a service or data should be started before higher-level ones, and service providers should be started before clients of that service. This is especially important for the case of services, because service calls do :term:`block ` by default, and mutual calls could cause a :term:`deadlock` (see [#how-to-handle-circular-dependencies]_ for details and on how to fix that). In practical terms, this means: 1. The elevator simulation is the lowest-level process, so it should be started first and depend on no other process. (If we had real actors controlled by firmware, and sensors, we would start both of them independently from each other, as the bottom level of the system.) 2. The controller depends on the simulation, and should be started after it. 3. The UI depends on the controller as its service provider, and is started last. .. index:: pair: ready-regexp; tutorial (C++) In order to synchronize hardware simulation, controller and UI, we use the "ready-regexp" directive introduced in section :ref:`tutorial/lnm/synchronizing-start-up/by-ready-regex`, which causes the LN manager to wait until a process that it started prints a certain string. For example, the ``elevator_simulation`` process will print "elevator hardware running", and with this directive in line 13, the LN manager will wait until it has reached this point: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/elevator-2.lnc :language: lnc :emphasize-lines: 13,19 :linenos: If you focus for a moment on the implementation of the hardware part, and the communication within it, there is a difference between the real elevator and the simulated elevator: In a real elevator, sensors and actors are independent, the former just deliver information, and the latter just perform actions. In a simulation however, the actions should move and adjust the simulated sensor readings. This is why we place them into a single process. There are two things which we need to change so that the Python programs can run. One is that we need to invoke the Python interpreter to run our scripts written in Python. We will use Python3 here: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/elevator-2.lnc :language: lnc :emphasize-lines: 12,16,23 :linenos: Second, we need to provide Python with the right environment. This can be done by passing the PYTHONPATH environment variables with which the LN Manager is running, and adding the ``user_execvpe`` flag, which tells the LN manager to pass the needed environment to the Python3 interpreter. Also, we need to adjust the work directory (:term:`CWD`) to the current directory the LNM is running in, and thirdly, we need tell Python in which sub-directory the different scripts can be found, so that we can use paths relative to the CWD, like so: With this, our program looks like this: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/elevator-3.lnc :language: lnc :emphasize-lines: 12-15,19-23,29-32 :linenos: One more thing is missing: we need to tell the LNM where to find our message definitions which we are going to use. We do this by inserting this line after line 4: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/elevator-4.lnc :language: lnc :emphasize-lines: 6 :lines: 1-8 :linenos: This directive is a global directive, we place it between the instance section and the process sections. Testing the new configuration ----------------------------- We can already run this configuration like this: .. code:: bash ln_manager -c examples/tutorial/example_python_elevator/code-snippets/elevator-4.lnc we get: .. figure:: images/tutorial_elevator_processes.png :alt: LNM running the elevator configuration The LN manager running the draft elevator configuration Well, when we try to start the elevator UI, the response will be this: .. figure:: images/tutorial_elevator_processes-cannot-start.png :alt: LNM running the elevator configuration Error indication when LN tries to run a non-existent program .. _tutorial/python/ln-manager-how-to-inspect-error-message: .. rubric:: Error messages and diagnosis in the LN manager .. index:: pair: LN manager; how to inspect error messages single: diagnosing missing processes Dang, what went wrong? If we click the "log" tab, the LN manager shows this error message: .. figure:: images/tutorial_elevator_processes-cannot-start-log.png :alt: LNM running the elevator configuration Error message when LN tries to run a non-existent program The solution to this is easy: Of course, the LN manager cannot start programs which do not yet exist. So, next thing we do is we are going to write them. But before that, it is useful to give you a rough sketch of each client API and how it works. .. index:: triple: tutorial (Python); inter-process communication; overview single: inter-process communication in LN; overview seealso: IPC; inter-process communication pair: client object; overview on communication see: ln.client; client object see: links_and_nodes.client; client object pair: port object; overview on communication pair: publish(); overview on communication pair: subscribe(); overview on communication pair: port.read(); overview on communication pair: port.write(); overview on communication pair: port.packet; overview on communication see: read(); port.read() see: write(); port.write() see: packet; port.packet single: time stamps; of logged messages single: synchronization; of messages exchanged by read() and write() .. _tutorial/Python_API/overview: Using the Communication Functions in the Client API: A First Overview ===================================================================== Before we go into the details how the different processes can communicate with each other, here a bird's eye view how it all works: * Whenever a program wants to use LN for :term:`inter-process communication`, it first needs to instantiate a :term:`client object` which registers the module with the :term:`LN manager`. The parameters if the client object are a unique name for the client instance, and optionally the program's command line arguments. * Now, there are two fundamental mechanisms for communication, :term:`publish/subscribe communication` and :term:`LN services `. * In the case of publish/subscribe communication, the process first needs to create a :term:`port object`. This is done with one of two methods of a **client object**, either the **client.publish()** method, or the **client.subscribe()** method. * Both the ``publish()`` and the ``subscribe()`` methods are called with two arguments. The first is the :term:`topic` to which the client wants to attach to, and the second is a :term:`message definition`, which defines the data type of the message. Usually, message definitions are associated with topics. .. note:: As with all other facilities for inter-process communication, in time-critical or performance-critical code, this initialization of communication should be done only once at the start of the program, because it is much slower than the actual communication. * Both the ``publish()`` and ``subscribe()`` methods create a :term:`port object`. A port object is an object instance that consists of a data member into which the parts of a message can be copied, and one of two I/O methods, either ``port.write()``, or port.read()``. The ``write()`` method is available for output ports which publish data, and the ``read()`` methods are available for input ports which were subscribed to a topic. * The ``read()`` method can be either :term:`blocking` (which means that it wait until data has been transmitted), or :term:`non-blocking` (which means they only try without waiting to communicate), or it can also have a :term:`time-out` parameter (which means that they wait, but only up to a maximum time). * before a ``write()``, and after a ``read()``, the data which is to be transmitted can be copied into or out of the ``packet`` member of the port object, whose members correspond to the elements of the message definition, and also have their respective type. * Success of the communication is indicated by the Boolean return values of ``read()`` or ``write()``, where ``True`` means that new data was transmitted, and ``False`` that there was no data. * both ``read()`` and ``write()`` also transport time stamps which allow to related different messages with each other. .. index:: pair: service.req; overview on communication pair: service.response; overview on communication pair: service.call(); overview on communication pair: services_wrapper; overview on communication pair: wrap_service(); overview on communication pair: wrap_service_provider(); overview on communication see: ln.wrap_service_provider(); overview on communication see: ln.services_wrapper; see services_wrapper * In the case of LN service communication, both clients and service providers need to retrieve a service object after instantiating the LN client object. For this, there exist two methods, one called :py:meth:`links_and_nodes.client.get_service()` for getting a service object for the service client, and :py:meth:`links_and_nodes.client.get_service_provider()` for a service provider program, to get an object reference to a service provider. There are two kind of service objects, client service objects, and provider service objects. Both belong to an initialized LN client, and have the name of the service and the name of the corresponding message definition for requests of that service as parameters. * Then, the service client class can define methods for calling services. Each of them has to assign the call parameters to a request buffer, perform the call using a call method, and read back the response data from the request buffer object. * Setting up the service provider class is a little more complex. After getting the service object reference, two functions need to be called: 1. :py:meth:`links_and_nodes.service_provider.set_handler()`, which sets a specific method to be called when a service call request is received. 2. The other method is :py:meth:`links_and_nodes.service_provider.do_register()`, which tells the LN manager and the rest of the LN system that the process which calls ``do_register()`` will handle these service requests. * After this registration step, the handler method that is called when a service request arrives needs to be implemented. All of this might sound still a bit abstract, and your head might be a bit spinning from all these new terms. Don't give up, we will address that right now by going step by step through the concrete example of the elevator service. After that section aimed to make the interface tangible, we will in :ref:`a detailed summary section ` return to the LN client API methods and discuss their properties and interface in a bit more detail, and also link to the more formal and detailed descriptions in the :doc:`reference_python` chapter, so that you can look up further information when needed. Implementing the Elevator Control Processes =========================================== This section covers the two processes which control the elevator - the ``elevator_simulation.py`` program and the ``controller.py`` program. .. index:: pair: publish/subscribe communication; example single: publish/subscribe; tutorial (Python) pair: topics; tutorial (Python) Communicating with publish/subscribe topic messages --------------------------------------------------- We set up the communication between controller and hardware via public/subscribe messages on topics. Because programs do not need to wait for messages, this is a good match to real-time control loops. Also, it is often possible to avoid threads just by checking for new messages and processing them quickly in a main event loop. The Elevator Hardware Simulation ................................ .. index:: pair: Python; using publish/subscribe communication If you look again at our :ref:`figure/tutorial/elevator-components-and-messages`, there are two components of the hardware which send messages: The floor count sensor, which is a general example for a sensor, and the motor, which is an actor. Because they share some state, we implement them in a single program. Nevertheless, they communicate via two different topics. We will name these topics``elevator03.actors`` and ``elevator03.sensors``, because we want to use elevator number 3 for our hardware tests, when the hardware arrives. Both topics will transmit different messages, which would make it easier to de-couple them in later implementations, and also would make it straightforward to add other sensors (for example, an emergency stop button) if we need to do that. We can implement both sensor and motor as two functions which share one element of global shared state, the current position. (In a more complex program, such shared-state elements should be made members of an elevator object, to avoid globally shared values. But we do not need this for this short example.) But before we do that, we need to cover the basics of registering, sending and receiving messages. So, let's write a program called :file:`python/elevator_simulation.py` First, the program needs to use the ``links_and_nodes`` Python module. So, we just need to import it: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/python/elevator-1.py :language: python :emphasize-lines: 1 :linenos: We assign the ``links_and_nodes`` module the short name ``ln``. This is a convention, which is useful to adhere to because we will need to type a lot less. .. important:: If the ``import links_and_nodes`` statement fails when running the program later, this is almost certainly because of some missing environment setup. Publishing and receiving Data in the elevator process ..................................................... .. index:: single: topics; connecting clients Next, we need to connect to the topics. This is done by creating two clients, and creating ports: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/python/elevator-2.py :language: python :emphasize-lines: 3-7 :linenos: These two instruction sequences each do two things: 1. First, they instantiate and register an :term:`LN client`. We need to instantiate such a client in every program in order to communicate via LN. Each client needs an unique name. As you see, a program can register more than one client. We do this here just to emphasize that the two functions of sensors and actors are in principle separate things which could run in separate programs. 2. Second, they create a port object by registering with a topic either as a **publisher** (line 4) or as a **subscriber** (line 7). In the hardware simulation, we register as a *publisher* for the sensor data, and as a *subscriber* for motor control commands. In addition to the topic, we also need to register a message definition which specifies the type of messages that we want to send. Note that we begin the port and client objects with an underscore - this means that they are internal to the :term:`module`. Now, we need two functions which publish and receive data. We go the way to show you the core operation first, and go on to embed them in a more structured way after that. Publishing the Sensor Data .......................... .. index:: single: topics; publishing data First, write a function that publishes the current floor count: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/python/elevator-3.py :language: python :emphasize-lines: 10-15 :linenos: There is some magic involved here but fortunately, it is easy to explain. Here is what happens: *When initializing a ``port`` object, it gets a ``package`` member, and this in turn has member variables which correspond one-to-one to each element in the message definition which the port was connected with.* To these elements, we assign the data which we want to send, and then we call the ``write()`` method of the port, which sends the data. This is all we need to do. There is one more detail needed: As mentioned, we have an alarm system for fire detection, and it is important that fire-related exceptions are handled correctly. So, we need to add this to the simulation. However, testing with real smoke on the premises turned out to be a bit hazardous, even when using KN95 masks. So, instead of checking for the appearance of physical smoke, we just look for "smoke" in the file system, more precisely, in the current work directory (:term:`CWD`) -- using ``path.exists()``. The resulting code is this: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/python/elevator-3a.py :language: python :emphasize-lines: 2, 16-17 :linenos: Receiving Motor Control Commands as a Subscriber ................................................ .. index:: single: port.read(); example single: port.write(); example We can do just the same with the motor control port, by writing a function ``receive_commands()``, that uses the ``port.read()`` method to receive a message: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/python/elevator-4.py :language: python :emphasize-lines: 21-22 :linenos: Here, the ``port.read()`` method returns either ``None`` (if no data was received, we will come soon to this case), or a ``packet`` object which contains the new data. To make the meaning of the commands easier to understand, we add a class which serves as an enumeration: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/python/elevator-5.py :language: python :emphasize-lines: 11-14 :linenos: Now, we can write a very simple control loop: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/python/elevator-6.py :language: python :emphasize-lines: 3-4, 42-62 :linenos: .. note:: For brevity, we have used the ``numpy.sign()`` function here. For our purpose, it simply returns -1 if a number is negative, +1 if a number is positive, and 0 if it is zero (this numpy function also has well-defined edge cases for symbolic floating point values which we are not using here). (For more specific information on this function, `here is a link to the numpy docs `_.) Extracting API Constants from the Hardware Simulation ..................................................... As we mentioned before, the constants which define the movement direction are used in the messages. As such, they are part of the public :term:`API` of the software :term:`modules `, and should be documented and kept separate from the implementation. Therefore, we extract lines 10 - 13 of the ``elevator.py`` process into a Python module that we call ElevatorConstants.py. We also add constants which cover the possible errors of the system. They are part of the interface, too. .. note:: Why do we add them that early? Adding error codes or new exceptions breaks backward-compatibility of an :term:`interface`, because clients which rely on earlier versions cannot handle these errors without change. So, it is a good idea to clearly define all possible errors from the start, so that :term:`interfaces ` can be kept stable later. Here, we add both error messages, and numerical error codes. They have double purpose: Error messages are nicer to work on for humans, while discrete error codes are necessary for programmatical error handling. Later, we will show how to transport an exception across the :term:`interface`, which will conveniently bundle together these two pieces of information. .. literalinclude:: examples/tutorial/example_python_elevator/python/ElevatorConstants.py :language: python :linenos: The Controller Process ...................... The controller process, which we name :file:`python/controller.py`, has essentially three parts: 1. One part communicates with the hardware simulation (or when the elevator hardware finally arrives, it will communicate with the real hardware). This code is almost completely symmetrical to the corresponding controller code - with one exception: Publisher and subscriber of the messages are swapped. 2. The second part communicates with the user interface :term:`module`. This is done with an :term:`LN service`, which we will show soon. 3. The third part is a bit of actual logic which converts UI requests into meaningful commands to the hardware, and returns when these are completed. So, let's start with the hardware communication: .. _tutorial/python/receiving-data : Receiving Sensor Messages ......................... For receiving sensor messages, this code does what we need: .. sourcecode:: python import collections ElevatorStatus = collections.namedtuple( "ElevatorStatus", ["floor_number", "movement_direction", "smoke_detected"] ) _clnt = ln.client("elevator, floor count sensor") _sensor_port = clnt.subscribe("elevator03.sensors", "elevator/sensors/floor_count") def receive_current_sensor_data(): _sensor_port.read() return ElevatorStatus( _sensor_port.packet.floor_number, _sensor_port.packet.movement_direction, _sensor_port.packet.smoke_detected, ) .. hint:: The call to collections.namedtuple defines a new container class with class name 'ElevatorStatus' with conveniently named members. See `the official standard library documentation for collections.namedtuple `_ for details. Using Time-Outs ............... .. index:: pair: time-out; publish/subscribe communication pair: time-out; port.read() triple: publish/subscribe; using timeouts; tutorial (Python) single: blocking calls single: non-blocking calls single: time-outs; introduction see: setting time-outs; time-out There is something which we have to consider now: We are reading the sensor data in a single event loop. This makes things a lot easier. However, if we use ``port.read()`` without parameters, this will cause a **blocking call**, which means that the program will suspend execution here and wait until some data arrives. For real-time control purposes, this is often not what we need. In such cases, we need to use either **non-blocking calls**, or set a time-out. Non-blocking calls do not wait at all, and they are used if the ``read()`` method is passed a Boolean ``False`` as an argument. For simplicity, we are going to use time-outs. They are invoked when the ``read()`` method gets a floating point number passed; the value determines the maximum number of seconds until the function returns, when it has not received data. .. sourcecode:: python def receive_current_sensor_data(timeout=0.1): _sensor_port.read(timeout) return ElevatorStatus( _sensor_port.packet.floor_number, _sensor_port.packet.movement_direction, _sensor_port.packet.smoke_detected, ) If the ``port.read()`` call times out, the ``packet`` data is unchanged, so it simply returns the old state. .. Note:: We could also use the return value of ``port.read()``: It is the new packet (which is :term:`truthy`), if the read succeeds, and ``None`` (which is :term:`falsy`), if the read times out. Sending Command Messages ........................ For sending commands to the elevator motor, we can just use: .. sourcecode:: python _actor_port = clnt.publish("elevator03.actors", "elevator/actors/motor_control") def send_hw_command(move_command): actor_port.packet.move_command = move_command _actor_port.write() As you see, here we use the same client instance to create another port. .. index:: single: time-out; port.write() The ``port.write()`` method does not have time-outs, because it just writes the data to a buffer, which will never block, and in a correct set-up will never fail (it could raise an exception if something goes wrong). Connecting UI and Controller with a Service ------------------------------------------- .. index:: pair: using LN services; client example triple: service client; wrap_service; tutorial (Python) Now, we can talk with the hardware. But what is still missing, is the connection with the user interface. We start with the client side, and create this now as follows: .. _tutorial/python/example/calling_service_client: Calling the Service from the UI ............................... For the user interface, we create a new program :file:`python/ui.py`. First thing, we need to import the ``links_and_nodes`` as usual: .. sourcecode:: python import links_and_nodes as ln Then, we create a service client and a service client handle from it as follows: .. index:: single: ln.services_wrapper single: ln.client.wrap_service() .. literalinclude:: examples/tutorial/example_python_elevator/python/ui.py :language: python :emphasize-lines: 3,5 :start-at: class ElevatorServiceClient :end-before: def prompt :linenos: :lineno-match: This creates a new client for ``elevator03.prompt``, and defines an object reference to a LN service client instance, using the method :py:meth:`links_and_nodes.client.get_service()`. We have chosen the service name ``elevator03.prompt`` to just make a point that topic and service names are really not required to be the same as the names of message definitions which are used by them - as explained in section :ref:`tutorial/message_definitions/names`, they are different things which can have different names if convenient (similar as the name of a file on disk, and a variable designating a file object in a program are different things). What is needed now is a method of ``ElevatorServiceClient`` that calls the service. Here it is: .. literalinclude:: examples/tutorial/example_python_elevator/python/ui.py :language: python :emphasize-lines: 2,3,7,8 :linenos: :start-at: def prompt :end-before: if __name__ == :lineno-match: What happens here is as follows: 1. In line 26, we simply get a shorter name to the service object that we got from the LN client, and call it ``svc``. 2. The service request has in our case a single parameter, with the name ``requested_floor``. In line 27, we assign this single parameter to the corresponding member in the buffer for call parameters. This buffer which has been set up automatically, and is called ``svc.req``, with a member for each field of the message definition. So, because our message definition contained a parameter ``requested_floor``, we assign the value of the method parameter "floor_number" to the field ``svc.req.requested_floor``. The fields have been generated automatically from the message definition, and the buffer was provided with the above call to ``get_service``. 3. In line 28, we call the service, using the function :py:meth:`links_and_nodes.service.call()`. What is does is as follows: It copies the data in the request parameters into an *internal* buffer, sends a message with its content to a service provider, and waits for the result, which is then copied back. The resulting parameters will appear as members of the field ``svc.resp``. 4. Before we return the result, we need to check for errors, which should usually raise an exception. This is done by testing whether ``svc.resp.error_message`` has a length larger than zero, or ``svc.resp.error_code`` has a non-zero value. If that is the case, we raise the exception with ``error_message`` and ``error_code`` as its constructor parameters. 5. Otherwise, we return the result data record in a dictionary, which holds both the error code, and the number of the floor where we arrived at. (This is the more general form. In the example, we could also just return the resulting floor number, because the error code will always be zero). Now we are almost done for the client part of the service communication. What this code does is that it first connects the newly constructed service client (which was created in the constructor of ``ElevatorServiceClient``) to the message definition ``elevator/request/elevator_call``, and also to a registered method ``prompt``. The call to the prompt() method will then be forwarded to the controller program, which takes the role of the LN service provider. .. _tutorial/python/example/service_provider: The Response Method in the Controller ..................................... .. index:: pair: using ln services; provider example pair: ln.service_provider; introduction pair: wrap_service_provider(); introduction pair: handle_service_group_requests(); introduction As the counterpart, the controller process needs some code which provides the service and registers it with LN. This is done as follows by using the :py:meth:`links_and_nodes.client.get_service_provider()`, :py:meth:`links_and_nodes.service_provider.set_handler()`, and :py:meth:`links_and_nodes.service_provider.do_register()` [#note_on_wrapped_api]_: .. literalinclude:: examples/tutorial/example_python_elevator/python/controller.py :language: python :emphasize-lines: 1 :linenos: :start-at: class ElevatorProvider :end-before: def run(self) :lineno-match: As in the case of the service client, this code registers the message provider process to LN by instantiating a :py:class:`links_and_nodes.client()` object. The parameter is as before the suggested name for the client. Then, it gets a service object reference, using :py:meth:`links_and_nodes.client.get_service_provider()`, connecting it to a service name and message definition. Here, "elevator03.prompt" is the service name. It needs to be equal to the name used in the client. The second parameter of this method is the name of the message definition of the service call for the message definition named :file:`elevator/request/elevator_call`, which resides in :file:`msg_defs/elevator/request/elevator_call`. [#service_interface]_ Using :py:meth:`links_and_nodes.service_provider.set_handler()`, it connects an instance method with the service object reference, telling LN that the method ``self.prompt`` (for ``ElevatorProvider.prompt()``) will handle the requests. Finally, the :py:meth:`links_and_nodes.service_provider.do_register()` method call makes the service provider known to the LN manager and the clients in the system, so that it can be found and called. The parameter of the method ``default group`` assigns the handler method to a so-called :term:`service group`, which is a group or set of handlers that are processed together in one and the same thread (usually the main thread). Now, we can turn to the method that actually implements the service handler as well as our control logic. We first show the skeleton that handles the request, omitting the logic part: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/python/elevator-8.py :language: python :linenos: :emphasize-lines: 1,6,13-15,17 :start-at: def prompt( :end-before: if __name__ == :lineno-match: Focusing on the LN API elements used in the ``prompt()`` method, there are three interesting parts linked to the method's three arguments ``conn``, ``req`` and ``resp``: First, the argument ``req`` holds the request parameters as its data members in a parameter buffer. Second, the argument ``resp`` has the purpose to capture the return values in a response data buffer. And thirdly, the request is responded and finished with calling ``conn.respond()``. This method is defined as :py:meth:`links_and_nodes.connection.respond()`, and sends the data back to the service client. The still missing remaining parts of the method consists of error handling and the logic of the elevator control. We will explain them soon. But before, we need to complete a little bit of wiring to start the controller program as a service provider. This is done defining a ``run()`` method, as follows: .. literalinclude:: examples/tutorial/example_python_elevator/python/controller.py :language: python :emphasize-lines: 1 :linenos: :start-at: def run(self) :end-before: def move_elevator( :lineno-match: The ``run()`` method starts handling of the service, by calling the LN client method :meth:`links_and_nodes.client.wait_and_handle_service_group_requests()`. This methods waits for a service call to arrive, then processes it by calling the registered service handler, and then returns. The while loop ensures that requests are served continuously. The method has two parameters. The first is the name of the :term:`service group` for which handlers should be processed. We use the same name for that group as we used in the ``do_register()`` method call, ``"default group"``. The second parameter is a time-out value which sets a maximum waiting time for any service call to that group to arrive, before ``wait_and_handle_service_group_requests()`` returns. Here, we could sequentially add several calls more to the method with other group names. (It is also possible to run service handlers from different groups so that they are executed in parallel, which requires the code of the handlers to be :term:`thread-safe`, and is much more complex.) Do not use multiple application threads to poll the same service group with ``wait_and_handle_service_group_requests()``. LN serializes such calls internally as a safety measure, but the recommended design is one dispatcher thread per service group. If handlers should execute in parallel, use the service-group thread-pool API instead. As a last element of the handler skeleton, we need to call the run() method from the main part of the program, like so: .. sourcecode:: python if __name__ == "__main__": p = ElevatorProvider() p.run() The ``prompt()`` method is where the program has to invoke the motor control and use the sensor readings to actually get the elevator to the requested place; We will fill that out in a minute. Before that, we will have a look at how the service client can actually be used. Having defined the outer structures, we can now fill the service client and handlers with application code. Invoking The Service from the UI program ........................................ To use the service from the user interface, we need to do nothing more than to get the number of the requested floor, and send it with the service call method. For our first tests, we do not use a full-blown GUI but just read the floor number from the keyboard, using the `input() `_ function from the Python3 standard library [#note_on_python2]_. Also, we need to catch any exceptions which may crop up. This is done by this code: .. sourcecode:: python if __name__ == '__main__': service_client = ElevatorServiceClient() while True: floor_string = input("type the number of the floor you want to go to!") try: floor_number = int(floor_string) except ValueError: print("seems that was not a valid number") continue print("OK, sending request to go to floor", floor_number) result = service_client.prompt(floor_number) print("The result is: {}".format(result)) Implementing the Elevator Control Function .......................................... Now, there now only two main aspects missing: First, the implementation of the service call in controller.py needs to be filled out. Second, we need to get the error handling straight, especially the case of a fire alarm (we do not want the elevator to be used during in a fire, or even worse, carry unsuspecting people into a floor that is burning!). We will do this in two parts, first the implementation of the main handler function. The second part will show how to handle failures in Python, using exceptions, and how to communicate them to the service caller. Here is a very simple implementation, which uses the functions ``receive_current_sensor_data()`` and ``send_hw_command()`` that we defined in section :ref:`tutorial/python/receiving-data` to provide the service. First, we add an initialization to the constructor of the service provider class, which initializes the state correctly: .. sourcecode:: python self.current_status = receive_current_sensor_data(timeout=-1) In this case, we disable the time-out, because we want a correct value to start with. We can do this by setting the time value to -1, as here, or by passing the Boolean value ``False``. Here is the elevator control logic in the filled-out body of the ``prompt()`` method: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/python/elevator-9.py :language: python :linenos: :start-at: def prompt( :end-before: if __name__ == :lineno-match: Our "elevator algorithm" is very basic: If the elevator is still moving, we wait for it to stop. (This can happen if the controller process is re-started during movement). Then, the algorithm checks whether we are already at the target floor. In that case, it returns; otherwise, it sends a hardware command and waits that the hardware sensor messages that it is at the desired state. The counterpart is the algorithm in the elevator simulator ``elevator_simulation.py``, which we already wrote. Because the system is more responsive with it, we convert the ``received_commands()`` function to use a time-out of one second if no new commands were given. .. literalinclude:: examples/tutorial/example_python_elevator/python/elevator_simulation.py :language: python :linenos: :emphasize-lines: 3 :start-at: def receive_commands :end-at: return :lineno-start: 38 .. note:: If you have read the example for the control code carefully, you might just have noted that in line 95 of the ``prompt()`` method of the controller, and in lines 22, 27, and 31 of the elevator simulation, we are comparing to floating point values which are successively incremented by small values. How can this work? Haven't we learned that one must *never* compare for equality to floating point values? The answer is simple: IEEE-754 Floating point numbers can in fact represent *some* values precisely, and here we are incrementing and decrementing by 1/16, which is a power of two. And such values can be represented exactly in the binary mantissa of floating point values. (If you are interested to learn more about this, you should definitely read David Goldberg's `What Every Computer Scientist Should Know About Floating Point Arithmetic `_ *(March, 1991 Issue of Computing Surveys, Association for Computing Machinery)*). Error Handling using Exceptions ............................... Now, we can fix the last missing element of the implementation, handling errors. We need to do three things: 1. check whether the input is in a correct range 2. handle alarms from the smoke detector correctly, so that the elevator goes straight to the ground floor, and any people in it can leave 3. pass the information about the error to the elevator client For the internal handling of these errors, we use exceptions, which are used to communicate any kind of failure or unexpected error within Python code. Here the adapted code for the controller process: .. literalinclude:: examples/tutorial/example_python_elevator/code-snippets/python/elevator-10.py :language: python :linenos: :emphasize-lines: 65-76,102-105,135-141 In lines 76 and 135, we wrap the service response function into a try-except statement. For the case of an out-of-range floor number, we check the range in lines 66 to 76, and raise an ``ElevatorException`` if it is wrong. The arguments to the exception are both the corresponding error message, and the error code defined in :file:`ElevatorConstants.py`. In the ``except`` clause of the try-except statement in line 135 to 140, we return the floor number, the error code from the exception, and the error message. The latter is the string representation of the exception object. It contains also the error code - we will see soon why both error code and message go to the message part of the response. The remaining case happens when the field "smoke_detected" of the sensor data packet becomes true. In this case, the first part of the error response is handled within the controller process: The target floor is set to the ground floor with number zero, and the elevator is moved to it. When the ground floor is reached, another exception is raised. [#are-empty-error-fields-required]_ One last thing: The ``prompt()`` method has gotten a bit long. To make it better to read, we refactor it into two smaller methods, to get to our final code: .. literalinclude:: examples/tutorial/example_python_elevator/python/controller.py :language: python :linenos: Error Handling across Service Messages ...................................... Here is what happens when the ``error_message`` field in the response data has a non-zero length: The value of the field ``error_message_len`` is set to the length of the string. When a message with this special field arrives at a LN service client in Python, another Exception is thrown, of type ``Exception``. Here we show how it is handled in the :file:`ui.py` client: .. literalinclude:: examples/tutorial/example_python_elevator/python/ui.py :language: python :linenos: :emphasize-lines: 30-54 :start-at: from ElevatorConstants What happens here is as follows: If the Python service client receives a result message where the field "error_message" is not empty, it generates an exception of type "Exception", with the error message being the exception argument. This exception is caught in line 35, where the name ``exc`` becomes bound to the exception object. [#note-accessing-return-values]_ Because we constructed the error message from the ``str()`` representation of the initial exception, the message contains a tuple which consists of the two arguments of the ``Exception.__init__()`` constructor: The initial error message, and the error code. The function ``ast.literal_eval()`` from the Python ``ast`` module [#reference-ast]_ parses this string in line 36, returning its components again into the variables ``message`` and ``error_code``. Using these two, we can handle errors programmatically. [#note-exceptions-in-LN-2.1]_ If we forgot to handle an error (or somebody adds another error code to the service, which is so far not handled by the current client code), the initial exception is thrown again in line 54, so that we will not overlook it. .. seealso:: It is also possible to use the "wrapped" Python API to handle error responses in a concise way, see :ref:`python/service/wrapped-api/error-handling` for details. Essentially, this adds an automatically generated wrapper function, that checks the return values and raises exceptions if some configured fields indicate an error. .. rubric:: Footnotes .. [#note-client-registration-name] The client name argument passed in the :py::class:`ln.client()` constructor is actually a suggestion. The exact behavior is described in :ref:`guide/concepts/clients/client-names`. .. [#how-to-handle-circular-dependencies] Deadlocks can be caused if two or more processes or threads perform blocking calls (such as service calls) which cause them to wait on each other at the same time, triggering a circular dependency at run-time. This can essentially freeze a system and represents a serious error. In the case of :term:`message passing` communication, this is less of a problem, because it can easily be made :term:`non-blocking` in most cases. However, circular dependencies are usually a sign that something is distinctly wrong with your design, so you almost always should try hard to resolve them, for example by extracting a commonly depended-on but itself independent low-level parts. .. [#note_on_wrapped_api] The methods shown are a general interface which is versatile and covers all use cases. To provide for simpler cases, there exists also a *wrapped API* which is described in :doc:`user_guide_client_apis` with an example in :doc:`quickstart_python_services_wrapped-api`. .. [#service_interface] Some older API signatures call this parameter the service interface, which means the same thing in spite of the different name. .. [#note_on_python2] For Python 2, this is equivalent to the ``raw_input()`` function, which does not evaluate its input. .. [#are-empty-error-fields-required] You might wonder whether the Python service provider always needs to provide values for the ``error_code`` and the ``error_message`` fields even if no error was found, as we have shown in our sample. Current client implementations do in fact require that all fields that are returned are passed explicitly. In the future, this restriction could be loosened, allowing implicit default values of zero. In the meantime, you can use a dictionary filled with default values, copy it, and use Python's ``dict.update()`` to set the changed values (or, in Python 3.9, the ``dict.merge()`` operator) for the copy, and return the result from the handler. .. [#reference-ast] See `the documentation on ast.literal_eval `_. .. [#note-accessing-return-values] That these responses are automatically turned into an exception means that the other response parameters cannot be accessed via the return value of the service call. It is, however, possible to access them via the *direct API* variant, which can also be used by Python clients. In contrast to this, error responses in C++ clients need to be turned into exceptions explicitly. .. [#note-exceptions-in-LN-2.1] Starting with LN-2.1.0, the error handling mechanism is a bit more flexible which makes it possible to pass other parameters directly when an exception is raised. This is useful to filter out specific error conditions from non-critical conditions. Running the Example ................... You can run the example with: .. sourcecode:: bash cd documentation/examples/tutorial/example_pythonelevator ln_manager -c elevator.lnc In the case that you need to set your environment with conan, you can use: .. sourcecode:: bash cd documentation/examples/tutorial/example_pythonelevator conan install ./conanfile.txt source activate*sh ln_manager -c elevator.lnc Here, you should replace e.g. ``links_and_nodes_manager/[~2]@common/stable`` by the current LN version, which is |DOCUMENTED_LN_MANAGER_VERSION|. .. only:: rmc And if you are using cissy, you can do: .. sourcecode:: bash cd documentation/examples/tutorial/example_pythonelevator cissy run which will open the LN Manager GUI so that you can start the program. .. _tutorial/python/using_the_lnm_gui: Using the LN Manager GUI to control the Elevator Example ======================================================== Starting all Processes ---------------------- When you have started the LNM GUI as instructed in the last paragraph, you can start the whole system by clicking at the "elevator" process group and pressing the button "start all" in the process control panel at the upper right half: .. figure:: images/example_tutorial_starting_process_group.png Process group with all three example processes and the green "start all" LED button in the top right panel. Do you remember our first unsuccessful attempt? It failed because the processes to start did not exit yet. This time, if you press the "start all" button, all processes turn to green: .. figure:: images/example_tutorial_process_group_started.png Elevator example after all processes have been successfully started Inspecting the processes ------------------------ We can click the "ui" process, and look at its terminal output: .. figure:: images/example_tutorial_ui_process.png The console user interface of the elevator Because the output window in the bottom half is a fully functional terminal, we can just click the pane, type in a number as an elevator request, and hit "Enter". Here is what happens then: .. figure:: images/example_tutorial_ui_request.png Result of elevator request in the command line interface If we click the controller process, the terminal output is this: .. figure:: images/example_tutorial_controller_output.png Terminal output of the controller process in the bottom pane We might want to see more of the terminal output. In order to do that, we need to make the bottom pane a bit higher. We can do that by placing the mouse on the upper border of that pane, grabbing it and moving it upwards, like this: .. figure:: images/example_tutorial_controller_output_enlarged.png Enlarged terminal output of the controller process; the red circle shows where to grab the border If we click the "elevator" process label on the upper left pane, we can see the elevator output: .. figure:: images/example_tutorial_simulator_output.png Output of the simulator Inspecting Topics communication ------------------------------- If we click on the "topics" tab, we can inspect topics communication: .. figure:: images/example_tutorial_simulator_topics.png Topics tab and UI elements Now, we can click for example the "elevator03.sensors" topic, and then the button "inspect published data": .. figure:: images/example_tutorial_simulator_topics_inspection.png Button and label for inspecting sensors We get this output box: .. figure:: images/example_tutorial_inspect_sensor.png Sensor data from the elevator Inspecting Service Calls ------------------------ We can also run service and inspect calls from the LN Manager. If we click the "services" tab, and then double-click the label "request_elevator", we get a new iPython window which looks like this: .. figure:: images/example_tutorial_inspect_services.png Inspecting the call_elevator service interactively. We can, type interactive Python code into the "In" panel. For example: .. sourcecode:: python svc.req.requested_floor = 3 svc.call() pprint.pprint(svc.resp) After we press the triangular run button (symbol '▷') which is marked with the red circle, this will request the elevator and print the response. In the other terminal windows, we can watch closely how our system operates. The latter is also explained in more detail in chapter :doc:`user_guide_components_and_their_usage` in section :ref:`guide/lnm_gui/inspecting/services`. Reloading the LN Manager Configuration -------------------------------------- As described in section :ref:`tutorial/lnm/reloading-configuration` in the LN manager tutorial, we can also stop a running process, change its configuration (for example, by changing the command to a modified script), and re-start the process, without exiting the LN manager or restarting the whole system. This allows it to easily test and debug LN client programs. .. index:: pair: tutorial (Python); API summary .. _tutorial/python_api/summary_of_api: Detailed Summary on the Python Client API which we have used ============================================================ This section takes up again the topic started in section :ref:`tutorial/Python_API/overview` near the start of this chapter, and tries to condense and sort some of the heap of information that it gave. While that section had the purpose of a bird's eye view on the Python API, here we will try to give you a kind of a road map to navigate it. It will summarize the API functions which we have used, and how they fit together. At the same time, it will *not* try to bury you alive with all the details on these functions and how to use them. Giving you *all* the details is in fact the stated purpose of the :doc:`reference_api` part. You are invited to peruse this reference later, to become more familiar with the system. (The second purpose of this chapter, explanation of concepts, is continued in the :doc:`user_guide_client_apis` part of the user guide, which will also give more details on specific topics.). .. index:: pair: LN clients; API summary Client Processes ---------------- Client processes are programs which use functions the LN API for :term:`inter-process communication`. When they are started, they need to contact the :term:`LN manager` to register. For this, they need to know the :term:`host name` and :term:`port number` of the LN manager. Both can be passed either via an environment variable, or via command-line arguments (Python´s :term:`sys.argv`), which are passed to the LN API functions to evaluate the relevant options. Setting the right environment variable values is done automatically when the client is started by the LN manager. This happens when the client process is configured in the :term:`LN manager configuration file ` to be managed by the LNM. In section :ref:`tutorial/lnm/configuration/python`, we explained how to do that. In this case, you do not need to worry about how they contact the LN manager. (The specifics of connection to the LN manager, the :term:`LN daemon`, and the :term:`LN arbiter` are in detail complex, but we will not dig into that here - it is described in the :doc:`user_guide_concepts` chapter in case you need it). .. index:: pair: Python module; API summary single: links_and_nodes (Python module) seealso: ln; links_and_nodes (Python module) The Python Module ----------------- .. py:currentmodule:: links_and_nodes The name of the Python module which contains the client API is :mod:`links_and_nodes`. It is available for both Python 2 and Python 3. It is strongly recommended to import it always like this: .. sourcecode:: python import links_and_nodes as ln .. important:: If importing the module fails, this is almost always causes by either a faulty installation, or by a bug in the environment setup. See section :doc:`getting_started` and :doc:`troubleshooting` for how to diagnose and correct these. .. seealso:: :ref:`reference/python/module` in the reference. .. index:: pair: client objects; Python API summary LN Client Instance ------------------ :class:`links_and_nodes.client` objects are needed for any use of the LN communication, be it with topics, services, or parameters. .. seealso:: :ref:`reference/python/client_class` in the reference. :ref:`reference/python/service/client_objects` in the reference. .. index:: pair: client objects; creating Constructor ........... Client objects are created with the :py:class:`client` constructor, like this: .. sourcecode:: python clnt = ln.client("elevator, floor count sensor") The argument is the name of the client with with it is preferably registered by the LN Manager.[#note-client-registration-name]_ It is also possible to pass the program's command line arguments to set options to the client: .. sourcecode:: python clnt = ln.client("elevator_service_client", sys.argv) You only need one LN client object in a program (but you are allowed to use more). .. seealso:: :ref:`reference/python/client_class/constructor` in the reference. .. index:: pair: message definitions; API summary .. _tutorial/python/summary_api/message-definitions: Message Definitions ------------------- Message definitions are needed to initialize any objects that provide communication services (for example, publish/subscribe or remote procedure calls). Therefore, we want to quickly recapitulate what you need to know to start using them here: They are small pieces of text which define data types and element names of message elements, very much like a struct in C, or a dictionary or namedtuple in Python. They are independent of the programming language in which a program is implemented. Message definitions do have names which are ASCII-encode strings which serve to identify them. They form part of a system's API and should not be changed after they have been published to other users. .. seealso:: :ref:`tutorial/message_definitions` in the tutorial. Publish/Subscribe Definitions ............................. As we have seen above, message definitions for publish/subscribe communication are simply a list of elements, one for each line, with the element type first, and the element name second. .. seealso:: :ref:`tutorial/message_definitions/publish-subscribe` in the tutorial. .. index:: single: message definitions; for LN services .. _tutorial/python/summary_api/service-message-definitions: Service Message Definitions ........................... Service message definitions have a slightly more complex syntax. They start with the line "service", followed by the line "request" and the request parameters, and finally the line "response" and the response parameters. Request and response parameters work very much like function arguments and return values in a function call. If more than parameter is returned, it is passed as a Python dictionary. .. seealso:: :ref:`tutorial/message_definitions/service` in the Python tutorial. .. index:: pair: port objects; Python API summary Port Objects ------------ Port objects are generated by member functions of a LN :class:`client` object. When creating them, we need to distinguish between subscriber ports and publisher ports. Subscriber Ports ................ Subscriber ports are created using the :meth:`client.subscribe()` method, like here: .. sourcecode:: python actor_port = actor_clnt.subscribe("elevator03.actors", "elevator/actors/motor_control") Here, the first parameter is called the "topic" of the message, which is a string identifier for the meaning and context of the message, and the second string parameter is the name of the message definition which must be retrievable by the LN Manager somewhere along its configured search path. .. index:: pair: client.publish(); Python API summary Publisher Ports ............... Publisher ports are created using :meth:`client.publish()`. An example for a publisher port is this: .. sourcecode:: python sensor_port = sensor_clnt.publish("elevator03.sensors", "elevator/sensors/floor_count") The parameters are the same as when creating a subscriber port: The first string is a topic name, and the second string is the message definition which the port will use to send messages. The only difference between the two types of ports are the methods which they provide in order to communicate (which we will describe soon). .. seealso:: :ref:`reference/python/port_objects` in the reference. Port Members ------------ Any port (both subscriber and publisher ports) have a single data member, which is the :attr:`port.packet` member. .. index:: pair: port.packet(); Python API summary port.packet ............ The :attr:`port.packet` member has itself one or more members which correspond with name and type to each element of the message definition with which the port has been initialized. (There is some type translation going on, but roughly one can say that the types correspond to each other). These members can be assigned to and read from, using simple assignments statements with "=", such as:: port.packet.floor_count = 1 .. seealso:: :ref:`reference/python/port/members` in the reference. Port Methods ------------ Ports have specific methods, depending on whether they are an input port or an output port: Publisher ports (also called output ports) have a :meth:`port.write()` method, and subscriber ports (also called "input ports" ) have a :meth:`port.read()` method. .. index:: pair: port.write(); Python API summary port.write() ............ The :meth:`port.write()` method transfers the data in the :attr:`port.packet` member to the LN messaging system. It always returns immediately. If there is any data in the message buffer which has not been read, it is overwritten. The :meth:`port.write()` method has an optional second parameter which is a timestamp value, a floating point number that can be used to identify the message if similar messages have been sent before. .. seealso:: :ref:`reference/python/port/method/write` in the reference. .. index:: pair: port.read(); Python API summary .. _tutorial/python/explanation/port.read: port.read() ........... The :meth:`port.read()` method reads the data for all elements of the :attr:`port.packet` data members from the LN communication buffer. The method has an optional ``timeout`` parameter, which can be either a Boolean or floating-point number. If data was already sent, the method returns without blocking and :term:`atomically `. In this case, the data is always transferred completely, and copied into the receiving buffer. Otherwise, the buffer is untouched. After such a successful transfer, the method always returns a value evaluating to ``True``. More specifically, it returns the packet that was read (the ``port.packet`` member), which evaluates to Boolean ``True``. If no data is there, then one of three things happen, strictly depending on the type and value of the optional ``blocking_or_timeout`` parameter of the function: * By default, if the method has no parameter, then the call is **blocking**. This means that it waits for data an unlimited amount of time. When data arrives, it returns with the new data in the request member variable. * If the parameter has the Boolean type and is set to ``True``, the call is blocking as well, and the method waits an unlimited time for data, just as in the case of no parameter. * If the type of the parameter is Boolean and set to ``False``, then read is **non-blocking**. This means that if there is data, the call returns immediately, and with a return value evaluating to ``True`` (returning the packet that was read). Otherwise, the call returns also immediately with a ``None`` value (equivalent to a Boolean value of ``False``), which means that no new data was read. * If the type of the parameter is a positive floating point number, the ``read()`` methods **uses a time-out**: It waits for that number of seconds for data to arrive. If that happens, it transfers the data and returns the data packet (equivalent to Boolean ``True``). Otherwise, when the waiting time expires, it returns with a return value of ``None``. * In case of a zero or negative floating point number, the call is also non-blocking, as if a Boolean ``False`` was passed. .. seealso:: :ref:`reference/python/port/method/read` in the reference. .. index:: pair: service provider class; Python API summary Service Providers and Service Clients ------------------------------------- For using :term:`LN services `, the Python links_and_nodes module provides a "direct API" which essentially first retrieves a handle to that service, then registers the service name and the used message definition, and in the case of the service provider also registers a handler function, which is called with specific arguments when a service request arrives. Service Provider ................ Setup ^^^^^ As we have seen in :ref:`the code example for the service provider `, the provider to initialize an LN client instance, using the `class:`client()` constructor, which we already did describe above. Then, the provider needs to get a service provider handle by calling :py:meth:`links_and_nodes.client.get_service_provider()`. The parameters of this call are the service name and the name of the used message definition. After this, the provider needs to set a handler function, using the :py:meth:`links_and_nodes.service_provider.set_handler()` method. Its argument is a Python method which will handle the service request. Finally, it has to call :py:meth:`links_and_nodes.service_provider.do_register()` to register the service with the LN manager. The argument to the method is the name of a :term:`service group`. .. seealso:: * :py:meth:`links_and_nodes.client.get_service_provider()` * :py:meth:`links_and_nodes.service_provider.set_handler()` * :py:meth:`links_and_nodes.service_provider.do_register()` Service Handling ^^^^^^^^^^^^^^^^ The handling of the service is done by the method which was passed with the ``set_handler`` call. This method has three call paremeters, ``conn``, ``req``, and ``resp``. The first is a connection object, the second is an instance with the request paremeters, and the third is an instance which will take the response parameters. The request is responded and finished by calling :py:meth:`links_and_nodes.connection.respond()`. Finally, the defined provider instance needs to be run. In our example, this happened by defining a new method ``run()`` which called the method :meth:`service_provider.handle_service_group_requests()`, and calling run with an instance of our new class after program start-up, like so: .. sourcecode:: python class ElevatorProvider(ln.service_provider): # [ ... ] def run(self): timeout = 0.5 self.handle_service_group_requests("default group", timeout) # [ ... ] if __name__ == "__main__": p = ElevatorProvider() p.run() .. seealso:: * :ref:`reference/python/service/client/wrapper` in the reference. * :py:meth:`links_and_nodes.connection.respond()` in the Python API * :meth:`service_provider.handle_service_group_requests()` in the Python API * :ref:`reference/python/service_class` in the reference. .. index:: pair: service client class; Python API summary Service Clients: Wrapper Class and Service Objects -------------------------------------------------- For the client, as we have seen in :ref:`our service client example `, the approach is similar. After instantiationg a :py:class:`links_and_nodes.client` object, the client needs to call the method :py:meth:`links_and_nodes.client.get_service()` in order to get a *service client handle*. This method has the service name and the message definition name as parameters. Calling the service happens in three steps: 1. First, the request parameters are assigned to the member req of the service client handle. 2. Second, the ``call()`` method of the service client handle is invoked. 3. Third, the response parameters can be read from the ``resp`` field of the client handle. A minimal example code looks like this: .. sourcecode:: python svc.req.requested_floor = 3 svc.call() pprint.pprint(svc.resp) .. seealso:: * :py:meth:`links_and_nodes.client.get_service()` in the Python API * :py:meth:`links_and_nodes.service.call()` in the Python API .. seealso:: :ref:`guide/api/python/services/direct_api` for using the direct API in the user guide. :ref:`reference/python/service/client_objects` in the reference.