BodgeUSBGadget.FunctionFs (BodgeUSBGadget v0.1.0)

Copy Markdown View Source

Custom USB device functions over FunctionFS.

Where BodgeUSBGadget covers the device classes the kernel implements, FunctionFS is for functions the kernel does not know: your own protocol, served from Elixir. The kernel handles enumeration and the composite plumbing; SETUP requests for this function arrive here as events, and the function's endpoints are files.

Lifecycle

# 1. A gadget with an ffs function (instance name "netmd"):
{:ok, g} = BodgeUSBGadget.define("player", %{functions: %{"ffs.netmd" => %{}}, ...})

# 2. Mount the instance and start the function:
:ok = FunctionFs.mount("netmd", "/dev/ffs-netmd")

{:ok, fun} =
  FunctionFs.start_link(
    mountpoint: "/dev/ffs-netmd",
    function: %{
      interface: %{class: 0xFF},
      endpoints: [%{address: 0x01, type: :bulk}, %{address: 0x81, type: :bulk}],
      flags: [:all_ctrl_recip]
    },
    strings: ["NetMD"],
    handler: &MyProtocol.handle_setup/2
  )

# 3. Only now can the gadget bind (FunctionFS must have its descriptors):
:ok = BodgeUSBGadget.bind(g, udc)

Control transfers: the handler

handler is a 2-arity fun called in the server for each SETUP aimed at this function (flags: [:all_ctrl_recip] includes device-recipient requests):

  • IN (setup.request_type >= 0x80): called as handler.(setup, nil); return {:reply, iodata} (truncated to wLength) or :stall.
  • OUT: the data stage (wLength bytes) is read first and the transfer thereby accepted, then handler.(setup, data) is called; the return value is ignored. (v1 limitation: OUT requests cannot be stalled.)

A crashing handler stalls the request and the server keeps running.

Everything else

Lifecycle events are sent to opts[:notify] (default: the caller) as {:functionfs, server, :bound | :enabled | :disabled | :unbound | :suspend | :resume}. :enabled means the host configured the device: endpoints are live from that point.

Endpoint I/O: open_endpoint/2 opens epN (numbered in declaration order, from 1). These files block until the host transacts and are not pollable, so drive them with read/2 and write/2 from their own process (a Task per direction is the usual shape); each in-flight call occupies a dirty I/O scheduler. Unbinding the gadget unblocks them with {:error, :eshutdown}.

Summary

Types

An open endpoint file handle (see open_endpoint/2).

One endpoint of the function, in declaration order (ep1, ep2, ...).

The function description the ep0 blobs are built from.

The control-request handler. See the moduledoc for the contract.

A decoded SETUP request.

Functions

Returns a specification to start this module under a supervisor.

Close an endpoint handle. Idempotent; unblocks nothing by itself.

Mount a FunctionFS instance (the NAME of an ffs.NAME gadget function) at mountpoint, creating the directory if needed. Needs root.

Open endpoint file epN for the function at mountpoint. Endpoints are numbered in descriptor declaration order, starting at 1. Drive the handle with read/2 and write/2; close it with close_endpoint/1 (a garbage collected handle also closes).

Read up to count bytes from an endpoint. Blocks until the host transacts (dirty I/O scheduler); unbinding the gadget makes a blocked read return {:error, :eshutdown}.

Start the function: opens ep0 under opts[:mountpoint], writes the descriptor and string blobs (built from opts[:function], a function_spec/0, and opts[:strings]), and serves SETUP events with opts[:handler]. After this returns, the gadget can be bound. opts[:notify] (default: the caller) receives lifecycle events; opts[:name] optionally names the server.

Unmount a FunctionFS instance.

The write-side twin of read/2. Returns {:ok, bytes_written}.

Types

endpoint()

@opaque endpoint()

An open endpoint file handle (see open_endpoint/2).

endpoint_spec()

@type endpoint_spec() :: %{
  :address => 0..255,
  :type => :bulk | :interrupt | :isochronous,
  optional(:max_packet_size) => 1..65535,
  optional(:interval) => 0..255
}

One endpoint of the function, in declaration order (ep1, ep2, ...).

function_spec()

@type function_spec() :: %{
  :interface => %{
    optional(:class) => 0..255,
    optional(:subclass) => 0..255,
    optional(:protocol) => 0..255,
    optional(:string_index) => 0..255
  },
  :endpoints => [endpoint_spec()],
  optional(:flags) => [:all_ctrl_recip]
}

The function description the ep0 blobs are built from.

handler()

@type handler() :: (setup(), binary() | nil -> {:reply, iodata()} | :stall | :ok)

The control-request handler. See the moduledoc for the contract.

setup()

@type setup() :: %{
  request_type: 0..255,
  request: 0..255,
  value: 0..65535,
  index: 0..65535,
  length: 0..65535
}

A decoded SETUP request.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

close_endpoint(endpoint)

@spec close_endpoint(endpoint()) :: :ok

Close an endpoint handle. Idempotent; unblocks nothing by itself.

mount(instance, mountpoint)

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

Mount a FunctionFS instance (the NAME of an ffs.NAME gadget function) at mountpoint, creating the directory if needed. Needs root.

open_endpoint(mountpoint, n)

@spec open_endpoint(Path.t(), pos_integer()) :: {:ok, endpoint()} | {:error, atom()}

Open endpoint file epN for the function at mountpoint. Endpoints are numbered in descriptor declaration order, starting at 1. Drive the handle with read/2 and write/2; close it with close_endpoint/1 (a garbage collected handle also closes).

read(endpoint, count)

@spec read(endpoint(), non_neg_integer()) :: {:ok, binary()} | {:error, atom()}

Read up to count bytes from an endpoint. Blocks until the host transacts (dirty I/O scheduler); unbinding the gadget makes a blocked read return {:error, :eshutdown}.

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Start the function: opens ep0 under opts[:mountpoint], writes the descriptor and string blobs (built from opts[:function], a function_spec/0, and opts[:strings]), and serves SETUP events with opts[:handler]. After this returns, the gadget can be bound. opts[:notify] (default: the caller) receives lifecycle events; opts[:name] optionally names the server.

ep0 is opened and the blobs are written before the server is started, so a failure (unmounted, no permissions, descriptors rejected) returns {:error, reason} without starting a process to crash a non-trapping caller through the link.

stop(server)

@spec stop(GenServer.server()) :: :ok

umount(mountpoint)

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

Unmount a FunctionFS instance.

write(endpoint, data)

@spec write(endpoint(), iodata()) :: {:ok, non_neg_integer()} | {:error, atom()}

The write-side twin of read/2. Returns {:ok, bytes_written}.