BodgeUSB
Talk to USB devices from Elixir on Linux, built on a USB-scoped syscall NIF over usbfs. It drives arbitrary USB devices from the BEAM without shelling out to libusb and without a generic "call any ioctl" gateway: the NIF exposes only a small, fixed, size-validated set of usbfs ioctls whose struct layouts it owns.
Each open device is one BodgeUSB process running an asynchronous URB engine
(enif_select-driven, no blocked schedulers). The blocking calls (bulk_in/4
and friends) are submit + await over that engine, so one process can also
pipeline many transfers and receive completions as messages.
Device-side USB (being a gadget, configfs/FunctionFS) lives in the companion
library bodge_usb_gadget.
Linux only. See harness/ for the fully virtual test rig (dummy_hcd,
g_zero/usbtest, raw-gadget fault injection, usbmon).
Features
- Enumeration and defensive descriptor parsing (device/config/interface/ endpoint/string) into structs; parsing is total (never raises).
- Control, bulk, interrupt, and isochronous transfers, with per-transfer timeouts and cancellation.
- The async primitive under the blocking calls:
submit/3returns a ref, the completion arrives as a{:bodge_usb, ref, result}message. - Interface claim/release and kernel-driver detach/reattach.
- Endpoint-stall recovery (
clear_halt), device reset, and typed handling of mid-transfer disconnect. - Hotplug notifications over the kernel netlink uevent socket.
Example
{:ok, dev} = BodgeUSB.open(0x0525, 0xA4A0)
BodgeUSB.detach_driver(dev, 0)
:ok = BodgeUSB.claim_interface(dev, 0)
{:ok, data} = BodgeUSB.bulk_in(dev, 0x81, 512)
{:ok, _n} = BodgeUSB.bulk_out(dev, 0x02, data)
# Async: pipeline transfers from one process, completions as messages.
{:ok, ref} = BodgeUSB.submit(dev, {:bulk_in, 0x81, 4096}, timeout: 1000)
receive do
{:bodge_usb, ^ref, {:ok, bytes}} -> byte_size(bytes)
end
BodgeUSB.close(dev)
{:ok, _hp} = BodgeUSB.watch_hotplug()
receive do
{:usb_hotplug, %{action: :add, busnum: b, devnum: d}} -> IO.inspect({b, d})
end
Permissions
Accessing /dev/bus/usb and the netlink uevent socket needs root or
appropriate udev rules / capabilities.
Installation
def deps do
[
{:bodge_usb, "~> 0.1.0"}
]
end