Driver SDK reference

Every API, the manifest, the sandbox and the D-Bus interfaces.

The Mota Driver SDK is for user-space drivers on Linux. A driver is an ordinary process that reaches its device through a standard kernel interface (hidraw, usbfs, VFIO or UIO) and hands what it produces to Mota through a standard sink:

 device ──(hidraw · usbfs · VFIO/UIO)──▶ driver process ──▶ sink ──▶ Mota
                                         (SDK runtime)      uinput  → keyboards, mice, pads
                                                            D-Bus   → org.motaos.Sensors
                                                            TAP     → a network interface
                                                            ublk    → /dev/ublkbN

mota-driverd (in mota-services) runs drivers: it watches for devices, starts the matching driver as a sandboxed systemd unit, restarts it if it crashes, and stops it when the device goes away.

To write your first driver, start with Your first Mota driver. This page is the reference. The API docs proper are the crate's rustdoc (cargo doc -p mota-driver-sdk --open in the Mota source tree).

Part Where
SDK crate mota-driver-sdk (imported as mota_driver) sdk/driver-sdk/; installed at /usr/share/mota/driver-sdk
mota-driverd (org.motaos.Drivers, org.motaos.Sensors) system/driverd/
mota-driver CLI sdk/driver-cli/
Example drivers sdk/drivers/
Test kit and VM tests sdk/driver-testkit/

Compatibility. Semver on the crate API and on the manifest schema. Drivers rebuild against a new major version; there is no binary ABI.


1. A driver

A driver is two files, installed under /usr/lib/mota/drivers/<name>/:

  • mota-driver.toml: the manifest: what it is, which devices it takes, its settings;
  • the executable (named after the driver unless driver.exec says otherwise).
use mota_driver::prelude::*;

struct MyDriver { /* devices, sinks */ }

impl Driver for MyDriver {
    async fn probe(ctx: &mut Context) -> Result<Self> {
        // Open ctx.require_device()?, create the sink. Err(Unsupported) declines.
        todo!()
    }
    async fn run(&mut self, ctx: &mut Context) -> Result<()> {
        // Do the work until the device goes away.
        todo!()
    }
}

fn main() -> std::process::ExitCode {
    mota_driver::run::<MyDriver>()
}

mota-driver new generates this for each class (see §7).


2. Manifest

mota-driver.toml, schema 1:

schema = 1

[driver]
name = "hid-remap"          # 1-48 of a-z, 0-9, '-'; also the package and directory name
version = "1.0.0"
class = "input"             # input | sensor | network | block
description = "Remaps keys on a HID keyboard"
# exec = "hid-remap"        # executable, default = name

[[match]]                   # one or more; each sets exactly one of:
hid = { vendor = 0x1209, product = 0x0001 }
# usb = { vendor = 0x046d, product = 0xc52b, interface_class = 0x03 }  # interface_class optional
# pci = { vendor = 0x1234, device = 0x11e8 }
# virtual = true            # no hardware: started once when installed / at boot

[config]                    # free-form; the driver reads it with ctx.config()
remap = { KEY_CAPSLOCK = "KEY_LEFTCTRL" }

Unknown keys are errors, so typos fail at load time. The class decides what the driver may reach (§6.3), not just what it's called.


3. Runtime

3.1 Driver and run

Driver::probe(ctx) Check the device, open it, create the sink. Return Error::Unsupported to decline the device.
Driver::run(&mut self, ctx) Work until done. The runtime cancels it on SIGTERM / SIGINT.
Drop Release everything. Sinks remove their device / interface / disk when dropped.
mota_driver::run::<D>() Call from main: sets up logging, parses arguments, loads the manifest, runs a single-threaded tokio runtime, maps the result to an exit code.

3.2 Context

Method
manifest() The loaded Manifest.
device() / require_device() The device the driver was started for (§6.1); require_device errors with NoDevice if there is none.
config::<T>() [config] deserialized into the driver's own serde::Deserialize type.
stopped().await Resolves when asked to stop (rarely needed: run is cancelled for you).

3.3 Arguments and environment

mota-driverd starts a driver as <exec> --manifest <path> [--device <path>]. For testing by hand the same values can come from the environment:

