Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions lib/modboss.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ defmodule ModBoss do

alias ModBoss.Mapping

@typep mode :: :readable | :writable
@typep mode :: :readable | :writable | :any
@type register_type :: :holding_register | :input_register | :coil | :discrete_input
@type read_func :: (register_type(), starting_address :: integer(), count :: integer() ->
{:ok, any()} | {:error, any()})
@type write_func :: (register_type(), starting_address :: integer(), value_or_values :: any() ->
:ok | {:error, any()})
@type values_to_write :: [{atom(), any()}] | %{atom() => any()}
@type values :: [{atom(), any()}] | %{atom() => any()}

@doc false
def read_all(module, read_func, opts \\ []) do
Expand Down Expand Up @@ -102,6 +102,41 @@ defmodule ModBoss do
end)
end

@doc """
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.

## Example

iex> ModBoss.encode(MyDevice.Schema, foo: "Yay")
{:ok, %{{:holding_register, 15} => 22881, {:holding_register, 16} => 30976}}
"""
@spec encode(module(), values()) :: {:ok, map()} | {:error, any()}
def encode(module, values) when is_atom(module) do
with {:ok, mappings} <- get_mappings(:any, module, get_keys(values)),
mappings <- put_values(mappings, values),
{:ok, mappings} <- encode(mappings) do
{:ok, flatten_encoded_values(mappings)}
end
end

defp flatten_encoded_values(mappings) do
mappings
|> Enum.flat_map(fn %Mapping{} = mapping ->
mapping.encoded_value
|> List.wrap()
|> Enum.with_index(mapping.starting_address)
|> Enum.map(fn {value_for_register, address} ->
{{mapping.type, address}, value_for_register}
end)
end)
|> Enum.into(%{})
end

@doc """
Write to modbus using named mappings.

Expand Down Expand Up @@ -136,7 +171,7 @@ defmodule ModBoss do
iex> ModBoss.write(MyDevice.Schema, write_func, foo: 75, bar: "ABC")
:ok
"""
@spec write(module(), write_func(), values_to_write()) :: :ok | {:error, any()}
@spec write(module(), write_func(), values()) :: :ok | {:error, any()}
def write(module, write_func, values) when is_atom(module) and is_function(write_func) do
with {:ok, mappings} <- get_mappings(:writable, module, get_keys(values)),
mappings <- put_values(mappings, values),
Expand Down Expand Up @@ -175,7 +210,7 @@ defmodule ModBoss do
cond do
Enum.any?(unknown_names) ->
names = unknown_names |> Enum.map_join(", ", fn name -> inspect(name) end)
{:error, "Unknown register(s) #{names} for #{inspect(module)}."}
{:error, "Unknown mapping(s) #{names} for #{inspect(module)}."}

mode == :readable and Enum.any?(unreadable(mappings)) ->
names = unreadable(mappings) |> Enum.map_join(", ", fn %{name: name} -> inspect(name) end)
Expand Down
2 changes: 1 addition & 1 deletion lib/modboss/encoding.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule ModBoss.Encoding do
To make use of these functions, use the `:as` option in your `ModBoss.Schema` but leave off
the `encode_` or `decode_` prefix.

In other words, to use the built-in ASCII translation, specifiy `as: :ascii` in your schema.
For example, to use the built-in ASCII translation, specify `as: :ascii` in your schema.

### Note about that extra arg…

Expand Down
2 changes: 1 addition & 1 deletion lib/modboss/mapping.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule ModBoss.Mapping do
register_count: integer(),
as: atom() | {module(), atom()},
value: any(),
encoded_value: integer(),
encoded_value: integer() | [integer()],
mode: :r | :rw | :w
}

Expand Down
106 changes: 103 additions & 3 deletions test/modboss_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ defmodule ModBossTest do
test "returns an error if any mapping names are unrecognized" do
device = start_supervised!({Agent, fn -> @initial_state end})

