# `CircuitsFT232H.USB`

Thin USB transport layer for the FT232H.

Wraps the [`:usb`](https://hex.pm/packages/usb) NIF (libusb) with
FT232H-specific endpoint addresses, FTDI SIO control-transfer helpers, and
modem-status-byte stripping for bulk reads.

This module is internal — higher layers (`CircuitsFT232H.MPSSE`,
`CircuitsFT232H.Device`) compose it into something useful. You normally
shouldn't need to call it directly.

# `t`

```elixir
@type t() :: %CircuitsFT232H.USB{
  address: 0..255,
  bus: 0..255,
  detached_kernel_driver?: boolean(),
  handle: :usb.device_handle(),
  max_packet_size: pos_integer()
}
```

An open FT232H USB handle.

# `close`

```elixir
@spec close(t()) :: :ok
```

Releases the interface, re-attaches the kernel driver if we detached it on
open, and closes the device.

Always returns `:ok`. Errors during teardown are logged but not surfaced —
by the time we're closing there is nothing useful for the caller to do.

# `decode_string_descriptor`

```elixir
@spec decode_string_descriptor(binary()) :: {:ok, String.t()} | {:error, term()}
```

Decodes a raw USB string descriptor binary into an Elixir string.

The on-wire format is `<<length, 0x03, utf16_le_bytes::binary>>`. Returns
`{:error, :invalid_descriptor}` if the input doesn't fit that shape
(typical when the chip returns `0xFF`-filled garbage for an unprogrammed
string), and `{:error, :invalid_utf16}` if the bytes after the header
don't decode as UTF-16LE.

# `disable_special_characters`

```elixir
@spec disable_special_characters(t(), timeout()) :: :ok | {:error, term()}
```

Disables the FTDI event/error characters (we don't use them in MPSSE).

# `enter_mpsse_mode`

```elixir
@spec enter_mpsse_mode(t(), timeout()) :: :ok | {:error, term()}
```

Puts the chip into MPSSE mode. After this, `write/2` to the bulk OUT
endpoint is interpreted as a stream of MPSSE commands.

# `exit_bitmode`

```elixir
@spec exit_bitmode(t(), timeout()) :: :ok | {:error, term()}
```

Resets bitmode to its default (UART). Useful to drop out of MPSSE before
releasing the device.

# `list_devices`

```elixir
@spec list_devices() :: {:ok, [CircuitsFT232H.USB.Descriptor.t()]} | {:error, term()}
```

Lists every FT232H currently attached to the host's USB bus.

Identification is by VID `0x403`,
PID `0x6014` — the values used by
the raw FT232H and the Adafruit FT232H Breakout.

# `open`

```elixir
@spec open(
  CircuitsFT232H.USB.Descriptor.t(),
  keyword()
) :: {:ok, t()} | {:error, term()}
```

Opens `descriptor` and prepares the chip for MPSSE traffic.

Detaches the kernel driver if one is attached (typically `ftdi_sio` on Linux
or Apple's FTDI VCP driver on macOS) and claims the FT232H's single USB
interface. Once open, use `write/2` and `read/3` to push and pull MPSSE
bytes, and the various `set_*`/`purge_*`/`reset/1` helpers to configure the
SIO via FTDI control transfers.

Pair every successful `open/2` with `close/1`.

# `purge_rx`

```elixir
@spec purge_rx(t(), timeout()) :: :ok | {:error, term()}
```

Purges the chip's RX FIFO.

# `purge_tx`

```elixir
@spec purge_tx(t(), timeout()) :: :ok | {:error, term()}
```

Purges the chip's TX FIFO.

# `read`

```elixir
@spec read(t(), non_neg_integer(), timeout()) :: {:ok, binary()} | {:error, term()}
```

Reads exactly `length` bytes of MPSSE payload from the chip.

Each USB packet from FT232H is prefixed by 2 modem-status bytes; this
function strips them before returning the result, so `length` refers to
*real* payload bytes.

# `read_string_descriptor`

```elixir
@spec read_string_descriptor(:usb.device(), 1..255) ::
  {:ok, String.t()} | {:error, term()}
```

Reads a USB string descriptor at the given index.

Used for retrieving the FTDI serial number, manufacturer string, or
product string. The first call reads the supported language IDs (index
0), then re-reads at the requested index in the first language.

Most failures are normal: `:access` if the udev rule isn't installed,
`:invalid_descriptor` if the chip has no programmed string at that index
(common for FT232Hs whose EEPROM hasn't been written).

# `reset`

```elixir
@spec reset(t(), timeout()) :: :ok | {:error, term()}
```

Resets the FTDI SIO. Equivalent to issuing `SIO_RESET` with `wValue = 0`.

# `set_bitmode`

```elixir
@spec set_bitmode(t(), 0..255, 0..255, timeout()) :: :ok | {:error, term()}
```

Sets the bitmode (high byte) and pin direction mask (low byte) on the
low byte port (ADBUS).

The direction byte sets each pin to output (1) or input (0). In MPSSE
mode the MPSSE engine overrides this for SCK/MOSI/MISO/CS, but the
initial value matters for any GPIOs.

# `set_latency_timer`

```elixir
@spec set_latency_timer(t(), 1..255, timeout()) :: :ok | {:error, term()}
```

Sets the chip's USB latency timer to `milliseconds` (1..255).

Lower values give snappier reads at the cost of more USB traffic; MPSSE
applications typically use 1-16 ms. The chip default is 16 ms.

# `strip_status`

```elixir
@spec strip_status(binary(), pos_integer()) :: binary()
```

Strips the 2-byte modem-status prefix from every USB packet in `data`.

`packet_size` is the bulk endpoint's max packet size (typically 512 for
high-speed). Exposed because it's a pure function and the easiest piece of
this module to unit-test.

# `write`

```elixir
@spec write(t(), iodata(), timeout()) :: :ok | {:error, term()}
```

Sends raw MPSSE bytes to the chip over the bulk OUT endpoint.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
