Create an Arbalet application

Now we know the bare interface to send models to our table, we learn now how to package a more complex application for Arbalet by inheriting from a class literrally named Application. This tutorial assumes that you already know the basics of Object-Oriented Programming and especially the notions of class, its constructor, the attributes/methods of a class, and inheritance.

The mother class Application already contains all what we need to create an application packaged into a class. We don't even need to import Arbalet nor Mdel since they are already included in this magic Application class. Let's start by importing some modules:


In [ ]:
from arbalet.core import Application, Rate
import argparse  # For argument parsing

Create a stub of application

Now we create a stub for our custom application WormApp, displaying a worm by inheriting from Application and creating an empty constructor.


In [ ]:
class WormApp(Application):
    def __init__(self):
        Application.__init__(self)  # Basic inheritance calls the super constructor

The previous cell contains almost no code so it does not show anything, this is just the basic template we are going to improve.

This Application class is just a way of packaging/organizing an application, but the most important point to remember is just that it contains an Arbalet controller class accessible via WormApp.arbalet and that all the code of the application must be in the run() method.

This way of packaging the app in the Application class might looks silly at first sight but allows powerful features such as application inheritence and method overloading, automatic closure and more.

To illustrate this, we can just print the size of the connected table in run():


In [ ]:
class WormApp(Application):
    def __init__(self):
        Application.__init__(self)
    
    def run(self):
        print("My height:", self.arbalet.height, "pixels")    # Contains the number of pixels in height (15 for the default table)
        print("My width:", self.arbalet.width, "pixels")      # Contains the number of pixels in width (10 for the default table)

Execution of the previous cell did not show anything, because this is only the declaration of the application WormApp, running it requires a call to start():


In [ ]:
WormApp().start()

This time the code in run() has been executed and the size has been printed, but the simulator opened and closed instantly. This is because this app has a very short execution time, and because the Application class always release resources when there is no more code to run in run(), thus using Application there is no more need for calling close(), it is automatic.

Improve the worm app with actual code dealing with pixels

In the previous tutorial we have drawn a flag with no harcoded digit and used only fields width and height. In practice your application must use these fields to adapt its content to any size. In some occasions you will need some constraints to be respected, e.g. some ratio between height and width, an odd or even number of columns or rows, or, more rarely, a specific size. In that case you should check at starting whether the constraints are respected and, if not, exit cleanly with a meaningful error message.

This class also encapsulates for you command-line argument parsing, which are then stored in a namespace at self.args. Some of arguments are reserved to the super class and are common to all applications. For instance the --no-gui argument that you have already heard about in first software tutorials can be accessed through the field self.args.no_gui.

Applications needing their own arguments can declare their own argparse.ArgumentParser, fill it with its accepted arguments (make sure there is no name collision with the common ones) and pass the object to Application. Do not call parser.parse_args() yourself, Application will do this for us!

Let's add a --color argument with short name -col, a string corresponding to the color of the worm, red by default:


In [ ]:
class WormApp(Application):
    def __init__(self):
        parser = argparse.ArgumentParser(description='This trivial application shows a worm')
        parser.add_argument('-col', '--color',
                            type=str,
                            default='red',
                            help='Color of the worm (string of a HTML color)')
        Application.__init__(self, parser)      # The argparse object is passed to class Application here

The constructor should just be used to initialize the workspace, declare arguments, and eventually initialize the model with some color, not more. The heart of the application must overload the method Application.run(). This run() also comes with a start() whose call ensures that all the resources are closed after a successful or failed run.

As our first code in run(), let's display a red pixel browsing the whole table. For this we will simply cleanup the model (setting all pixels black with set_all()) and then attributing the red color to a single pixel with set_pixel().

There are 3 important elements in the code here after:

The Rate class

The Rate class is a controlled loop, it allows you to loop at a specific frame rate in hertz whatever the time your calculations take (assuming that the CPU is not overloaded). For instance Rate(5) will loop at ~5Hz, resulting each pixel to stay lit 200ms.

Model locking

The with self.arbalet.user_model statement allows to lock the model. In general this prevents the hardware and simulator from reading an unstable frame. For instance, to draw the next pixel we decided to set all pixels to black. If the frame is sent to the table right after set_all the table will actually show a black screen which we do not want. Therefore, locking the model while painting on it allows to display only stable models.

Text display

The model allows to render pixelated text on the table thanks to a single call user_model.write("Some text", "color")


In [ ]:
class WormApp(Application):
    def __init__(self):
        parser = argparse.ArgumentParser(description='This trivial application shows a worm')
        parser.add_argument('-col', '--color',
                            type=str,
                            default='red',
                            help='Color of the worm (string of a HTML color)')
        Application.__init__(self, parser)      # The argparse object is passed to class Application here

    def run(self):
        # We start by displaying some text
        self.arbalet.user_model.write('My color: ' + self.args.color.upper(), self.args.color)

        # Then we display the actual worm
        rate = Rate(5)
        for h in range(self.arbalet.height):
            for w in range(self.arbalet.width):
                with self.arbalet.user_model:
                    self.arbalet.user_model.set_all('black')
                    # The model is now all black (i.e. light off), in an unstable state
                    self.arbalet.user_model.set_pixel(h, w, self.args.color)
                    # The next pixel has been lit, the model is now stable we can release the lock
                rate.sleep()

You can execute this new app by calling start():


In [ ]:
WormApp().start()

Note: Python notebook like this page are not very suitable for Arbalet applications, they must be saved into a new subfolder of the arbapp. To be able to change the color thanks to the argument --color that we specified you must convert this notebook into python script and execute it directly. For this you can clic on File > Download as > Python (.py). Then run python 3.create_an_arbapp.py --color blue to execute the app with a blue worm.

Congratulations! You are now ready to develop your own app, make sure you create its own folder in the arbapps/ repository.

If you need extra features for games like reading a keyboard/joystick, playing music, you can use pygame, for any other feature you should find a python module satisfying your needs. The last tutorial illustrates the touch feature.