BodgeUSBGadget (BodgeUSBGadget v0.1.0)

Copy Markdown View Source

USB gadget (device-side) definition over configfs.

The mirror image of host-side USB (see the bodge_usb library): instead of talking to a USB device, this machine is one. A gadget is described by a spec (IDs, strings, functions, configurations), materialized as a configfs tree, and bound to a UDC (USB device controller). The kernel's function drivers (usb_f_hid, usb_f_acm, usb_f_ecm, usb_f_mass_storage, ...) implement the class protocol; chardev-backed functions (HID, serial) are then driven through any file API.

Pure filesystem plumbing: no processes, no hidden state. A t/0 is just the gadget's name and configfs path.

spec = %{
  vendor_id: 0xCAFE,
  product_id: 0xBABE,
  strings: %{manufacturer: "bodge", product: "demo", serialnumber: "g-1"},
  functions: %{
    "hid.usb0" => %{
      protocol: 0,
      subclass: 0,
      report_length: 8,
      report_desc: <<0x06, 0x00, 0xFF, 0x09, 0x01, 0xA1, 0x01, ...>>
    }
  },
  configs: %{
    "c.1" => %{configuration: "demo", max_power: 120, functions: ["hid.usb0"]}
  }
}

{:ok, g} = BodgeUSBGadget.define("demo", spec)
:ok = BodgeUSBGadget.bind(g, "dummy_udc.0")
{:ok, "/dev/hidg0"} = BodgeUSBGadget.device_node(g, "hid.usb0")
...
:ok = BodgeUSBGadget.unbind(g)
:ok = BodgeUSBGadget.remove(g)

Requirements: a UDC (/sys/class/udc non-empty; OTG-capable hardware or dummy_hcd), configfs mounted, libcomposite plus the usb_f_* modules for the functions used, and permissions on /sys/kernel/config (root).

Function attribute values: integers are written in decimal, binaries raw (e.g. report_desc), booleans as 1/0. max_power is in mA. os_desc (Windows compatibility descriptors) is not covered; for fully custom device functions see BodgeUSBGadget.FunctionFs.

Summary

Types

Gadget description. functions maps "type.instance" names to attribute maps; configs maps "label.N" names to %{configuration: String.t(), max_power: mA, functions: [function_name]}. Extra raw gadget attributes (e.g. "bDeviceClass") go under :attrs.

t()

A defined gadget: its configfs directory.

Functions

Bind the gadget to a UDC, making it appear on the bus. udc defaults to the first controller in udcs/0.

Create the configfs tree for name from spec. Returns {:ok, gadget}; on any failure the partial tree is torn down again and {:error, reason} is returned, where reason is an atom or a {tag, detail} tuple (e.g. {:function_failed, {"hid.usb0", :enoent}}). Fails with {:error, :already_defined} if a gadget of that name exists. opts[:root] overrides the configfs root (tests).

The /dev node backing a chardev function, resolved through the function's dev attribute (major:minor -> /sys/dev/char), with the acm port number as a fallback. E.g. {:ok, "/dev/hidg0"} for "hid.usb0".

The network interface name created by an ethernet-style function (ecm/ncm/rndis/eem), from its ifname attribute.

Unbind (if bound) and delete the gadget's configfs tree. configfs requires a strict order (config symlinks, then config/function/string directories, then the gadget itself); every step is best-effort so a half-built tree is also removed. Returns :ok once the gadget directory is gone.

The UDC the gadget is bound to, or :unbound.

List the available UDCs (/sys/class/udc).

Unbind the gadget from its UDC (disconnect). Idempotent.

Types

spec()

@type spec() :: %{
  optional(:vendor_id) => 0..65535,
  optional(:product_id) => 0..65535,
  optional(:bcd_usb) => 0..65535,
  optional(:bcd_device) => 0..65535,
  optional(:attrs) => %{optional(String.t()) => term()},
  optional(:strings) => %{optional(atom()) => String.t()},
  optional(:functions) => %{optional(String.t()) => map()},
  optional(:configs) => %{optional(String.t()) => map()}
}

Gadget description. functions maps "type.instance" names to attribute maps; configs maps "label.N" names to %{configuration: String.t(), max_power: mA, functions: [function_name]}. Extra raw gadget attributes (e.g. "bDeviceClass") go under :attrs.

t()

@type t() :: %BodgeUSBGadget{name: String.t(), path: Path.t()}

A defined gadget: its configfs directory.

Functions

bind(gadget, udc \\ nil)

@spec bind(t(), String.t() | nil) :: :ok | {:error, term()}

Bind the gadget to a UDC, making it appear on the bus. udc defaults to the first controller in udcs/0.

define(name, spec, opts \\ [])

@spec define(String.t(), spec(), keyword()) :: {:ok, t()} | {:error, term()}

Create the configfs tree for name from spec. Returns {:ok, gadget}; on any failure the partial tree is torn down again and {:error, reason} is returned, where reason is an atom or a {tag, detail} tuple (e.g. {:function_failed, {"hid.usb0", :enoent}}). Fails with {:error, :already_defined} if a gadget of that name exists. opts[:root] overrides the configfs root (tests).

device_node(gadget, function)

@spec device_node(t(), String.t()) :: {:ok, Path.t()} | {:error, term()}

The /dev node backing a chardev function, resolved through the function's dev attribute (major:minor -> /sys/dev/char), with the acm port number as a fallback. E.g. {:ok, "/dev/hidg0"} for "hid.usb0".

network_interface(gadget, function)

@spec network_interface(t(), String.t()) :: {:ok, String.t()} | {:error, term()}

The network interface name created by an ethernet-style function (ecm/ncm/rndis/eem), from its ifname attribute.

remove(gadget)

@spec remove(t()) :: :ok | {:error, term()}

Unbind (if bound) and delete the gadget's configfs tree. configfs requires a strict order (config symlinks, then config/function/string directories, then the gadget itself); every step is best-effort so a half-built tree is also removed. Returns :ok once the gadget directory is gone.

udc(gadget)

@spec udc(t()) :: {:ok, String.t()} | :unbound | {:error, term()}

The UDC the gadget is bound to, or :unbound.

udcs()

@spec udcs() :: [String.t()]

List the available UDCs (/sys/class/udc).

unbind(gadget)

@spec unbind(t()) :: :ok | {:error, term()}

Unbind the gadget from its UDC (disconnect). Idempotent.