# `ModBoss`
[🔗](https://github.com/goodpixel/modboss/blob/v0.2.0/lib/modboss.ex#L1)

Human-friendly modbus reading, writing, and translation.

Read and write modbus values by name, with automatic encoding and decoding.

## Telemetry

ModBoss optionally emits telemetry events for reads and writes via the
[`:telemetry`](https://hex.pm/packages/telemetry) library. See
`ModBoss.Telemetry` for all event names, measurements, and metadata.

# `read_func`

```elixir
@type read_func() :: (ModBoss.Mapping.object_type(),
                starting_address :: ModBoss.Mapping.address(),
                count :: ModBoss.Mapping.count() -&gt;
                  {:ok, any()} | {:error, any()})
```

# `values`

```elixir
@type values() :: [{atom(), any()}] | %{required(atom()) =&gt; any()}
```

# `write_func`

```elixir
@type write_func() :: (ModBoss.Mapping.object_type(),
                 starting_address :: ModBoss.Mapping.address(),
                 value_or_values :: any() -&gt;
                   :ok | {:error, any()})
```

# `encode`

```elixir
@spec encode(module(), values(), keyword()) :: {:ok, map()} | {:error, String.t()}
```

Encode values per the mapping without actually writing them.

This can be useful in test scenarios and enables values to be encoded in bulk without actually
being written via Modbus.

Returns a map with keys of the form `{type, address}` and `encoded_value` as values.

## Opts
  * `:context` — a map of arbitrary data that will be included in the
    `ModBoss.Encoding.Metadata` struct passed to encode functions (when using
    2-arity encoders). Defaults to `%{}`.

## Example

    iex> ModBoss.encode(MyDevice.Schema, foo: "Yay")
    {:ok, %{{:holding_register, 15} => 22881, {:holding_register, 16} => 30976}}

# `read`

```elixir
@spec read(module(), atom() | [atom()], read_func(), keyword()) ::
  {:ok, any()} | {:error, any()}
```

Read from modbus using named mappings.

This function takes either an atom or a list of atoms representing the mappings to read,
batches the mappings into contiguous addresses per type, then reads and decodes the values
before returning them.

For each batch, `read_func` will be called with the type of modbus object (`:holding_register`,
`:input_register`, `:coil`, or `:discrete_input`), the starting address for the batch
to be read, and the count of addresses to read from. It must return either `{:ok, result}`
or `{:error, message}`.

If a single name is requested, the result will be an :ok tuple including the single result
for that named mapping. If a list of names is requested, the result will be an :ok tuple
including a map with mapping names as keys and mapping values as results.

## Opts
  * `:max_gap` — Allows reading across unrequested gaps to reduce the overall
    number of requests that need to be made. Results from gaps will be
    retrieved and discarded. This value can be specified as a single integer
    (which applies to all types) or per object type. Only readable ModBoss
    mappings will be included in gap reads. To opt a readable mapping out of
    gap reads, specify `gap_safe: false` on that mapping. See the `:gap_safe`
    option in `ModBoss.Schema` for details.
  * `:debug` — if `true`, returns a map of mapping details instead of just the value;
    defaults to `false`
  * `:decode` — if `false`, ModBoss doesn't attempt to decode the retrieved values;
    defaults to `true`. This option can be especially useful if you need insight
    into a particular value that is failing to decode as expected.
  * `:max_attempts` — maximum number of times each `read_func` callback will
    be attempted before giving up. Defaults to `1` (no retries). Only
    `{:error, _}` triggers a retry; exceptions are not retried.
  * `:context` — a map of arbitrary data that will be included in the
    `ModBoss.Encoding.Metadata` struct passed to decode functions (when using
    2-arity decoders) and in telemetry event metadata. Defaults to `%{}`.
    Useful for conditionally decoding values based on runtime information
    like firmware version or hardware revision, and for identifying which
    device or connection a request belongs to in telemetry handlers.

> #### Gaps {: .info}
>
> Every Modbus request incurs network round-trip overhead, so fewer, larger reads
> are often faster than many small ones—even if some of the addresses in between
> aren't needed.
>
> The `:max_gap` option allows you to specify how many unrequested addresses
> you're willing to include in a single read in order to bridge the gap between
> requested mappings and reduce the total number of requests.
> If enabled, **a single request may bridge multiple gaps, each up to that size.**
>
> A gap will only be bridged if **every** address within it belongs to a
> known, gap-safe mapping (`gap_safe: true`, the default for readable mappings).
> Unmapped addresses and mappings with `gap_safe: false` both prevent a gap
> from being bridged.
>
> For example, given this schema:
>
>     schema do
>       holding_register 1, :temp, as: {ModBoss.Encoding, :signed_int}
>       holding_register 2, :status, as: {ModBoss.Encoding, :unsigned_int}
>       holding_register 3, :error_count, as: {ModBoss.Encoding, :unsigned_int}, gap_safe: false
>       holding_register 4, :mode, as: {ModBoss.Encoding, :unsigned_int}
>       holding_register 5, :humidity, as: {ModBoss.Encoding, :unsigned_int}
>     end
>
> Reading `:temp` and `:humidity` with `max_gap: 5` will **not** batch them
> into a single request because address 3 (`:error_count`) is not gap-safe.
> Removing `:error_count` from the schema wouldn't help either—the address
> would then be unmapped, which also prevents bridging.

## Examples

    read_func = fn object_type, starting_address, count ->
      result = custom_read_logic(…)
      {:ok, result}
    end

    # Read one mapping
    ModBoss.read(SchemaModule, :foo, read_func)
    {:ok, 75}

    # Read multiple mappings
    ModBoss.read(SchemaModule, [:foo, :bar, :baz], read_func)
    {:ok, %{foo: 75, bar: "ABC", baz: true}}

    # Read *all* readable mappings
    ModBoss.read(SchemaModule, :all, read_func)
    {:ok, %{foo: 75, bar: "ABC", baz: true, qux: 1024}}

    # Enable reading extra registers to reduce the number of requests
    ModBoss.read(SchemaModule, [:foo, :bar], read_func, max_gap: 10)
    {:ok, %{foo: 75, bar: "ABC"}}

    # …or allow reading across different gap sizes per type
    ModBoss.read(SchemaModule, [:foo, :bar], read_func, max_gap: %{holding_register: 10})
    {:ok, %{foo: 75, bar: "ABC"}}

    # Get "raw" Modbus values (as returned by `read_func`)
    ModBoss.read(SchemaModule, [:foo, :bar], read_func, decode: false)
    {:ok, %{foo: [75], bar: [16706, 17152]}}

# `write`

```elixir
@spec write(module(), values(), write_func(), keyword()) :: :ok | {:error, any()}
```

Write to modbus using named mappings.

ModBoss automatically encodes your `values`, then batches any encoded values destined for
contiguous objects—creating separate batches per object type.

For each batch, `write_func` will be called with the type of object (`:holding_register` or
`:coil`), the starting address for the batch to be written, and a list of values to write.
It must return either `:ok` or `{:error, message}`.

> #### Batch values {: .info}
>
> Each batch will contain **either a list or an individual value** based on the number of
> addresses to be written—so you should be prepared for both.

> #### Non-atomic writes! {: .warning}
>
> While `ModBoss.write/4` has the _feel_ of being atomic, it's important to recognize that it
> is not! It's fully possible that a write might fail after prior writes within the same call to
> `ModBoss.write/4` have already succeeded.
>
> Within `ModBoss.write/4`, if any call to `write_func` returns an error tuple,
> the function will immediately abort, and any subsequent writes will be skipped.

## Opts
  * `:max_attempts` — maximum number of times each `write_func` callback will
    be attempted before giving up. Defaults to `1` (no retries). Only
    `{:error, _}` triggers a retry; exceptions are not retried.
  * `:context` — a map of arbitrary data that will be included in the
    `ModBoss.Encoding.Metadata` struct passed to encode functions (when using
    2-arity encoders) and in telemetry event metadata. Defaults to `%{}`.
    Useful for conditionally encoding values based on runtime information
    like firmware version or hardware revision, and for identifying which
    device or connection a request belongs to in telemetry handlers.

## Example

    write_func = fn object_type, starting_address, value_or_values ->
      custom_write_logic(…)
      :ok
    end

    iex> ModBoss.write(MyDevice.Schema, [foo: 75, bar: "ABC"], write_func)
    :ok

---

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