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 ashandler.(setup, nil); return{:reply, iodata}(truncated towLength) or:stall. - OUT: the data stage (
wLengthbytes) is read first and the transfer thereby accepted, thenhandler.(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
@opaque endpoint()
An open endpoint file handle (see open_endpoint/2).
@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, ...).
@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.
The control-request handler. See the moduledoc for the contract.
@type setup() :: %{
request_type: 0..255,
request: 0..255,
value: 0..65535,
index: 0..65535,
length: 0..65535
}
A decoded SETUP request.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec close_endpoint(endpoint()) :: :ok
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.
@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).
@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}.
@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.
@spec stop(GenServer.server()) :: :ok
Unmount a FunctionFS instance.
@spec write(endpoint(), iodata()) :: {:ok, non_neg_integer()} | {:error, atom()}
The write-side twin of read/2. Returns {:ok, bytes_written}.