From 946545e9aa267957bfd97f9f1da14e7be5fdb5ff Mon Sep 17 00:00:00 2001 From: Ben Coppock Date: Tue, 17 Jun 2025 14:00:40 -0700 Subject: [PATCH 1/5] Fix documentation typo --- lib/modboss/encoding.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modboss/encoding.ex b/lib/modboss/encoding.ex index 274a9a2..0ead505 100644 --- a/lib/modboss/encoding.ex +++ b/lib/modboss/encoding.ex @@ -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… From e48268435671fb8175a9d1174851ec77f322bdbd Mon Sep 17 00:00:00 2001 From: Ben Coppock Date: Thu, 19 Jun 2025 22:40:29 -0700 Subject: [PATCH 2/5] Fix spec for encode_value --- lib/modboss/mapping.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modboss/mapping.ex b/lib/modboss/mapping.ex index df7314e..96327cd 100644 --- a/lib/modboss/mapping.ex +++ b/lib/modboss/mapping.ex @@ -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 } From f4bc58e5bef0617a131452469891583f905249e1 Mon Sep 17 00:00:00 2001 From: Ben Coppock Date: Thu, 19 Jun 2025 23:01:38 -0700 Subject: [PATCH 3/5] Fix arity in test description --- test/modboss_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/modboss_test.exs b/test/modboss_test.exs index 5622918..53fbfb5 100644 --- a/test/modboss_test.exs +++ b/test/modboss_test.exs @@ -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}) From d5963268848b21f5012f5cb0672466d43c0664c0 Mon Sep 17 00:00:00 2001 From: Ben Coppock Date: Fri, 20 Jun 2025 08:25:02 -0700 Subject: [PATCH 4/5] Add `encode/2` to encode values without writing --- lib/modboss.ex | 41 +++++++++++++++-- test/modboss_test.exs | 100 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/lib/modboss.ex b/lib/modboss.ex index 2abad30..6342916 100644 --- a/lib/modboss.ex +++ b/lib/modboss.ex @@ -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 @@ -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. @@ -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), diff --git a/test/modboss_test.exs b/test/modboss_test.exs index 53fbfb5..3479f62 100644 --- a/test/modboss_test.exs +++ b/test/modboss_test.exs @@ -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 register/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) From ab4ef884147aaf51445218fe7632f7db56ec84dd Mon Sep 17 00:00:00 2001 From: Ben Coppock Date: Fri, 20 Jun 2025 10:24:33 -0700 Subject: [PATCH 5/5] =?UTF-8?q?Use=20=E2=80=9Cmapping=E2=80=9D=20instead?= =?UTF-8?q?=20of=20=E2=80=9Cregister=E2=80=9D=20in=20error=20msg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modboss.ex | 2 +- test/modboss_test.exs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/modboss.ex b/lib/modboss.ex index 6342916..927fc61 100644 --- a/lib/modboss.ex +++ b/lib/modboss.ex @@ -210,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) diff --git a/test/modboss_test.exs b/test/modboss_test.exs index 3479f62..c870cac 100644 --- a/test/modboss_test.exs +++ b/test/modboss_test.exs @@ -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 @@ -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 @@ -725,7 +725,7 @@ defmodule ModBossTest do """) assert {:error, message} = ModBoss.encode(schema, %{foo: 1, bar: 2, baz: 3}) - assert String.match?(message, ~r/Unknown register/i) + assert String.match?(message, ~r/Unknown mapping/i) assert {:ok, _encoded_values} = ModBoss.encode(schema, %{foo: 1, bar: 2}) end