# `BB.Sensor`
[🔗](https://github.com/beam-bots/bb/blob/main/lib/bb/sensor.ex#L5)

Behaviour and API for sensors in the BB framework.

This module serves two purposes:

1. **Behaviour** - Defines callbacks for sensor implementations
2. **API** - Provides functions for working with sensors

## Behaviour

Sensors read from hardware or other sources and publish messages. They can
be attached at the robot level, to links, or to joints.

## Usage

The `use BB.Sensor` macro sets up your module as a sensor callback module.
Your module is NOT a GenServer - the framework provides a wrapper GenServer
(`BB.Sensor.Server`) that delegates to your callbacks.

### Required Callbacks

- `init/1` - Initialise sensor state from resolved options

### Optional Callbacks

- `disarm/1` - Make hardware safe (only for sensors with active hardware)
- `handle_options/2` - React to parameter changes at runtime
- `handle_call/3`, `handle_cast/2`, `handle_info/2` - Standard GenServer-style callbacks
- `handle_continue/2`, `terminate/2` - Lifecycle callbacks
- `options_schema/0` - Define accepted configuration options

### Options Schema

If your sensor accepts configuration options, pass them via `:options_schema`:

    defmodule MyTemperatureSensor do
      use BB.Sensor,
        options_schema: [
          bus: [type: :string, required: true, doc: "I2C bus name"],
          address: [type: :integer, required: true, doc: "I2C device address"],
          poll_interval_ms: [type: :pos_integer, default: 1000, doc: "Poll interval"]
        ]

      @impl BB.Sensor
      def init(opts) do
        bus = Keyword.fetch!(opts, :bus)
        address = Keyword.fetch!(opts, :address)
        bb = Keyword.fetch!(opts, :bb)
        {:ok, %{bus: bus, address: address, bb: bb}}
      end
    end

For sensors that don't need configuration, omit `:options_schema`:

    defmodule SimpleSensor do
      use BB.Sensor

      @impl BB.Sensor
      def init(opts) do
        {:ok, %{bb: opts[:bb]}}
      end
    end

### Parameter References

Options can reference parameters for runtime-adjustable configuration:

    sensor :temp, {MyTempSensor, poll_interval: param([:sensors, :poll_rate])}

When the parameter changes, `handle_options/2` is called with the new resolved
options. Override it to update your state accordingly.

### Auto-injected Options

The `:bb` option is automatically provided and should NOT be included in your
`options_schema`. It contains `%{robot: module, path: [atom]}`.

### Safety Registration

Most sensors don't require safety callbacks since they only read data.
If your sensor controls hardware that needs to be disabled on disarm
(e.g., a spinning LIDAR), implement the optional `disarm/1` callback:

    defmodule MyHardwareSensor do
      use BB.Sensor

      @impl BB.Sensor
      def init(opts), do: {:ok, %{}}

      @impl BB.Sensor
      def disarm(opts), do: stop_hardware(opts)
    end

When `disarm/1` is implemented, the framework automatically registers your
sensor with `BB.Safety`.

# `disarm`
*optional* 

```elixir
@callback disarm(opts :: keyword()) :: :ok | {:error, term()}
```

Make the hardware safe.

Called with the opts provided at registration. Must work without GenServer state.
This callback is optional for sensors - only implement it if your sensor
controls hardware that needs to be disabled on disarm (e.g., a spinning LIDAR).

# `handle_call`
*optional* 

```elixir
@callback handle_call(request :: term(), from :: GenServer.from(), state :: term()) ::
  {:reply, reply :: term(), new_state :: term()}
  | {:reply, reply :: term(), new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:noreply, new_state :: term()}
  | {:noreply, new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state :: term()}
  | {:stop, reason :: term(), reply :: term(), new_state :: term()}
```

Handle synchronous calls.

Same semantics as `c:GenServer.handle_call/3`.

# `handle_cast`
*optional* 

```elixir
@callback handle_cast(request :: term(), state :: term()) ::
  {:noreply, new_state :: term()}
  | {:noreply, new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state :: term()}
```

Handle asynchronous casts.

Same semantics as `c:GenServer.handle_cast/2`.

# `handle_continue`
*optional* 

```elixir
@callback handle_continue(continue_arg :: term(), state :: term()) ::
  {:noreply, new_state :: term()}
  | {:noreply, new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state :: term()}
```

Handle continue instructions.

Same semantics as `c:GenServer.handle_continue/2`.

# `handle_info`
*optional* 

```elixir
@callback handle_info(msg :: term(), state :: term()) ::
  {:noreply, new_state :: term()}
  | {:noreply, new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state :: term()}
```

Handle all other messages.

Same semantics as `c:GenServer.handle_info/2`.

# `handle_options`
*optional* 

```elixir
@callback handle_options(new_opts :: keyword(), state :: term()) ::
  {:ok, new_state :: term()} | {:stop, reason :: term()}
```

Handle parameter changes at runtime.

Called when a referenced parameter changes. The `new_opts` contain all options
with the updated parameter value(s) resolved.

Return `{:ok, new_state}` to update state, or `{:stop, reason}` to shut down.

# `init`

```elixir
@callback init(opts :: keyword()) ::
  {:ok, state :: term()}
  | {:ok, state :: term(), timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term()}
  | :ignore
```

Initialise sensor state from resolved options.

Called with options after parameter references have been resolved.
The `:bb` key contains `%{robot: module, path: [atom]}`.

Return `{:ok, state}` or `{:ok, state, timeout_or_continue}` on success,
`{:stop, reason}` to abort startup, or `:ignore` to skip this sensor.

# `options_schema`
*optional* 

```elixir
@callback options_schema() :: Spark.Options.t()
```

Returns the options schema for this sensor.

The schema should NOT include the `:bb` option - it is auto-injected.
If this callback is not implemented, the module cannot accept options
in the DSL (must be used as a bare module).

# `terminate`
*optional* 

```elixir
@callback terminate(reason :: term(), state :: term()) :: term()
```

Clean up before termination.

Same semantics as `c:GenServer.terminate/2`.

# `publish_joint_state`

```elixir
@spec publish_joint_state(module(), [atom()], keyword()) :: :ok
```

Publish a single-joint `JointState` message for the sensor at
`sensor_path`, converting the supplied sensor-space values into
joint-space before publishing.

`path` is the sensor's full path (i.e. the `:bb.path` injected into
driver opts). `opts` is the keyword list accepted by
`BB.Message.Sensor.JointState`'s schema, with `:positions`,
`:velocities`, and `:efforts` in the sensor's own coordinate space.

The published message goes to `[:sensor | path]`. The frame_id is the
joint name above the sensor (when joint-attached) or the sensor name
itself (otherwise).

# `to_joint_space`

```elixir
@spec to_joint_space(module(), [atom()], BB.Message.t()) :: BB.Message.t()
```

Translate a sensor-space outbound message into joint-space using the
transmission of the sensor at `sensor_path`.

Convenient for sensor drivers that build a message in their own
coordinate space and then publish it on a topic of their own choosing.
Performs a fresh transmission resolution against the current parameter
store on every call, so it stays correct across runtime parameter
changes without the caller needing to subscribe.

Returns the message unchanged when the sensor has no transmission
(i.e. it isn't joint-attached, or its transmission block is absent).

---

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