Variable
MOTA_DRIVER_MANIFEST Manifest path (default: mota-driver.toml next to the executable).
MOTA_DRIVER_DEVICE The device.
MOTA_DRIVER_LOG Log filter, tracing EnvFilter syntax (default info).

3.4 Exit codes

Code Meaning Restarted?
0 exit::OK Stopped when asked. no
1 exit::FAILED Any other error. yes
3 exit::UNSUPPORTED probe declined the device (Error::Unsupported). no
4 exit::GONE The device went away (Error::NoDevice). no

3.5 Errors and logging

mota_driver::Error: Io, Manifest, Unsupported, NoDevice, Usb, Other. Which one you return decides the exit code above.

Logging goes to stderr and so to the journal (mota-driver logs <name>). The SDK re-exports the crate it sets up: mota_driver::tracing::info!(...).


4. Hardware access (mota_driver::hw)

4.1 hw::hid: HID devices through hidraw

hid::list() HidNodes present (devnode, info: bus, vendor, product, name).
HidDevice::open(path) Open /dev/hidrawN.
read_report(&mut buf).await Next input report. Unplugging gives NoDevice.
write_report(&report) Output report (e.g. keyboard LEDs).
report_descriptor(), is_keyboard() Inspect the device.
grab_kernel_input() Exclusively grab the kernel's own evdev nodes for this device, so its input doesn't arrive twice (once from the kernel, once from the driver).

mota_driver::keys has the Linux KEY_* codes, from_name / name, HID usage → key code mapping, and BootKeyboard, a decoder for boot-protocol keyboard reports.

4.2 hw::usb: USB devices through usbfs

usb::from_node(path) The device behind /dev/bus/usb/BBB/DDD (what mota-driverd passes).
usb::list(), usb::find(bus, address) Enumerate / look up.
UsbDeviceInfo::open() → UsbDevice Open it.
UsbDevice::claim(interface) → UsbInterface Claim an interface; detaches the kernel driver bound to it.
interrupt_in/out, bulk_in/out .await Transfers. An unplug gives NoDevice.
raw() The underlying [nusb] handle (control transfers, alternate settings); mota_driver::hw::usb::nusb is re-exported.

4.3 hw::pci: PCI devices through VFIO or UIO

mota-driverd binds the device to a user-space back end before starting the driver (§6.2) and passes its sysfs path; PciDevice::open takes that or a bare address (0000:00:04.0).

VFIO (vfio-pci) UIO (uio_pci_generic)
When the machine has an IOMMU (VT-d / AMD-Vi enabled) no IOMMU
BARs yes yes
Interrupts MSI-X, MSI or INTx INTx
DMA yes, confined by the IOMMU to the driver's buffers no: dma_alloc returns Unsupported
backend(), vendor_id(), device_id(), address() What was opened.
bar(n) → Bar Map memory BAR n. read8/32/64, write8/32/64 are volatile and bounds-/alignment-checked (an error, never a stray access).
config_read16/32, config_write16/32 Config space.
irq(IrqMode::Auto) → Irq Auto picks MSI-X, then MSI, then INTx. MSI/MSI-X enable bus mastering (an MSI is a memory write by the device).
Irq::wait().await / wait_blocking(timeout) Next interrupt(s); wait_blocking is for threads outside the async runtime (block handlers).
Irq::unmask() Re-arm INTx after acknowledging it on the device; a no-op for MSI. Always call it after handling.
dma_alloc(size) → DmaBuffer Page-rounded memory the device reaches at iova(); as_slice / as_mut_slice. Enables bus mastering. Unmapped on drop.
enable_bus_master(), reset()

A DMA to an address the driver didn't map is blocked by the IOMMU and reported by the kernel (DMAR: … fault addr …); the test suite checks this.


5. Sinks (mota_driver::sink)

5.1 sink::input: a virtual input device (uinput)

let mut kb = VirtualInput::builder("My pad")
    .keys(&[keys::KEY_A, keys::KEY_B])       // and/or .rel_axes(..), .abs_axis(..)
    .id(input::BUS_USB, 0x1209, 0x0001, 1)   // optional
    .build()?;
kb.key(keys::KEY_A, true)?; kb.sync()?;      // or kb.tap(KEY_A)

Events go through evdev → libinput → the compositor like any keyboard or mouse. Only declared keys/axes can be sent. Dropping it removes the device.

5.2 sink::sensor: readings on org.motaos.Sensors

