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.execsays 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.