For End-Users

Table of Contents

First and foremost, please use a decent IDE. You hearing, Armin?

I tried to shove documentation wherever possible, so that the IDE can give some hints of the type and the meaning of the variables. To do that, just hover your mouse over the thing you want some information. Some IDEs also give autocomplete suggestions, which can be used to the same end. It may not always be helpful, but at least I tried. Also, the code is almost all type annotated, which may also help.

Package structure #

The installed package contains 4 submodules, that can be divided in two groups, plus a module that will be generated locally. However, normally, you should only need pyimclsts.network and the module generated by pyimclsts.extract.

  • Message extraction/generation:
    • pyimclsts.extract It is intended to be executed to generate python classes that represent messages.
    • pyimclsts.extractutils Contains some useful functions to analyze the IMC.xml file.
  • Networking:
    • pyimclsts.core Contains functions that support the network operations, such as serialization/deserialization and the CRC16 algorithm.
    • pyimclsts.network Contains functions that allow reading and writing messages in a stream fashion, namely, the subscriber function.

Lastly, when the pyimclsts.extract is run with python3 -m pyimclsts.extract, it creates a folder containing the IMC messages as classes, as well as other supporting data types, such as enumerations, and bitfields. It should look like this:

  • pyimc_generated
    • __init__.py
    • _base.py
    • bitfields.py
    • categories
      • AcousticCommunication.py
      • Actuation.py
    • enumerations.py
    • messages.py

enumerations.py and bitfields.py contain globally (in the IMC.xml) defined enumerations and bitfields (locally defined enumerations or bitfields are stored inside the corresponding message class). messages.py re-exports the messages defined in their corresponding category file. It re-exports, for example, the Announce message, found in categories/Networking.py.

Publisher-Subscriber model #

There are three main elements in this model: a subscriber, a publisher and a message bus. In short, a publisher application writes messages to a shared interface, also known as the message bus, and a subscriber application register with the broker the messages it wants to consume. The message broker, therefore, gathers the messages sent by the publishers and distribute them to the subscribers.

The subscriber #

In this implementation, the subscriber class provides objects that register the subscriber functions. To instantiate it, we give it an interface from which it will read the messages and then we can give it the callbacks for each message we desired. (In a sense, it actually works as a message broker, but we kept this name since from the point of view of the LSTS systems, the applications written with this tools would be subscribers and the vehicles would be the publishers. And I’m lazy and don’t want to change it now.)

The subscriber methods #

Besides some private methods (the ones that start with _, Python convention), the main subscriber methods are presented below:

class subscriber:
    ...
    def subscribe_async(self, 
                        callback : Callable[[_core.IMC_message, Callable[[_core.IMC_message], None]], None], 
                        msg_id : Optional[Union[int, _core.IMC_message, str, _types.ModuleType]] = None, *, 
                        src : Optional[str] = None, 
                        src_ent : Optional[str] = None):
        ...

    def periodic_async(self, 
                      callback : Callable[[_core.IMC_message], None], 
                      period : float):
        ...

    def subscribe_mp(self, 
                    callback : Callable[[_core.IMC_message, Callable[[_core.IMC_message], None]], None], 
                    msg_id : Optional[Union[int, _core.IMC_message, str, _types.ModuleType]] = None, *, 
                    src : Optional[str] = None, 
                    src_ent : Optional[str] = None):
        ...

    def call_once(self, 
                  callback : Callable[[Callable[[_core.IMC_message], None]], None], 
                  delay : Optional[float] = None) -> None:
        ...

    def print_information(self) -> None:
        '''Looks for (and asks for, when applicable) the first Announce and EntityList messages and print them.
        
        Executes no other subscription.
        '''
        ...

    def stop(self) -> None:
        ...
    
    def run(self) -> None:
        ...

To subscribe a function, there are 3 (+ 1 not yet implemented) possible ways: subscribe_async, periodic_async, call_once and subscribe_mp (not implemented). As the names suggest, call_once can be used to call a function once, optionally after a delay; periodic_async executes a callback every period seconds. subscribe_async executes the callback for every received message in msg_id and filters according to src and src_ent, if given. msg_id can be an int (the message id), the message class (or its instance) or a str (camel case) or a Python module (the files/modules inside the category folder) to specify a category of messages. src and src_ent are strings that indicate the vehicle and the entity inside a vehicle, for example, “lauv-xplore-1” and “TemperatureSensor”.

The subscribed functions must receive as arguments 1. A send_callback, and 2. A message (when applicable). The send_callback is nothing more than a function object of the method bound to the instance of the internal message broker of the subscriber. Is this greek? Let me clarify: Internally, the subscriber uses the given IO interface (file or TCP, for now) and creates a message_broker, which is used to manage (send and receive) messages. By using a message_broker we can internally use the same interface for both files or TCP. So, finally, the send_callback is simply a reference to the .send() method of this message_broker. You can use it as a normal function. Normally, the `src`, `src_ent`, `dst` and `dst_ent` are inferred from the IO interface, but you can use this function to overwrite them. Simply pass them as named arguments (as ints), for example, send_callback(msg, dst=31). For more information regarding the message, please check IMC Message.

run and stop start and stop the event loop. That is, once run() is called, the application will be blocked as the control of the program will now be given to and managed by subscriber. To stop the event loop, you may pass the .stop callback itself to the instance to the subscriber. For example:

if __name__ == '__main__':
    conn = p.tcp_interface('localhost', 6006)
    vehicle = FollowRef_Vehicle('lauv-xplore-1')

    sub = p.subscriber(conn)
    def stop_subscriber(send_callback):
        sub.stop()
    # or more neatly:
    stop_subscriber = lambda _ : sub.stop()
    
    # Set a delay
    sub.call_once(stop_subscriber, 60)

print_information is just an utility function, that I wrote mainly for file reading or usage during simulations. It saves the list of subscribed functions, starts the event loop in search of an Announce and an EntityList messages, prints them, stops the event loop and restores the list of subscribed functions.