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

Macros for establishing Modbus schema.

The schema allows names to be assigned to individual modbus objects or groups
of contiguous modbus objects along with encoder/decoder functions. It also
allows objects to be flagged as readable and/or writable.

## Naming a mapping

You create a ModBoss mapping with this format:

    holding_register 17, :outdoor_temp, as: {ModBoss.Encoding, :signed_int}

This establishes address 17 as a holding register with the name
`:outdoor_temp`. The raw value from the register will be passed to
`ModBoss.Encoding.decode_signed_int/1` when decoding.

Similarly, to set aside a **group of** registers to hold a single logical
value, it would look like:

    holding_register 20..23, :model_name, as: {ModBoss.Encoding, :ascii}

This establishes addresses 20–23 as holding registers with the name
`:model_name`. The raw values from these registers will be passed (as a list)
to `ModBoss.Encoding.decode_ascii/1` when decoding.

## Mode

All ModBoss mappings are read-only by default. Use `mode: :rw` to allow both
reads & writes. Or use `mode: :w` to configure a mapping as write-only.

## Automatic encoding/decoding

Depending on whether a mapping is flagged as readable/writable, it is expected
that you will provide functions with `encode_` or `decode_` prepended to the
value provided by the `:as` option.

For example, if you specify `as: :on_off` for a writable mapping, ModBoss
will expect that the schema module defines either:

* `encode_on_off/1` — accepts the value to encode
* `encode_on_off/2` — accepts the value to encode and a
  `ModBoss.Encoding.Metadata` struct

Use the 2-arity version when you need access to metadata (e.g. address count,
mapping name, or user-provided context). Both must return either
`{:ok, encoded_value}` or `{:error, message}`.

The same applies to decode functions: define either `decode_on_off/1` (accepts
the encoded value) or `decode_on_off/2` (accepts the encoded value and
metadata).

If the function to be used lives outside of the current module, a tuple
including the module name can be passed. For example, you can use built-in
encoders from `ModBoss.Encoding`.

> #### output of `encode_*` {: .info}
>
> Your encode function may need to encode for **one or multiple** objects,
> depending on the mapping. You are free to return either a single value or
> a list of values—the important thing is that the number of values returned
> needs to match the number of objects from your mapping. If it doesn't,
> ModBoss will return an error when encoding.
>
> For example, if encoding "ABC!" as ascii into a mapping with 3 registers,
> these characters would technically only _require_ 2 registers (one 16-bit
> register for every 2 characters). However, your encoding should return a
> list of length equaling what you've assigned to the mapping in your schema—
> i.e. in this example, a list of length 3.

> #### input to `decode_*` {: .info}
>
> When decoding a mapping involving a single address, the decode function will
> be passed the single value from that address/object as provided by your read
> function.
>
> When decoding a mapping involving multiple addresses
> (e.g. in `ModBoss.Encoding.decode_ascii/1`), the decode function will be
> passed a **List** of values.

## Example

    defmodule MyDevice.Schema do
      use ModBoss.Schema

      schema do
        holding_register 1..5, :model, as: {ModBoss.Encoding, :ascii}
        holding_register 6, :outdoor_temp, as: {ModBoss.Encoding, :signed_int}
        holding_register 7, :indoor_temp, as: {ModBoss.Encoding, :unsigned_int}

        input_register 200, :foo, as: {ModBoss.Encoding, :unsigned_int}
        coil 300, :bar, as: :on_off, mode: :rw
        holding_register 301, :setpoint, as: :scaled, mode: :w
        discrete_input 400, :baz, as: {ModBoss.Encoding, :boolean}
      end

      # 1-arity: no metadata needed
      def encode_on_off(:on), do: {:ok, 1}
      def encode_on_off(:off), do: {:ok, 0}

      def decode_on_off(1), do: {:ok, :on}
      def decode_on_off(0), do: {:ok, :off}

      # 2-arity: uses metadata.context for runtime behavior
      def encode_scaled(value, metadata) do
        case metadata.context do
          %{unit: :fahrenheit} -> {:ok, round((value - 32) * 5 / 9)}
          _ -> {:ok, value}
        end
      end
    end

# `coil`
*macro* 

Adds a coil to a schema.

## Opts
* `:mode` — Makes the mapping readable/writable — can be one of `[:r, :rw, :w]` (default: `:r`)
* `:as` — Determines which encoding/decoding functions to use when writing/reading values.
  See explanation of [automatic encoding/decoding](ModBoss.Schema.html#module-automatic-encoding-decoding).
* `:gap_safe` — Whether this mapping's addresses are safe to read incidentally
  when bridging a gap between other requested mappings (default: `true` for readable
  mappings, `false` for write-only). You should set this to `false` for any coil
  that triggers side effects when read (e.g. clear-on-read coils). See the `:max_gap`
  option in `ModBoss.read/4` for details on gap tolerance.

# `discrete_input`
*macro* 

Adds a read-only discrete input to a schema.

## Opts
* `:as` — Determines which decoding functions to use when reading values.
  See explanation of [automatic encoding/decoding](ModBoss.Schema.html#module-automatic-encoding-decoding).
* `:gap_safe` — Whether this mapping's addresses are safe to read incidentally
  when bridging a gap between other requested mappings (default: `true`). You should
  set this to `false` for any input that triggers side effects when read (e.g.
  clear-on-read inputs). See the `:max_gap` option in `ModBoss.read/4` for details
  on gap tolerance.

# `holding_register`
*macro* 

Adds a holding register to a schema.

## Opts
* `:mode` — Makes the mapping readable/writable — can be one of `[:r, :rw, :w]` (default: `:r`)
* `:as` — Determines which encoding/decoding functions to use when writing/reading values.
  See explanation of [automatic encoding/decoding](ModBoss.Schema.html#module-automatic-encoding-decoding).
* `:gap_safe` — Whether this mapping's addresses are safe to read incidentally
  when bridging a gap between other requested mappings (default: `true` for readable
  mappings, `false` for write-only). You should set this to `false` for any register
  that triggers side effects when read (e.g. clear-on-read registers). See the
  `:max_gap` option in `ModBoss.read/4` for details on gap tolerance.

# `input_register`
*macro* 

Adds a read-only input register to a schema.

## Opts
* `:as` — Determines which decoding functions to use when reading values.
  See explanation of [automatic encoding/decoding](ModBoss.Schema.html#module-automatic-encoding-decoding).
* `:gap_safe` — Whether this mapping's addresses are safe to read incidentally
  when bridging a gap between other requested mappings (default: `true`). You should
  set this to `false` for any register that triggers side effects when read (e.g.
  clear-on-read registers). See the `:max_gap` option in `ModBoss.read/4` for details
  on gap tolerance.

# `schema`
*macro* 

Establishes a Modbus schema in the current module.

---

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