labgrid as a library - a primer

In this post, we’ll explore the basic concepts of labgrid - a python framework for interfacing embedded systems.

What is labgrid?

I find the cleanest definition of what labgrid is in this sentence:

The idea behind labgrid is to create an abstraction of the hardware control layer needed for testing of embedded systems, automatic software installation, and automation during development.

As the definition points out, labgrid can serve multiple use cases. For this post, we’ll stick to a basic example to introduce core concepts and leave more complex use cases for other posts.

Installing

Follow these steps to install labgrid on a Linux laptop.

apt-get install python3 python3-virtualenv python3-pip virtualenv

# Installing in a virtualen
virtualenv -p python3 labgrid-venv
source labgrid-venv/bin/activate

git clone https://github.com/labgrid-project/labgrid
cd labgrid 
pip install -r requirements.txt
pip install .
cd ..

# Test the installation. No errors mean success
python -c "import labgrid"

Overview

In the example, the laptop will use labgrid as a library to execute shell commands on the RPi. It will use both the SSH and UART connection to achieve the same thing.

The diagram below shows the connections between the laptop and the RPi.

+---------------------+         +-----------------------+
|                     |         | ssh server            |
|      192.168.18.127 +---------+ 192.168.18.210        |
|                     |         |                       |
| Laptop              |         |          Raspberry Pi |
|                     |         |          Model: 3b+   |
|        /dev/ttyUSB0 +---------+ UART pins             |
|                     |         |                       |
+---------------------+         +-----------------------+

Prerequisites

It is assumed you know how to set up the RPi so that it:

  • boots Raspbian Buster Lite
  • is in the same subnet as the laptop
  • has SSH enabled with default credentials (pi, raspberry)
  • has a UART connection with the laptop
    • the UART connection is detected on the laptop as /dev/ttyUSB0 or similar
    • the /dev/ttyUSB0 can be accessed without root permissions sudo chmod 777 /dev/ttyUSB0

For some help on how to get there check here and here.

Code

Here is a working example first. This code is executed on the laptop and interacts with the RPi.

from labgrid import Target
from labgrid.resource import RawSerialPort, NetworkService
from labgrid.driver import SSHDriver, SerialDriver, ShellDriver

username = "pi"
password = "raspberry"

rpi = Target("RPi")

network_service = NetworkService(rpi,
                                 address="192.168.18.210",
                                 name="rpi_network",
                                 username=username,
                                 password=password)

raw_serial_port = RawSerialPort(rpi, None, port="/dev/ttyUSB0", speed=115200)
# At this point both resources belong to the target. This happened implicitly.


ssh_driver = SSHDriver(rpi, name=None)
serial_driver = SerialDriver(rpi, name=None)

prompt_regex_pattern = f'{username}' + r'@\w+:[^ ]+ '
login_regex_pattern = ' login: '
shell_driver = ShellDriver(rpi, name=None,
                           prompt=prompt_regex_pattern,
                           login_prompt=login_regex_pattern,
                           username=username,
                           password=password)
# By belonging to the same target, the drivers get automatically bounded to the resources or other drivers that fit their requirements.
# The SSHDriver bounded to NetworkService
# The ShellDriver bounded to SerialDriver whicb bounded to RawSerialPort

rpi.activate(ssh_driver)
rpi.activate(shell_driver)

# When activated the drivers consume the object below it.
# This means making an ssh connection or taking over `/dev/ttyUSB0`.
# After that they are ready to be used

cmg = "uname -ra"
print(f"Running '{cmg}' via ssh")
ssh_result = ssh_driver.run("uname -ra")
print(f"ssh_driver result:\n {ssh_result}")

print(f"Running '{cmg}' via serial")
shell_result = shell_driver.run("uname -ra")
print(f"shell_driver result:\n {shell_result}")

Result:

Running 'uname -ra' via ssh
ssh_driver result:
 (['Linux raspberrypi 5.4.51-v7+ #1333 SMP Mon Aug 10 16:45:19 BST 2020 armv7l GNU/Linux'], [], 0)
Running 'uname -ra' via serial
shell_driver result:
 (['Linux raspberrypi 5.4.51-v7+ #1333 SMP Mon Aug 10 16:45:19 BST 2020 armv7l GNU/Linux'], [], 0)

Explanation

We start by defining a Target which is the object representing the RPi.

rpi = Target("RPi")

After that, we think in terms of “what Resources the Rpi exposes from the perspective of the laptop”. A Resource is an actionable endpoint that labgrid can work with. In the following setup two exist:

  • there is a /dev/ttyUSB0 registered on the laptop which is a UART connection to the RPi
  • the RPi has an ssh server the laptop can connect to

Translated into the language of labgrid configuration, that means 2 resources:

After the execution of the code below the rpi contains the newly created resources.

network_service_resource = NetworkService(rpi,
                                          address="192.168.18.210",
                                          name="rpi_network",
                                          username="pi",
                                          password="raspberry")
raw_serial_port = RawSerialPort(rpi, None, port="/dev/ttyUSB0", speed=115200)

But resources are just endpoints, they are just potential for action but not direct means to it. Here the concept of a Driver comes in. A Driver is an object which will give you an actionable API as long as it’s bound on to something it can work with. This visual representation from the official docs explains it well:

So how do you figure out which drivers and resources bind together? By checking the drivers docs.

The prerequisite for a driver to bind is that the object below implements the correct protocol. A protocol is another word for an API or “interface” in the OOP jargon. In the example I knew upfront what can bind together:

[Driver]     [Resource]
SSHDriver -> NetworkService

[Driver]         [Driver]       [Resource]
ShellDriver -> SerialDriver -> RawSerialPort

That is what the structure looks like at once the code below gets executed.

ssh_driver = SSHDriver(rpi, name=None)
serial_driver = SerialDriver(rpi, name=None)
prompt_regex_pattern = f'{username}' + r'@\w+:[^ ]+ '
login_regex_pattern = ' login: '
shell_driver = ShellDriver(rpi, name=None,
                           prompt=prompt_regex_pattern,
                           login_prompt=login_regex_pattern,
                           username=username,
                           password=password)

The last part involves activating the drivers. An activation is an act of getting the driver ready to run. If we use a car analogy, binding is the process of putting the car together from parts and activation is igniting the engine.

As you can see, we aren’t directly activating the Serial Driver. Instead, we activate the ShellDriver. The reason for this lies in the fact that the ShellDriver is the one who’s API we want to use. Given that it’s on top of the stack activating it will automatically activate the SerialDriver.

rpi.activate(ssh_driver)
rpi.activate(shell_driver)

After all those steps we’re now free to use the API to interact with the RPi. The docs say that both SSHDriver and ShellDriver implement the command protocol. This leaves us with the convenience of the run method to execute the example command uname -ra.

cmg = "uname -ra"
print(f"Running '{cmg}' via ssh")
ssh_result = ssh_driver.run("uname -ra")
print(f"ssh_driver result:\n {ssh_result}")

print(f"Running '{cmg}' via serial")
shell_result = shell_driver.run("uname -ra")
print(f"shell_driver result:\n {shell_result}")

Conclusion

The example was purposefully basic to introduce the basic concepts instead of acting as a showcase of what labgrid can do.

It has shown labgrid API to have a tendency of doing things behind the scenes during object initialization.

Once the implicit assumptions have been made clear, the overall target/driver/resource relationship starts making sense.

Written on November 21, 2020