let bus = SensorBus::connect().await?;
let s = bus.register("cpu", "CPU package", Kind::Temperature).await?;  // id unique within the driver
s.publish(54.5).await?;

Each kind has one fixed unit, so readers never convert:

Kind Unit Kind Unit
temperature °C current A
illuminance lx power W
humidity %RH fan-speed rpm
pressure hPa distance cm
voltage V

Sensor ids are <driver>/<id>. Only a running sensor-class driver started by mota-driverd may register or publish, and only for its own sensors; they disappear when the driver stops.

5.3 sink::net: a network interface (TAP)

let tap = Tap::builder().name("mnet%d").mtu(1500).build()?;   // .mac([..]) optional
tap.set_carrier(true)?;                       // link up/down as NetworkManager sees it
let n = tap.recv(&mut buf).await?;            // a frame the system sent
tap.send(&frame).await?;                      // a frame "received from the wire"

The interface is an ordinary Ethernet link: NetworkManager lists it and the kernel routes through it. set_ipv4(addr, prefix) is for tunnel-style drivers that own their addressing; a driver for a real NIC leaves addressing to NetworkManager. frame_size() is the buffer size recv needs. Dropping the Tap removes the interface.

5.4 sink::block: a disk (ublk)

struct Disk { /* your device */ }
impl BlockHandler for Disk {
    fn read(&mut self, offset: u64, buf: &mut [u8]) -> io::Result<()> { .. }
    fn write(&mut self, offset: u64, buf: &[u8]) -> io::Result<()> { .. }
    fn flush(&mut self) -> io::Result<()> { Ok(()) }   // optional
}
let disk = BlockDevice::builder().size(64 << 20)   // .block_size(4096) .queue_depth(..) .read_only(..)
    .build(Disk { .. })?;                          // /dev/ublkbN
disk.wait().await                                  // in run(): until stopped from outside

The handler runs on its own thread, one request at a time, so it may block (it can use Irq::wait_blocking). Offsets and lengths are bytes and always fall inside the disk. Dropping the BlockDevice drains in-flight I/O and removes the disk. If a driver dies without that, the kernel keeps a dead ublk device; the next build() deletes dead devices whose server is gone, so a restarted driver gets its old /dev/ublkbN back.


6. mota-driverd

6.1 Binding

driverd scans sysfs at start and then follows kernel uevents. A device is bound to the first enabled driver whose [[match]] accepts it:

Device Passed as --device Unit name
hidraw /dev/hidrawN mota-driver-<name>-hidrawN.service
USB /dev/bus/usb/BBB/DDD mota-driver-<name>-usb-BBB-DDD.service
PCI function /sys/bus/pci/devices/<address> mota-driver-<name>-pci-<address with : → ->.service
virtual = true (none) mota-driver-<name>-virtual.service

Unplugging stops the unit. Driver units are independent of driverd: they keep running across a driverd restart and are adopted again.

6.2 PCI devices

driverd takes a PCI function only if no kernel driver owns it (or it is already on vfio-pci / uio_pci_generic). It never unbinds a kernel driver; to give a kernel-driven device to a Mota driver, blacklist the kernel driver. A matching but kernel-owned device is logged once and left alone.

With an IOMMU group it binds the device to vfio-pci, otherwise to uio_pci_generic. When the driver stops (disabled, uninstalled), the device is unbound and left with no driver, as it was found.

6.3 Sandbox

Every driver runs as a transient unit with: NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, kernel tunables / modules / logs / control groups / clock / hostname protected, MemoryDenyWriteExecute, RestrictNamespaces, native syscalls only, DevicePolicy=closed, and per class:

Class Capabilities Device nodes Network Kernel module loaded first
input none its device, /dev/uinput, the kernel's evdev nodes (read) none uinput
sensor none its device none
network CAP_NET_ADMIN its device, /dev/net/tun AF_UNIX, AF_NETLINK, AF_INET, AF_INET6 tun
block CAP_SYS_ADMIN its device, /dev/ublk-control, /dev/ublkcN none ublk_drv

A PCI driver additionally gets /dev/vfio/vfio + /dev/vfio/<group> and a 256 MiB LimitMEMLOCK (VFIO), or /dev/uioN and its device's sysfs directory writable (UIO). Nothing else from /dev.

6.4 Restarts and states

