Skip to main content
A custom driver controls an instrument that Lager does not include, such as an in-house test fixture. You write it once as a small bundle and install it on a box with lager drivers install. Then every user can drive its nets with the standard CLI. The driver code is stored on the box only.

Bundle layout

A driver can import the Python standard library, the packages that come with the box (such as pyserial, pyusb and hidapi) and the lager package. For other packages, see Python packages.

The manifest

The manifest gives the driver’s type, its net settings and its actions. The CLI builds the lager <type> command from it, and the box checks every call against it.
Each param and arg has a type: str, int, float, bool or choice. A param can also be net, which names another saved net of the given role; the driver then uses that net to reach the hardware. Fields can also have required, default, min and max (numbers only), choices (for choice) and help. Action, param and arg names are 1 to 32 lower case letters, digits and _, starting with a letter. No - is allowed; the CLI shows hold_time as --hold-time. These arg names are reserved, because the CLI uses them already: box, json, as_json, help, netname, action, role, quiet, http_timeout, ctx, box_addr, params.

The driver

  • Only methods marked with @action can be called, and each must be named in the manifest. The method gets the action’s args as keyword arguments, with the types the manifest gives.
  • setup() runs before the action and teardown() runs after it, also when the action raises an exception.
  • Return a dict, a value or None. A dict’s message is printed by the CLI; the other keys are shown below it, and --json prints the whole result.
  • An exception becomes an error for the user, with its type and message.
  • print() output and self.log go to the box log, not to the user.

Python packages

A driver can use packages that are not on the box. List them in a requirements file and name it in the manifest:
The box installs the packages when you run lager drivers install, so a call never waits for them. They are stored with the driver in /etc/lager/drivers/, so they stay installed after lager update. If a package cannot be installed, the driver is not installed, and the version that was installed before keeps working. The file must pin every package to one version with a hash, including the packages that your packages need. pip-compile writes such a file:
Rules:
  • Exact versions with hashes only. Each line is name==version with at least one --hash. Options such as -r, -e and --index-url are not allowed.
  • Wheels only. The box does not build packages from source. A package must have a wheel for the box’s Python (3.12) and CPU. Boxes can be x86_64 or aarch64 (ARM), so pick packages that have wheels for both, or pure-Python wheels. pip-compile --generate-hashes lists the hashes for every platform.
  • The box’s own packages come first. A driver cannot replace a package that the box already has. If you pin a package the box has, the version must be the same as the box’s, or the install is refused. Leave such packages out.
  • Each driver has its own packages. Two drivers can use different versions of the same package.
  • Limits. The box gives the install 5 minutes, and the installed packages can use at most 200 MB.

A box without internet

Put the wheel files in a wheels/ folder in the bundle. The box uses them before it looks on the internet. They count toward the 5 MB bundle limit. To download them for the box’s CPU:
Use manylinux_2_36_x86_64 for an x86_64 box. Running uname -m on the box shows which one it is. Pure-Python wheels work on both.

After an update

If a lager update changes the box’s Python version, lager drivers list shows the driver as error and its Deps as stale. Install the packages again with:
A package’s code runs on the box, with full access to the box’s hardware and network, when the driver imports it. The box only installs wheels, so no package code runs during the install. But treat every package in the file as code that runs on the box, and install only packages you trust.

How a call runs

Each call runs the driver in a new process on the box. A driver that crashes or hangs cannot stop the box. The box loads Lager before the call, so the process starts quickly. It loads driver.py again for each call, so an installed update takes effect on the next call. Two calls on the same net run one after the other. If a lager uart session holds the UART net that the driver uses, the call is refused until that session closes.

Using a driver from a script

A lager python script can use a custom net directly:
open_driver() loads the driver in the script’s own process. It runs setup() on entry and teardown() on exit. It raises ManifestError when the net is not a custom net. It also raises it when a net param names a missing net or a net of the wrong role. It raises DriverFailed when the driver does not import or has no such action. lager.drivers also exports DriverBusy, DriverTimeout and DriverUnavailable. The box’s HTTP API uses them for a busy net, a timeout and a driver that is not installed.

Install and use it

A driver runs on the box with the same access as a lager python script. It is kept out of the public Lager code, but a person who can connect to the box can read it.