# `CircuitsFT232H.Device`

A GenServer that owns the USB handle to one FT232H and serialises all
traffic with it.

Started once per physical chip and registered in `CircuitsFT232H.Registry`
by the device's id (currently `"<bus>:<address>"` — once we read FTDI
serial numbers this becomes the serial). All MPSSE traffic flows through
`transaction/4`, so the chip sees commands from at most one process at a
time.

The Device enforces two kinds of allocation:

  * **I2C/SPI mutual-exclusion lock** — at most one of `:i2c` or `:spi` may
    be active on a chip at once. `claim_mode/2` returns
    `{:error, {:mode_busy, current}}` for a conflicting claim.
  * **GPIO pin allocation** — individual pins may be opened by GPIO
    consumers, and the Device tracks which pins are claimed in which
    direction. Pin claims are checked against the active protocol's
    reserved pin set so the two don't fight over the same wires.

# `id`

```elixir
@type id() :: String.t()
```

Canonical identifier for a physical FT232H.

Prefers the chip's FTDI-programmed serial number string when one is
available (so the id is stable across replugs). Falls back to
`"<bus>:<address>"` if the chip has no serial or it couldn't be read
(e.g. permission denied).

# `mode`

```elixir
@type mode() :: :none | :i2c | :spi
```

Protocol modes a chip can be locked into.

# `pin`

```elixir
@type pin() :: 0..15
```

Linear pin index. `0..7` map to ADBUS0..ADBUS7 ("AD0".."AD7"); `8..15`
map to ACBUS0..ACBUS7 ("AC0".."AC7").

# `pin_direction`

```elixir
@type pin_direction() :: :input | :output
```

Direction of a GPIO pin.

# `t`

```elixir
@type t() :: %CircuitsFT232H.Device{
  acbus_dir: byte(),
  acbus_value: byte(),
  adbus_dir: byte(),
  adbus_value: byte(),
  extra_reserved_pins: [pin()],
  gpio_pins: %{optional(pin()) =&gt; pin_direction()},
  id: id() | nil,
  mode: mode(),
  usb: CircuitsFT232H.USB.t() | nil
}
```

Internal GenServer state.

# `claim_gpio_pin`

```elixir
@spec claim_gpio_pin(id(), pin(), pin_direction(), 0..1) :: :ok | {:error, term()}
```

Claims `pin` for GPIO use in the given `direction`. Fails if the pin is
already claimed, if it's reserved by the active protocol, or if `pin` is
out of range.

On success the pin is initialised — direction set, and for outputs the
initial value driven.

# `claim_mode`

```elixir
@spec claim_mode(id(), :i2c | :spi, [pin()]) ::
  :ok | {:error, {:mode_busy, mode()} | {:pin_busy, pin()}}
```

Claims one of the mutually-exclusive protocol modes for the chip.

Returns `:ok` if the chip is currently unclaimed (or already claimed in
the requested mode), or `{:error, {:mode_busy, current_mode}}` otherwise.
Also fails with `{:error, {:pin_busy, pin}}` if any currently-open GPIO
pin would conflict with the new mode.

`extra_pins` lets a backend reserve additional pins beyond the protocol's
defaults — e.g. the I2C backend reserves `AD7` when clock stretching is
enabled, so a GPIO consumer can't grab the SCL-feedback pin.

# `find_or_start`

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

Starts a Device under `CircuitsFT232H.DeviceSupervisor` if one isn't
already running for the descriptor's id.

Returns the existing pid if one is found. Use this from the I2C/SPI/GPIO
backends — multiple buses may map to the same physical chip.

# `gpio_pin_direction`

```elixir
@spec gpio_pin_direction(id(), pin()) :: pin_direction() | nil
```

Returns the direction currently configured for `pin`, or `nil` if the pin
isn't open.

# `id_for`

```elixir
@spec id_for(CircuitsFT232H.USB.Descriptor.t()) :: id()
```

Returns the canonical id for a USB descriptor.

# `mode`

```elixir
@spec mode(id()) :: mode()
```

Returns the current protocol mode the chip is locked into.

# `protocol_pins`

```elixir
@spec protocol_pins(mode()) :: [pin()]
```

Returns the list of pins reserved by the given protocol mode. Pins outside
this list are available for GPIO use.

# `read_gpio_bytes`

```elixir
@spec read_gpio_bytes(id()) :: {:ok, {byte(), byte()}} | {:error, term()}
```

Reads the current state of the low-byte (ADBUS0..7) and high-byte
(ACBUS0..7) ports in a single USB round-trip. Used by the GPIO poller to
detect edges across many pins without paying per-pin latency.

# `read_gpio_pin`

```elixir
@spec read_gpio_pin(id(), pin()) :: 0..1 | {:error, term()}
```

Reads the current logic level of a previously-opened GPIO pin.

# `release_gpio_pin`

```elixir
@spec release_gpio_pin(id(), pin()) :: :ok
```

Releases a previously-claimed GPIO pin and switches it back to input.

# `release_mode`

```elixir
@spec release_mode(id()) :: :ok
```

Releases whichever mode the chip is currently locked into.

# `set_gpio_pin_direction`

```elixir
@spec set_gpio_pin_direction(id(), pin(), pin_direction()) :: :ok | {:error, term()}
```

Changes the direction of an already-opened GPIO pin. The pin keeps its
current driven value when switching to output.

# `start_link`

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

Starts a Device server for the FT232H described by `:descriptor`.

Registers under the device's id in `CircuitsFT232H.Registry`. Subsequent
callers can locate the server via `whereis/1` or by referring to it by
id in any of the public functions on this module.

Options:
  * `:descriptor` (required) — the `CircuitsFT232H.USB.Descriptor` for
    the chip.
  * `:latency_ms` (default `16`) — USB latency timer.

# `stop`

```elixir
@spec stop(id()) :: :ok
```

Stops the Device server for the given id.

# `transaction`

```elixir
@spec transaction(id(), iodata(), non_neg_integer(), timeout()) ::
  :ok | {:ok, binary()} | {:error, term()}
```

Sends `command` (an MPSSE byte stream) to the chip and optionally reads
`response_length` bytes back.

`command` may be any `iodata`. If `response_length` is `0` the call
returns `:ok` once the bytes have been queued to the chip. Otherwise it
returns `{:ok, binary}` of exactly `response_length` payload bytes (the
USB status prefix is stripped).

Use this for protocol-level MPSSE sequences. For pin-level operations,
prefer `write_gpio_pin/3`, `read_gpio_pin/2`, and friends — they share
the Device's pin-state tracking so GPIO and protocol traffic don't
trample each other's bytes.

# `validate`

```elixir
@spec validate(id()) :: :ok | {:error, term()}
```

Verifies the chip is in MPSSE mode and responding to commands by issuing
an unrecognised opcode and checking for the standard `0xFA <opcode>` echo.

# `whereis`

```elixir
@spec whereis(id()) :: {:ok, pid()} | {:error, :not_started}
```

Finds the pid of the Device server with the given id, if one is running.

# `write_gpio_pin`

```elixir
@spec write_gpio_pin(id(), pin(), 0..1) :: :ok | {:error, term()}
```

Writes `value` (0 or 1) to a previously-opened GPIO output pin.

---

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