A crash restarts the driver after 500 ms. Exit codes 3 and 4 never restart. After 4 starts in 5 minutes systemd stops restarting; driverd shows the binding as disabled and notifies the user. mota-driver enable (or replugging) clears it.

Binding states: starting, running, stopped, unsupported (probe declined), disabled (crash loop), failed (couldn't start).

6.5 D-Bus: org.motaos.Drivers

System bus, object /org/motaos/Drivers.

Method
ListDrivers() → s JSON [{name, version, class, description, enabled, path}]
ListBindings() → s JSON [{driver, unit, device, state, restarts, last_exit}]
Reload() → b Re-read installed drivers and bind matching devices (package scripts call this).
SetEnabled(s name, b enabled) → b Root, or polkit org.motaos.drivers.manage (administrator, auth_admin_keep). Disabling stops the driver's instances.
Version() → s

6.6 D-Bus: org.motaos.Sensors

System bus, object /org/motaos/Sensors.

Member
ListSensors() → s JSON [{id, driver, name, kind, unit, value, updated}] (updated: Unix ms)
Read(s id) → (d value, t updated_ms)
signal ReadingChanged(s id, d value)
Register, Publish Driver side; use sink::sensor. Refused for anything that isn't a running sensor-class driver unit.

7. mota-driver CLI

Command
new DIR [--name N] [--class C] [--hid V:P \| --usb V:P \| --pci V:D \| --virtual] [--sdk PATH] New project from a template. The class picks the template (input, sensor, network, block); --usb and --pci pick the USB / PCI templates. Default match: hid 1209:0001 for input, virtual otherwise. The SDK is found above the current directory (sdk/driver-sdk) or at /usr/share/mota/driver-sdk.
build [DIR] cargo build --release.
package [DIR] [-o OUT] Build and write mota-driver-<name>_<version>_<arch>.deb.
install DEB apt-get install it (root).
list Installed drivers and their bindings.
logs NAME [-f] The driver's journal.
enable NAME, disable NAME Through SetEnabled (polkit).

Packages

mota-driver package installs the executable and manifest to /usr/lib/mota/drivers/<name>/, depends on mota-services, and calls Reload from postinst / postrm, so installing a driver binds matching devices at once and removing it stops them.


8. Testing

Unit tests cargo test -p mota-driver-sdk -p mota-driverd -p mota-driver-cli
mota-driver-vmtest input <driver> <manifest> A virtual uhid keyboard against an input driver.
mota-driver-vmtest pci <address> QEMU's edu device through hw::pci: registers, interrupts, DMA, a stray DMA for the IOMMU to block.
vmtest-driverd.sh driverd with a real virtual USB keyboard (dummy_hcd + configfs HID gadget): bind, sandbox, crash loop, unplug.
vmtest-sensors.sh, vmtest-network.sh, vmtest-block.sh One per sink, against the example drivers.
vmtest-pci.sh [--no-iommu] PCI claiming, VFIO and UIO, under QEMU.
qemu/pci-vm.sh, qemu/vmsh.py Boot a Mota ISO under QEMU (works without KVM) with an emulated IOMMU and the edu device; drive its console.

The scripts run as root inside a Mota VM (see their headers).


9. Examples (sdk/drivers/)

Driver Class Shows
hid-remap input hidraw → remapped keys through uinput; grabbing the kernel's input
hwmon-sensors sensor hwmon / thermal zones and file sources on org.motaos.Sensors
udp-tunnel network a TAP interface carrying Ethernet over UDP, carrier from keepalives
ram-disk block a sparse RAM disk
edu-disk block PCI: a 4 KiB disk kept in QEMU's edu device, every transfer a DMA with an interrupt

10. Limits in 1.2

  • Classes are input, sensor, network and block. There is no display or audio class (those stay with the compositor and PipeWire).
  • Block devices have one queue and a synchronous handler.
  • Without an IOMMU, PCI drivers have no DMA.
  • A device a kernel driver owns is never taken automatically.
  • No binary compatibility between SDK major versions; rebuild.
Mota Driver SDK 1.2 for x86_64 Linux (glibc 2.39+). Already on Mota OS 1.2? It's preinstalled.
SHA-256 5a4df2cb4a4d28f688fa52b9be8c9ab24e27b9f00b2979bdf5c488d9f19d3614