assert {:error, "Unknown register(s) :foobar, :bazqux for ModBossTest.FakeSchema."} =
assert {:error, "Unknown mapping(s) :foobar, :bazqux for ModBossTest.FakeSchema."} =
ModBoss.read(FakeSchema, read_func(device), [:foobar, :bazqux])
end

Expand Down Expand Up @@ -440,7 +440,7 @@ defmodule ModBossTest do
end
end

describe "ModBoss.write/4" do
describe "ModBoss.write/3" do
test "writes registers referenced by human-readable names from map" do
device = start_supervised!({Agent, fn -> @initial_state end})
:ok = ModBoss.write(FakeSchema, write_func(device), %{baz: 1, corge: 1234})
Expand All @@ -456,7 +456,7 @@ defmodule ModBossTest do
test "returns an error if any mapping names are unrecognized" do
device = start_supervised!({Agent, fn -> @initial_state end})

assert {:error, "Unknown register(s) :foobar, :bazqux for ModBossTest.FakeSchema."} =
assert {:error, "Unknown mapping(s) :foobar, :bazqux for ModBossTest.FakeSchema."} =
ModBoss.write(FakeSchema, write_func(device), %{foobar: 1, bazqux: 2})
end

Expand Down Expand Up @@ -631,6 +631,106 @@ defmodule ModBossTest do
end
end

describe "ModBoss.encode/2" do
test "translates values from a Keyword List per the schema" do
schema = unique_module()

Code.compile_string("""
defmodule #{schema} do
use ModBoss.Schema
alias ModBoss.Encoding

modbus_schema do
holding_register 1, :foo, as: {Encoding, :boolean}
holding_register 2, :bar, as: {Encoding, :boolean}
holding_register 3..4, :baz, as: {Encoding, :ascii}
input_register 100, :qux
coil 101, :quux
discrete_input 102, :corge
end
end
""")

assert {:ok,
%{
{:holding_register, 1} => 1,
{:holding_register, 2} => 0,
{:holding_register, 3} => 22383,
{:holding_register, 4} => 30497,
{:input_register, 100} => 3,
{:coil, 101} => 2,
{:discrete_input, 102} => 1
}} =
ModBoss.encode(schema,
foo: true,
bar: false,
baz: "Wow!",
qux: 3,
quux: 2,
corge: 1
)
end

test "translates values from a map per the schema" do
schema = unique_module()

Code.compile_string("""
defmodule #{schema} do
use ModBoss.Schema
alias ModBoss.Encoding

modbus_schema do
holding_register 1, :foo, as: {Encoding, :boolean}
holding_register 2, :bar, as: {Encoding, :boolean}
holding_register 3..4, :baz, as: {Encoding, :ascii}
input_register 100, :qux
coil 101, :quux
discrete_input 102, :corge
end
end
""")

assert {:ok,
%{
{:holding_register, 1} => 1,
{:holding_register, 2} => 0,
{:holding_register, 3} => 22383,
{:holding_register, 4} => 30497,
{:input_register, 100} => 3,
{:coil, 101} => 2,
{:discrete_input, 102} => 1
}} =
ModBoss.encode(schema, %{
foo: true,
bar: false,
baz: "Wow!",
qux: 3,
quux: 2,
corge: 1
})
end

test "returns an error if any registers are unrecognized" do
schema = unique_module()

Code.compile_string("""
defmodule #{schema} do
use ModBoss.Schema

modbus_schema do
holding_register 1, :foo
holding_register 2, :bar
end
end
""")

assert {:error, message} = ModBoss.encode(schema, %{foo: 1, bar: 2, baz: 3})
assert String.match?(message, ~r/Unknown mapping/i)

assert {:ok, _encoded_values} = ModBoss.encode(schema, %{foo: 1, bar: 2})
end
end

defp set_registers(device, %{} = values) when is_pid(device) do
keys = Map.keys(values)

Expand Down