Tutorial ======== This page will describe how the rover system works and how to get going with creating a new RoverProcess. While following this tutorial, please don't commit any new files or changes you make to the git repository, or we'll have a mess of useless files :) . Use a new branch if you have to. A RoverProcess is essentially a program that is in charge of one component of the rover's behaviour, and/or hardware. We designed the RoverProcesses to have a similar structure as an Arduino sketch, so all your initialization goes in a "setup" function, and anything you want to run continually in a "loop" function. There is also an interprocess communication (IPC) system that allows RoverProcesses to communicate with each other, and leads to more advanced event driven programming. All RoverProcess are stored in the folder "roverprocess", which is also a python package. Adding a new RoverProcess ------------------------- To add a new RoverProcess, start by copying the ExampleProcess.py and call it whatever you want your process to be named. So if your process name is TestProcess, call the file TestProcess.py. The ExampleProcess is written as an attempt to show off the features of the rover software, and not everything is required for your process to run. The absolute minimal RoverProcess is as follows:: from .RoverProcess import RoverProcess class MinimalProcess(RoverProcess): pass That's it! The RoverProces super/parent class has default definitions for all its methods, so anything you don't need you can leave out. To enable the process, open up ``main.py`` and find the line in the ``main`` function where the ``init_modulesList`` is called. The parameters should be names of RoverProcesses you want enabed. Change the line to be:: modulesList = init_modulesList("MinimalProcess") Now run the main script as `described in its documentation`__. It should look something like this:: root : INFO Enabled modules: root : INFO ('MinimalProcess',) root : INFO Registering process subscribers... root : INFO STARTING: ['MinimalProcess'] To stop the rover software, press Ctrl-c. You should get this:: ^Croot : INFO STOP: ['MinimalProcess'] StateManager : INFO StateManager shutting down root : INFO MinimalProcess shutting down StateManager : INFO StateManager shutting down StateManager : INFO StateManager shut down success! You can see how the main script started up your MinimalProcess, but when it stops, there is also a StateManager that stops as well. The StateManager is another RoverProcess that manages the IPC mechanism. You can ignore it for the most part, unless you plan on adding features to the whole rover process system its self. At this point, if you don't know Python, you should probably learn it ;) . There are plenty of tutorials on the internet and Youtube, though the official Python documentation has a decent tutorial as well. Don't worry about becoming fluent, just learn up to and including classes and inheritance and how modules work, and you should be fine. The rest is best learned through experience and practice. Obviously, this MinimalProcess is pretty boring, so lets make our process actually do something. At the top, import the ``time`` module. In your MinimalProcess, add a method called ``loop`` that takes no parameters other than ``self``. The ``loop`` method is run continually until you press Ctrl-c to stop the system. Let's create a simple hello world printer:: def loop(self): print("Hello World!") time.sleep(1) This should print "Hello World!" to the screen every second. Sometimes you need to do stuff once right when the system starts up such as initializing variables. This can be done by adding a ``setup`` method to the process:: def setup(self, args): self.var = 42 print("In setup()") This will declare a new member called var and assign it a value of 42. It can be accessed later in the main loop or in message handler functions. The parameter ``args`` is just the arguments passed into the ``__init__`` method (the constructor). There used to be uses for it, but you probably won't need to use it anymore. If you run the rover software now, you should get:: root : INFO Enabled modules: root : INFO ('MinimalProcess',) root : INFO Registering process subscribers... root : INFO STARTING: ['MinimalProcess'] In setup() Hello World Hello World Hello World Hello World ^Croot : INFO MinimalProcess shutting down root : INFO STOP: ['MinimalProcess'] StateManager : INFO StateManager shutting down StateManager : INFO StateManager shutting down StateManager : INFO StateManager shut down success! That's the basics of adding a minimal process, the next section will talk about the IPC system. Before you move on however, there is one last thing to cover regarding printing to the console. While there is nothing technically wrong with using the ``print()`` function, the RoverProcess system provides a more useful way to print messages to the user/developer. This is the logging module. The method ``self.log()`` is a wrapper for Python's built in logging module. It allows us to print more detailed information about which process is printing messages to the console and when, and it provides logging "levels" which allows us to classify some statements as debug messages, others as warnings or errors, and we can easily change how verbose the rover software is by changing a line in the main script. Check out the `Python Logging`_ documentation for more information. While you can import the logging module and make calls to it directly, ``self.log()`` provided by the RoverProcess should be fine for most uses. Try changing the print statement in ``setup`` from ``print("In setup()")`` to:: self.log("In setup()", "DEBUG") In the ``loop`` method, change ``print("Hello World!")`` to:: self.log("Hello World!") Now, running the software should give you this:: root : INFO Enabled modules: root : INFO ('MinimalProcess',) root : INFO Registering process subscribers... root : INFO STARTING: ['MinimalProcess'] MinimalProcess : DEBUG In setup() MinimalProcess : INFO Hello World MinimalProcess : INFO Hello World MinimalProcess : INFO Hello World MinimalProcess : INFO Hello World ^Croot : INFO MinimalProcess shutting down root : INFO STOP: ['MinimalProcess'] StateManager : INFO StateManager shutting down StateManager : INFO StateManager shutting down StateManager : INFO StateManager shut down success! Notice the format of the output. When many processes are spamming stuff to the console, this is *very* handy to tell which process is doing what. You will also have noticed that the second column displays the logging level. The ``self.log`` method optionally takes a logging level as we did in our setup method. It defaults to ``INFO`` if not given. Now in the main script find the line in the ``init_logging`` function that calls the ``logging.basicConfig`` function. Change the level from ``DEBUG`` to ``INFO`` and run the software again. The debug log in the setup method was not printed. Always be selective about what level you give statements. Generally, anything that gets printed every 100 milliseconds should be ``DEBUG``. One last thing about logging: everything is also written to a file called log.log. This may be useful if the rover crashes or something. Hopefully in the future we can have different log levels for the file and the console. Rover messaging system ---------------------- Probably the most interesting part of the rover system is the mechanism for processes to communicate with each other. The system uses a publisher/subscriber scheme, also known as the observer design pattern. The concept is simple: There are two types of entities, ones that produce or publish data, and ones that "subscribe to" or consume data. Data from publishers are automatically pushed to all subscribers. In the rover system, we allow RoverProcesses to be both publishers and subscribers. You can be subscribed to as many messages as you can want, and can publish any message that you want, but be careful not to conflict with other processes. Maybe the issue of conflicts can be fixed in the future. One key thing to remember about our publisher/subscriber system, is that each process has *no idea* where a message comes from when it receives one, or where a message is going when it sends one. The StateManager makes sure all messages end up in the right place. This may seem restrictive at first, but it allows the system to be highly modular, and in most cases is self configuring. I will use the term "message" quite often from now on, so now is a good time to explain what I mean by that. From the source code: .. automodule:: roverprocess.RoverProcess :members: RoverMessage :noindex: A message is just a key-value pair, where the key is like a name or label identifying what the data represents, and the value can be any data you want. If you aren't familiar with named tuples in python, take a look at the documentation on `Python NamedTuples`_ for examples on how to use them. Create two new minimal RoverProcesses, one called Generator and the other called Printer or something like that. Enable them and make sure they run. Now, in the Printer process, add a setup method and call ``self.subscribe()`` to subscribe to some message:: def setup(self, args): self.subscribe("test") The Printer process is now subscribed to the message "test". Now we need the other process to produce the "test" message. Add a loop to the Generator process that publishes the "test" message with some value:: def loop(self): self.publish("test", 42) time.sleep(1) If you run this now, nothing will happen because we haven't told the Printer process what to do when we receive the "test" message. This can be done with the ``messageTrigger`` method. For now, lets just log out the value of the message:: def messageTrigger(self, message): self.log(message.data) The output should look like this:: root : INFO Enabled modules: root : INFO ('Generator', 'Printer') root : INFO Registering process subscribers... root : INFO STARTING: ['Generator', 'Printer'] Printer : INFO 42 Printer : INFO 42 Printer : INFO 42 Printer : INFO 42 Printer : INFO 42 ^Croot : INFO Generator shutting down root : INFO STOP: ['Generator', 'Printer'] StateManager : INFO StateManager shutting down root : INFO Printer shutting down StateManager : INFO StateManager shutting down StateManager : INFO StateManager shut down success! Now let's add some more messages to deal with. In the Generator's loop method make it publish the string "" under the key "name", right after it publishes the "test" message. Then change the body of the Printer process's ``messageTrigger`` method to use an if/else statement on the message's key:: def messageTrigger(self, message): if message.key == "test": self.log(message.data*2, "DEBUG") elif message.key == "msg": self.log("Hello " + message.data) else: self.log(message.data) You can of course do what ever you want in each condition. I chose to double the value of the "test" message and log it as a debug message, say hello to what ever "name" comes in, and just log other messages. Remember to subscribe the Printer to the "name" message! Running this should give the following output:: root : INFO Enabled modules: root : INFO ('Printer', 'Generator') root : INFO Registering process subscribers... root : INFO STARTING: ['Printer', 'Generator'] Printer : DEBUG 84 Printer : INFO Hello Rover Printer : DEBUG 84 Printer : INFO Hello Rover Printer : DEBUG 84 Printer : INFO Hello Rover Printer : DEBUG 84 Printer : INFO Hello Rover ^Croot : INFO Generator shutting down root : INFO STOP: ['Printer', 'Generator'] StateManager : INFO StateManager shutting down root : INFO Printer shutting down StateManager : INFO StateManager shutting down StateManager : INFO StateManager shut down success! If a process is subscribed to many messages, the if/else statement in the ``messageTrigger`` method could get quite messy. To avoid this, there is an alternative way to process an incoming message; callback functions. Callback functions are just functions that are called whenever a certain event happens, in our case when a particular message comes in. You can register a callback for a message by declaring a method called ``on_(self, data)`` where ```` is the key of the message. It takes in one (in addition to self) parameter that is the data component of the message. Try this out by moving the condition for the "name" message to a callback:: def messageTrigger(self, message): if message.key == "test": self.log(message.data*2, "DEBUG") else: self.log(message.data) def on_name(self, name): self.log("Hello " + name) Note how you can change the name of the callbacks "data" parameter to something more descriptive. This code should run the same as before. -------- That is the basics of how to use the Roveberrypy framework. Look around the auto generated docs for individual modules to learn how they work, and stay tuned for more tutorials soon. __ main.html .. _Python Logging: https://docs.python.org/3.5/library/logging.html .. _Python NamedTuples: https://docs.python.org/3.5/library/collections.html#collections.namedtuple