# `Feetech`
[🔗](https://github.com/beam-bots/feetech/blob/main/lib/feetech.ex#L5)

Driver for Feetech TTL-based serial bus servos.

This module provides a GenServer-based interface for communicating with
Feetech servos using their proprietary serial protocol.

## Basic Usage

    # Start the driver (defaults to STS3215 control table)
    {:ok, pid} = Feetech.start_link(port: "/dev/ttyUSB0")

    # Check if servo is responding
    {:ok, status} = Feetech.ping(pid, 1)

    # Read current position (radians)
    {:ok, position} = Feetech.read(pid, 1, :present_position)

    # Move to position (radians)
    :ok = Feetech.write(pid, 1, :goal_position, 1.57)

    # Read raw position (steps)
    {:ok, steps} = Feetech.read_raw(pid, 1, :present_position)

## Operating Modes

Servos support multiple operating modes:

  * `:position` - Standard position control (default)
  * `:velocity` - Continuous rotation with speed control
  * `:step` - Stepper mode for multi-turn positioning

Change modes by writing to the `:mode` register.

## Bulk Operations

For controlling multiple servos simultaneously:

    # Write to multiple servos at once
    Feetech.sync_write(pid, :goal_position, [
      {1, 1.57},
      {2, 0.0},
      {3, -1.57}
    ])

    # Read from multiple servos
    {:ok, positions} = Feetech.sync_read(pid, [1, 2, 3], :present_position)

## Buffered Writes

For synchronized movement across multiple servos:

    # Buffer commands (servos don't move yet)
    Feetech.reg_write(pid, 1, :goal_position, 1.57)
    Feetech.reg_write(pid, 2, :goal_position, 0.0)

    # Execute all buffered commands simultaneously
    Feetech.action(pid)

# `option`

```elixir
@type option() ::
  {:port, String.t()}
  | {:baud_rate, pos_integer()}
  | {:control_table, module()}
  | {:timeout, pos_integer()}
  | {:name, GenServer.name()}
```

# `register_name`

```elixir
@type register_name() :: atom()
```

# `servo_id`

```elixir
@type servo_id() :: 0..254
```

# `action`

```elixir
@spec action(GenServer.server()) :: :ok
```

Triggers all buffered `reg_write` commands.

Typically sent to all servos using the broadcast ID.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `ping`

```elixir
@spec ping(GenServer.server(), servo_id()) ::
  {:ok, Feetech.Error.status_info()} | {:error, Feetech.Error.error()}
```

Pings a servo to check if it's responding.

Returns the parsed status from the servo.

## Examples

    {:ok, %{errors: [], torque_enabled: true}} = Feetech.ping(pid, 1)

# `read`

```elixir
@spec read(GenServer.server(), servo_id(), register_name()) ::
  {:ok, term()} | {:error, Feetech.Error.error()}
```

Reads a register value with unit conversion.

The value is converted according to the control table specification
(e.g., steps to radians for position).

## Examples

    {:ok, 1.57} = Feetech.read(pid, 1, :present_position)  # radians
    {:ok, true} = Feetech.read(pid, 1, :torque_enable)     # boolean

# `read_raw`

```elixir
@spec read_raw(GenServer.server(), servo_id(), register_name()) ::
  {:ok, integer()} | {:error, Feetech.Error.error()}
```

Reads a register value as a raw integer (no conversion).

## Examples

    {:ok, 2048} = Feetech.read_raw(pid, 1, :present_position)  # steps

# `recovery`

```elixir
@spec recovery(GenServer.server(), servo_id()) ::
  {:ok, Feetech.Error.status_info()} | {:error, Feetech.Error.error()}
```

Resets a servo's control table to factory defaults.

# `reg_write`

```elixir
@spec reg_write(GenServer.server(), servo_id(), register_name(), term()) ::
  :ok | {:ok, Feetech.Error.status_info()} | {:error, Feetech.Error.error()}
```

Writes a buffered command that executes on `action/1`.

Use this to synchronize movement across multiple servos.

## Examples

    :ok = Feetech.reg_write(pid, 1, :goal_position, 1.57)
    :ok = Feetech.reg_write(pid, 2, :goal_position, 0.0)
    :ok = Feetech.action(pid)  # Both servos move simultaneously

# `reset`

```elixir
@spec reset(GenServer.server(), servo_id()) ::
  {:ok, Feetech.Error.status_info()} | {:error, Feetech.Error.error()}
```

Resets a servo's state (rotation count for multi-turn mode).

# `start_link`

```elixir
@spec start_link([option()]) :: GenServer.on_start()
```

Starts the Feetech driver.

## Options

  * `:port` - Serial port path (required), e.g., `"/dev/ttyUSB0"`
  * `:baud_rate` - Baud rate, defaults to 1,000,000
  * `:control_table` - Control table module, defaults to `Feetech.ControlTable.STS3215`
  * `:timeout` - Response timeout in ms, defaults to 100
  * `:name` - GenServer name for registration

## Examples

    {:ok, pid} = Feetech.start_link(port: "/dev/ttyUSB0")

# `stop`

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

Closes the UART connection and stops the driver.

# `sync_read`

```elixir
@spec sync_read(GenServer.server(), [servo_id()], register_name()) ::
  {:ok, [term()]} | {:error, Feetech.Error.error()}
```

Reads converted values from multiple servos.

Returns values in the same order as the ID list.

## Examples

    {:ok, [1.57, 0.0, -1.57]} = Feetech.sync_read(pid, [1, 2, 3], :present_position)

# `sync_read_raw`

```elixir
@spec sync_read_raw(GenServer.server(), [servo_id()], register_name()) ::
  {:ok, [integer()]} | {:error, Feetech.Error.error()}
```

Reads raw values from multiple servos.

# `sync_write`

```elixir
@spec sync_write(GenServer.server(), register_name(), [{servo_id(), term()}]) ::
  :ok | {:error, Feetech.Error.error()}
```

Writes converted values to multiple servos simultaneously.

## Examples

    :ok = Feetech.sync_write(pid, :goal_position, [
      {1, 1.57},
      {2, 0.0},
      {3, -1.57}
    ])

# `sync_write_raw`

```elixir
@spec sync_write_raw(GenServer.server(), register_name(), [{servo_id(), integer()}]) ::
  :ok | {:error, Feetech.Error.error()}
```

Writes raw values to multiple servos simultaneously.

# `write`

```elixir
@spec write(GenServer.server(), servo_id(), register_name(), term(), keyword()) ::
  :ok | {:ok, Feetech.Error.status_info()} | {:error, Feetech.Error.error()}
```

Writes a converted value to a register.

The value is converted from user units (e.g., radians) to raw units
according to the control table specification.

By default, this is a fire-and-forget operation. Use `await_response: true`
to wait for acknowledgement.

## Options

  * `:await_response` - Wait for servo response (default: false)

## Examples

    :ok = Feetech.write(pid, 1, :goal_position, 1.57)
    {:ok, status} = Feetech.write(pid, 1, :goal_position, 1.57, await_response: true)

# `write_raw`

```elixir
@spec write_raw(GenServer.server(), servo_id(), register_name(), integer(), keyword()) ::
  :ok | {:ok, Feetech.Error.status_info()} | {:error, Feetech.Error.error()}
```

Writes a raw integer value to a register (no conversion).

## Examples

    :ok = Feetech.write_raw(pid, 1, :goal_position, 2048)

---

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