diff --git a/README.md b/README.md index 6f5946d..59a9ddc 100644 --- a/README.md +++ b/README.md @@ -108,19 +108,19 @@ From here you can read and write by name… #### Requesting a single value returns just one value: ```elixir -iex> ModBoss.read(MyDevice.Schema, read_func, :outdoor_temp) +iex> ModBoss.read(MyDevice.Schema, :outdoor_temp, read_func) {:ok, 72} ``` #### Requesting multiple values returns a map: ```elixir -iex> ModBoss.read(MyDevice.Schema, read_func, [:outdoor_temp, :model_name, :version]) +iex> ModBoss.read(MyDevice.Schema, [:outdoor_temp, :model_name, :version], read_func) {:ok, %{outdoor_temp: 72, model_name: "AI4000", version: "0.1"}} ``` -#### Writing is performed via a keyword list or map: +#### Writing is performed via map or keyword list: ```elixir -iex> ModBoss.write(MyDevice.Schema, write_func, version: "0.2") +iex> ModBoss.write(MyDevice.Schema, %{version: "0.2"}, write_func) :ok ``` diff --git a/lib/modboss.ex b/lib/modboss.ex index 2743078..9abbc70 100644 --- a/lib/modboss.ex +++ b/lib/modboss.ex @@ -42,7 +42,7 @@ defmodule ModBoss do 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 singule result + 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. @@ -112,32 +112,32 @@ defmodule ModBoss do end # Read one mapping - ModBoss.read(SchemaModule, read_func, :foo) + ModBoss.read(SchemaModule, :foo, read_func) {:ok, 75} # Read multiple mappings - ModBoss.read(SchemaModule, read_func, [:foo, :bar, :baz]) + ModBoss.read(SchemaModule, [:foo, :bar, :baz], read_func) {:ok, %{foo: 75, bar: "ABC", baz: true}} # Read *all* readable mappings - ModBoss.read(SchemaModule, read_func, :all) + 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, read_func, [:foo, :bar], max_gap: 10) + 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, read_func, [:foo, :bar], max_gap: %{holding_registers: 10}) + ModBoss.read(SchemaModule, [:foo, :bar], read_func, max_gap: %{holding_registers: 10}) {:ok, %{foo: 75, bar: "ABC"}} # Get "raw" Modbus values (as returned by `read_func`) - ModBoss.read(SchemaModule, read_func, decode: false) - {:ok, bar: [16706, 17152]} + ModBoss.read(SchemaModule, [:foo, :bar], read_func, decode: false) + {:ok, %{foo: [75], bar: [16706, 17152]}} """ - @spec read(module(), read_func(), atom() | [atom()], keyword()) :: + @spec read(module(), atom() | [atom()], read_func(), keyword()) :: {:ok, any()} | {:error, any()} - def read(module, read_func, name_or_names, opts \\ []) do + def read(module, name_or_names, read_func, opts \\ []) do {names, opts} = evaluate_read_opts(module, name_or_names, opts) with {:ok, mappings} <- get_mappings(:readable, module, names) do @@ -461,15 +461,15 @@ defmodule ModBoss do ## Example write_func = fn object_type, starting_address, value_or_values -> - result = custom_write_logic(…) - {:ok, result} + custom_write_logic(…) + :ok end - iex> ModBoss.write(MyDevice.Schema, write_func, foo: 75, bar: "ABC") + iex> ModBoss.write(MyDevice.Schema, [foo: 75, bar: "ABC"], write_func) :ok """ - @spec write(module(), write_func(), values(), keyword()) :: :ok | {:error, any()} - def write(module, write_func, values, opts \\ []) + @spec write(module(), values(), write_func(), keyword()) :: :ok | {:error, any()} + def write(module, values, write_func, opts \\ []) when is_atom(module) and is_function(write_func) do names = get_keys(values) opts = evaluate_write_opts(opts) diff --git a/test/modboss/telemetry_test.exs b/test/modboss/telemetry_test.exs index c8b452c..392b4ee 100644 --- a/test/modboss/telemetry_test.exs +++ b/test/modboss/telemetry_test.exs @@ -52,7 +52,7 @@ defmodule ModBoss.TelemetryTest do test "emits :read start and stop spans for a successful read", %{device: device} do set_objects(device, %{{:holding_register, 1} => 42}) - {:ok, 42} = ModBoss.read(TestSchema, read_func(device), :foo) + {:ok, 42} = ModBoss.read(TestSchema, :foo, read_func(device)) # Per-operation start assert_receive {:telemetry, [:modboss, :read, :start], start_measurements, start_metadata} @@ -78,7 +78,7 @@ defmodule ModBoss.TelemetryTest do test "emits :read_callback start and stop spans for each read callback", %{device: device} do set_objects(device, %{{:holding_register, 1} => 42}) - {:ok, 42} = ModBoss.read(TestSchema, read_func(device), :foo) + {:ok, 42} = ModBoss.read(TestSchema, :foo, read_func(device)) # Per-request start assert_receive {:telemetry, [:modboss, :read_callback, :start], start_measurements, @@ -120,7 +120,7 @@ defmodule ModBoss.TelemetryTest do }) {:ok, %{foo: 10, bar: 20, grault: 1}} = - ModBoss.read(TestSchema, read_func(device), [:foo, :bar, :grault]) + ModBoss.read(TestSchema, [:foo, :bar, :grault], read_func(device)) # Callback requests assert_receive {:telemetry, [:modboss, :read_callback, :stop], _m1, meta1} @@ -151,7 +151,7 @@ defmodule ModBoss.TelemetryTest do {:holding_register, 12} => 3 }) - {:ok, [1, 2, 3]} = ModBoss.read(TestSchema, read_func(device), :qux) + {:ok, [1, 2, 3]} = ModBoss.read(TestSchema, :qux, read_func(device)) assert_receive {:telemetry, [:modboss, :read, :stop], read_measurements, _} assert read_measurements.objects_requested == 3 @@ -178,7 +178,7 @@ defmodule ModBoss.TelemetryTest do }) {:ok, %{alpha: 10, echo: 50}} = - ModBoss.read(GapSchema, read_func(device), [:alpha, :echo], max_gap: 10) + ModBoss.read(GapSchema, [:alpha, :echo], read_func(device), max_gap: 10) # Per-operation: 1 request, 2 objects requested, 5 addresses read, 3 gap addresses assert_receive {:telemetry, [:modboss, :read, :stop], measurements, _} @@ -209,7 +209,7 @@ defmodule ModBoss.TelemetryTest do }) {:ok, %{alpha: 10, charlie: 30, echo: 50}} = - ModBoss.read(GapSchema, read_func(device), [:alpha, :charlie, :echo], max_gap: 10) + ModBoss.read(GapSchema, [:alpha, :charlie, :echo], read_func(device), max_gap: 10) assert_receive {:telemetry, [:modboss, :read, :stop], measurements, _} assert measurements.modbus_requests == 1 @@ -232,7 +232,7 @@ defmodule ModBoss.TelemetryTest do {:holding_register, 2} => 20 }) - {:ok, %{foo: 10, bar: 20}} = ModBoss.read(TestSchema, read_func(device), [:foo, :bar]) + {:ok, %{foo: 10, bar: 20}} = ModBoss.read(TestSchema, [:foo, :bar], read_func(device)) assert_receive {:telemetry, [:modboss, :read, :stop], measurements, _} assert measurements.gap_addresses_read == 0 @@ -243,7 +243,7 @@ defmodule ModBoss.TelemetryTest do test "includes names as a list even for singular reads", %{device: device} do set_objects(device, %{{:holding_register, 1} => 42}) - {:ok, 42} = ModBoss.read(TestSchema, read_func(device), :foo) + {:ok, 42} = ModBoss.read(TestSchema, :foo, read_func(device)) assert_receive {:telemetry, [:modboss, :read, :start], _, %{names: names}} assert names == [:foo] @@ -255,7 +255,7 @@ defmodule ModBoss.TelemetryTest do {:holding_register, 2} => 20 }) - {:ok, %{foo: 10, bar: 20}} = ModBoss.read(TestSchema, read_func(device), [:foo, :bar]) + {:ok, %{foo: 10, bar: 20}} = ModBoss.read(TestSchema, [:foo, :bar], read_func(device)) assert_receive {:telemetry, [:modboss, :read, :start], _, %{names: names}} assert Enum.sort(names) == [:bar, :foo] @@ -263,7 +263,7 @@ defmodule ModBoss.TelemetryTest do test "emits stop with error result when read_func returns an error", %{device: _device} do failing_read_func = fn _type, _addr, _count -> {:error, "connection refused"} end - {:error, "connection refused"} = ModBoss.read(TestSchema, failing_read_func, :foo) + {:error, "connection refused"} = ModBoss.read(TestSchema, :foo, failing_read_func) assert_receive {:telemetry, [:modboss, :read, :start], _, _} @@ -326,8 +326,8 @@ defmodule ModBoss.TelemetryTest do {:error, "timeout"} = ModBoss.read( schema, - failing_on_second, [:alpha, :bravo, :charlie, :delta, :echo, :golf], + failing_on_second, max_gap: 5 ) @@ -364,7 +364,7 @@ defmodule ModBoss.TelemetryTest do # Each callback will be attempted twice for a total of 4 attempts in the end… {:ok, %{foo: 10, grault: 1}} = - ModBoss.read(TestSchema, flaky_read, [:foo, :grault], max_attempts: 2) + ModBoss.read(TestSchema, [:foo, :grault], flaky_read, max_attempts: 2) # Batch 1: attempt 1 fails, attempt 2 succeeds assert_receive {:telemetry, [:modboss, :read_callback, :stop], _, cb1_attempt1} @@ -397,7 +397,7 @@ defmodule ModBoss.TelemetryTest do set_objects(device, %{{:holding_register, 1} => 42}) raising_read = flakify(read_func(device), fn -> raise "raised!" end, flakes: 1) - {:ok, 42} = ModBoss.read(TestSchema, raising_read, :foo, max_attempts: 2) + {:ok, 42} = ModBoss.read(TestSchema, :foo, raising_read, max_attempts: 2) # Attempt 1: raise, callback exception span assert_receive {:telemetry, [:modboss, :read_callback, :exception], _, meta1} @@ -424,7 +424,7 @@ defmodule ModBoss.TelemetryTest do raise "boom!" end - {:error, %RuntimeError{message: "boom!"}} = ModBoss.read(TestSchema, boom_func, :foo) + {:error, %RuntimeError{message: "boom!"}} = ModBoss.read(TestSchema, :foo, boom_func) # Callback-level: exception event with full metadata assert_receive {:telemetry, [:modboss, :read_callback, :start], start_measurements, @@ -471,7 +471,7 @@ defmodule ModBoss.TelemetryTest do end test "does not emit events for validation errors (e.g. unknown names)", %{device: device} do - {:error, _} = ModBoss.read(TestSchema, read_func(device), :nonexistent) + {:error, _} = ModBoss.read(TestSchema, :nonexistent, read_func(device)) refute_receive {:telemetry, [:modboss, :read, :start], _, _} refute_receive {:telemetry, [:modboss, :read_callback, :start], _, _} @@ -482,7 +482,7 @@ defmodule ModBoss.TelemetryTest do } do set_objects(device, %{{:holding_register, 1} => 42}) - {:ok, 42} = ModBoss.read(TestSchema, read_func(device), :foo, telemetry_label: :foo_label) + {:ok, 42} = ModBoss.read(TestSchema, :foo, read_func(device), telemetry_label: :foo_label) assert_receive {:telemetry, [:modboss, :read, :start], _, start_metadata} assert start_metadata.label == :foo_label @@ -501,7 +501,7 @@ defmodule ModBoss.TelemetryTest do boom_func = fn _type, _addr, _count -> raise "boom!" end {:error, %RuntimeError{}} = - ModBoss.read(TestSchema, boom_func, :foo, telemetry_label: :my_device) + ModBoss.read(TestSchema, :foo, boom_func, telemetry_label: :my_device) assert_receive {:telemetry, [:modboss, :read, :stop], _, meta} assert meta.label == :my_device @@ -515,7 +515,7 @@ defmodule ModBoss.TelemetryTest do } do set_objects(device, %{{:holding_register, 1} => 42}) - {:ok, 42} = ModBoss.read(TestSchema, read_func(device), :foo) + {:ok, 42} = ModBoss.read(TestSchema, :foo, read_func(device)) assert_receive {:telemetry, [:modboss, :read, :start], _, metadata} refute Map.has_key?(metadata, :label) @@ -546,7 +546,7 @@ defmodule ModBoss.TelemetryTest do end test "emits :write start and stop spans for a successful write", %{device: device} do - :ok = ModBoss.write(TestSchema, write_func(device), baz: 99) + :ok = ModBoss.write(TestSchema, [baz: 99], write_func(device)) # Per-operation start assert_receive {:telemetry, [:modboss, :write, :start], start_measurements, start_metadata} @@ -567,7 +567,7 @@ defmodule ModBoss.TelemetryTest do end test "emits :write_callback start and stop spans for each write callback", %{device: device} do - :ok = ModBoss.write(TestSchema, write_func(device), baz: 99) + :ok = ModBoss.write(TestSchema, [baz: 99], write_func(device)) # Per-request start assert_receive {:telemetry, [:modboss, :write_callback, :start], start_measurements, @@ -595,7 +595,7 @@ defmodule ModBoss.TelemetryTest do end test "emits aggregated data for the :write event", %{device: device} do - :ok = ModBoss.write(TestSchema, write_func(device), baz: 1, blah: 1, grault: 1) + :ok = ModBoss.write(TestSchema, [baz: 1, blah: 1, grault: 1], write_func(device)) # Callback requests assert_receive {:telemetry, [:modboss, :write_callback, :stop], _, meta1} @@ -623,7 +623,7 @@ defmodule ModBoss.TelemetryTest do end test "emits correct counts for multi-address writes", %{device: device} do - :ok = ModBoss.write(TestSchema, write_func(device), qux: [1, 2, 3]) + :ok = ModBoss.write(TestSchema, [qux: [1, 2, 3]], write_func(device)) assert_receive {:telemetry, [:modboss, :write, :stop], write_measurements, _} assert write_measurements.objects_requested == 3 @@ -640,7 +640,7 @@ defmodule ModBoss.TelemetryTest do {:error, "device busy"} end - {:error, "device busy"} = ModBoss.write(TestSchema, failing_write_func, baz: 1) + {:error, "device busy"} = ModBoss.write(TestSchema, [baz: 1], failing_write_func) assert_receive {:telemetry, [:modboss, :write, :start], _, _} @@ -684,7 +684,7 @@ defmodule ModBoss.TelemetryTest do end {:error, "timeout"} = - ModBoss.write(schema, failing_on_second, first: 1, second: 2, third: 3) + ModBoss.write(schema, [first: 1, second: 2, third: 3], failing_on_second) assert_receive {:telemetry, [:modboss, :write, :stop], measurements, metadata} assert metadata.result == {:error, "timeout"} @@ -698,7 +698,7 @@ defmodule ModBoss.TelemetryTest do test "retries emit per-attempt callback spans and total_attempts", %{device: device} do flaky_write = flakify(write_func(device), fn -> {:error, "flaky"} end, flakes: 1) - :ok = ModBoss.write(TestSchema, flaky_write, [baz: 99], max_attempts: 3) + :ok = ModBoss.write(TestSchema, [baz: 99], flaky_write, max_attempts: 3) assert_receive {:telemetry, [:modboss, :write_callback, :stop], _, attempt1} assert attempt1.attempt == 1 @@ -722,7 +722,7 @@ defmodule ModBoss.TelemetryTest do end {:error, %RuntimeError{message: "kaboom!"}} = - ModBoss.write(TestSchema, kaboom_func, baz: 1) + ModBoss.write(TestSchema, [baz: 1], kaboom_func) # Callback-level: exception event with full metadata assert_receive {:telemetry, [:modboss, :write_callback, :start], start_measurements, @@ -766,7 +766,7 @@ defmodule ModBoss.TelemetryTest do end test "does not emit events for validation errors (e.g. unknown names)", %{device: device} do - {:error, _} = ModBoss.write(TestSchema, write_func(device), nonexistent: 1) + {:error, _} = ModBoss.write(TestSchema, [nonexistent: 1], write_func(device)) refute_receive {:telemetry, [:modboss, :write, :start], _, _} refute_receive {:telemetry, [:modboss, :write_callback, :start], _, _} @@ -776,7 +776,7 @@ defmodule ModBoss.TelemetryTest do device: device } do :ok = - ModBoss.write(TestSchema, write_func(device), [baz: 99], + ModBoss.write(TestSchema, [baz: 99], write_func(device), telemetry_label: %{port: :rs485, address: 12} ) @@ -796,7 +796,7 @@ defmodule ModBoss.TelemetryTest do test "retries through exceptions and succeeds", %{device: device} do raising_write = flakify(write_func(device), fn -> raise "raised!" end, flakes: 1) - :ok = ModBoss.write(TestSchema, raising_write, [baz: 99], max_attempts: 2) + :ok = ModBoss.write(TestSchema, [baz: 99], raising_write, max_attempts: 2) # Attempt 1: raise, callback exception span assert_receive {:telemetry, [:modboss, :write_callback, :exception], _, meta1} @@ -820,7 +820,7 @@ defmodule ModBoss.TelemetryTest do kaboom_func = fn _type, _addr, _values -> raise "kaboom!" end {:error, %RuntimeError{}} = - ModBoss.write(TestSchema, kaboom_func, [baz: 1], telemetry_label: :my_device) + ModBoss.write(TestSchema, [baz: 1], kaboom_func, telemetry_label: :my_device) assert_receive {:telemetry, [:modboss, :write, :stop], _, meta} assert meta.label == :my_device @@ -832,7 +832,7 @@ defmodule ModBoss.TelemetryTest do test "does not include label key in metadata when telemetry_label is not provided", %{ device: device } do - :ok = ModBoss.write(TestSchema, write_func(device), baz: 99) + :ok = ModBoss.write(TestSchema, [baz: 99], write_func(device)) assert_receive {:telemetry, [:modboss, :write, :start], _, metadata} refute Map.has_key?(metadata, :label) diff --git a/test/modboss_test.exs b/test/modboss_test.exs index 57015e7..ad781f3 100644 --- a/test/modboss_test.exs +++ b/test/modboss_test.exs @@ -54,7 +54,7 @@ defmodule ModBossTest do # Even with a large gap tolerance, unmapped address 2 must prevent batching {:ok, %{foo: 11, baz: 33}} = - ModBoss.read(schema, read_func(device), [:foo, :baz], max_gap: 10) + ModBoss.read(schema, [:foo, :baz], read_func(device), max_gap: 10) assert 2 = get_read_count(device) end @@ -79,7 +79,7 @@ defmodule ModBossTest do # Even with a large gap tolerance, we'll need two reads since address 2 is unreadable {:ok, %{foo: 11, baz: 33}} = - ModBoss.read(schema, read_func(device), [:foo, :baz], max_gap: 10) + ModBoss.read(schema, [:foo, :baz], read_func(device), max_gap: 10) assert 2 = get_read_count(device) end @@ -87,20 +87,20 @@ defmodule ModBossTest do test "reads an individual mapping by name, returning a single result" do device = start_supervised!({Agent, fn -> @initial_state end}) encode_and_set(device, FakeSchema, foo: 123) - {:ok, 123} = ModBoss.read(FakeSchema, read_func(device), :foo) + {:ok, 123} = ModBoss.read(FakeSchema, :foo, read_func(device)) end test "reads values for mappings that cover multiple address" do device = start_supervised!({Agent, fn -> @initial_state end}) encode_and_set(device, FakeSchema, qux: [10, 11, 12]) - {:ok, [10, 11, 12]} = ModBoss.read(FakeSchema, read_func(device), :qux) + {:ok, [10, 11, 12]} = ModBoss.read(FakeSchema, :qux, read_func(device)) end test "reads multiple (and non-contiguous) mappings by name, returning a map of requested values" do device = start_supervised!({Agent, fn -> @initial_state end}) encode_and_set(device, FakeSchema, foo: :a, bar: :b, baz: :c, qux: [:x, :y, :z]) - assert {:ok, result} = ModBoss.read(FakeSchema, read_func(device), [:foo, :qux]) + assert {:ok, result} = ModBoss.read(FakeSchema, [:foo, :qux], read_func(device)) assert %{foo: :a, qux: [:x, :y, :z]} == result end @@ -108,14 +108,14 @@ defmodule ModBossTest do device = start_supervised!({Agent, fn -> @initial_state end}) assert {:error, "Unknown mapping(s) :foobar, :bazqux for ModBossTest.FakeSchema."} = - ModBoss.read(FakeSchema, read_func(device), [:foobar, :bazqux]) + ModBoss.read(FakeSchema, [:foobar, :bazqux], read_func(device)) end test "refuses to read unless all mappings are declared readable" do device = start_supervised!({Agent, fn -> @initial_state end}) assert {:error, "ModBoss Mapping(s) :baz in ModBossTest.FakeSchema are not readable."} = - ModBoss.read(FakeSchema, read_func(device), [:bar, :baz]) + ModBoss.read(FakeSchema, [:bar, :baz], read_func(device)) end test "batches contiguous reads for each type up to the Modbus protocol's maximum" do @@ -164,37 +164,37 @@ defmodule ModBossTest do single = [:holding_1, :holding_125] double = [:holding_1, :holding_125, :holding_126] - assert {:ok, %{}} = ModBoss.read(schema, read_func(device), single) + assert {:ok, %{}} = ModBoss.read(schema, single, read_func(device)) assert 1 = get_read_count(device) - assert {:ok, %{}} = ModBoss.read(schema, read_func(device), double) + assert {:ok, %{}} = ModBoss.read(schema, double, read_func(device)) assert 2 = get_read_count(device) single = [:input_201, :input_325] double = [:input_201, :input_325, :input_326] - assert {:ok, %{}} = ModBoss.read(schema, read_func(device), single) + assert {:ok, %{}} = ModBoss.read(schema, single, read_func(device)) assert 1 = get_read_count(device) - assert {:ok, %{}} = ModBoss.read(schema, read_func(device), double) + assert {:ok, %{}} = ModBoss.read(schema, double, read_func(device)) assert 2 = get_read_count(device) single = [:coil_2001, :coil_4000] double = [:coil_2001, :coil_4000, :coil_4001] - assert {:ok, %{}} = ModBoss.read(schema, read_func(device), single) + assert {:ok, %{}} = ModBoss.read(schema, single, read_func(device)) assert 1 = get_read_count(device) - assert {:ok, %{}} = ModBoss.read(schema, read_func(device), double) + assert {:ok, %{}} = ModBoss.read(schema, double, read_func(device)) assert 2 = get_read_count(device) single = [:discrete_input_5001, :discrete_input_7000] double = [:discrete_input_5001, :discrete_input_7000, :discrete_input_7001] - assert {:ok, %{}} = ModBoss.read(schema, read_func(device), single) + assert {:ok, %{}} = ModBoss.read(schema, single, read_func(device)) assert 1 = get_read_count(device) - assert {:ok, %{}} = ModBoss.read(schema, read_func(device), double) + assert {:ok, %{}} = ModBoss.read(schema, double, read_func(device)) assert 2 = get_read_count(device) end @@ -251,44 +251,44 @@ defmodule ModBossTest do holding_registers = [:holding_foo, :holding_bar, :holding_baz, :holding_qux] single_read = Enum.take(holding_registers, max_holding_register_reads) - assert {:ok, _} = ModBoss.read(schema, read_func(device), single_read) + assert {:ok, _} = ModBoss.read(schema, single_read, read_func(device)) assert 1 = get_read_count(device) double_read = Enum.take(holding_registers, max_holding_register_reads + 1) - assert {:ok, %{}} = ModBoss.read(schema, read_func(device), double_read) + assert {:ok, %{}} = ModBoss.read(schema, double_read, read_func(device)) assert 2 = get_read_count(device) # Input registers input_registers = [:input_foo, :input_bar, :input_baz, :input_qux] single_read = Enum.take(input_registers, max_input_register_reads) - assert {:ok, _} = ModBoss.read(schema, read_func(device), single_read) + assert {:ok, _} = ModBoss.read(schema, single_read, read_func(device)) assert 1 = get_read_count(device) double_read = Enum.take(input_registers, max_input_register_reads + 1) - assert {:ok, %{}} = ModBoss.read(schema, read_func(device), double_read) + assert {:ok, %{}} = ModBoss.read(schema, double_read, read_func(device)) assert 2 = get_read_count(device) # Coils coils = [:coil_foo, :coil_bar, :coil_baz, :coil_qux] single_read = Enum.take(coils, max_coil_reads) - assert {:ok, _} = ModBoss.read(schema, read_func(device), single_read) + assert {:ok, _} = ModBoss.read(schema, single_read, read_func(device)) assert 1 = get_read_count(device) double_read = Enum.take(coils, max_coil_reads + 1) - assert {:ok, %{}} = ModBoss.read(schema, read_func(device), double_read) + assert {:ok, %{}} = ModBoss.read(schema, double_read, read_func(device)) assert 2 = get_read_count(device) # Discrete Inputs discrete_inputs = [:discrete_foo, :discrete_bar, :discrete_baz, :discrete_qux] single_read = Enum.take(discrete_inputs, max_discrete_input_reads) - assert {:ok, _} = ModBoss.read(schema, read_func(device), single_read) + assert {:ok, _} = ModBoss.read(schema, single_read, read_func(device)) assert 1 = get_read_count(device) double_read = Enum.take(discrete_inputs, max_discrete_input_reads + 1) - assert {:ok, %{}} = ModBoss.read(schema, read_func(device), double_read) + assert {:ok, %{}} = ModBoss.read(schema, double_read, read_func(device)) assert 2 = get_read_count(device) end @@ -326,7 +326,7 @@ defmodule ModBossTest do names = [:holding_1, :coil_1, :coil_2, :input_1, :discrete_1] {:ok, %{holding_1: [1, 2], coil_1: 101, coil_2: 102, input_1: 201, discrete_1: 301}} = - ModBoss.read(schema, read_func(device), names) + ModBoss.read(schema, names, read_func(device)) assert 4 == get_read_count(device) end @@ -338,7 +338,7 @@ defmodule ModBossTest do assert_raise RuntimeError, "Attempted to read 3 values starting from address 10 but received 0 values.", fn -> - ModBoss.read(FakeSchema, read_func(device), [:foo, :qux]) + ModBoss.read(FakeSchema, [:foo, :qux], read_func(device)) end end @@ -375,7 +375,7 @@ defmodule ModBossTest do }) assert {:ok, %{yep: true, nope: false, text: "Oh wow"}} = - ModBoss.read(schema, read_func(device), [:yep, :nope, :text]) + ModBoss.read(schema, [:yep, :nope, :text], read_func(device)) end test "returns an error if decoding fails" do @@ -406,7 +406,7 @@ defmodule ModBossTest do }) message = "Failed to decode :nope. Not sure what to do with 33." - assert {:error, ^message} = ModBoss.read(schema, read_func(device), [:yep, :nope]) + assert {:error, ^message} = ModBoss.read(schema, [:yep, :nope], read_func(device)) end test "allows reading of 'raw' values" do @@ -438,10 +438,10 @@ defmodule ModBossTest do }) assert {:ok, %{yep: true, nope: false, text: "Hello"}} = - ModBoss.read(schema, read_func(device), [:yep, :nope, :text]) + ModBoss.read(schema, [:yep, :nope, :text], read_func(device)) assert {:ok, %{yep: 1, nope: 0, text: [18533, 27756, 28416]}} = - ModBoss.read(schema, read_func(device), [:yep, :nope, :text], decode: false) + ModBoss.read(schema, [:yep, :nope, :text], read_func(device), decode: false) end test "bypasses (potentially-buggy) decode logic when asked to return raw values" do @@ -469,7 +469,7 @@ defmodule ModBossTest do }) assert {:ok, %{yep: 1, nope: 0}} = - ModBoss.read(schema, read_func(device), [:yep, :nope], decode: false) + ModBoss.read(schema, [:yep, :nope], read_func(device), decode: false) end test "fetches all readable mappings if told to read `:all`" do @@ -498,7 +498,7 @@ defmodule ModBossTest do {:discrete_input, 500} => 1 }) - assert {:ok, result} = ModBoss.read(schema, read_func(device), :all) + assert {:ok, result} = ModBoss.read(schema, :all, read_func(device)) assert %{ foo: [10, 20], @@ -529,11 +529,11 @@ defmodule ModBossTest do set_objects(device, values) # Should make 2 separate requests since they're not contiguous - {:ok, _} = ModBoss.read(schema, read_func(device), [:group_1, :group_2]) + {:ok, _} = ModBoss.read(schema, [:group_1, :group_2], read_func(device)) assert 2 = get_read_count(device) # Should make 1 request if we're okay reading across the gap - {:ok, _} = ModBoss.read(schema, read_func(device), [:group_1, :group_2], max_gap: 2) + {:ok, _} = ModBoss.read(schema, [:group_1, :group_2], read_func(device), max_gap: 2) assert 1 = get_read_count(device) end @@ -563,7 +563,7 @@ defmodule ModBossTest do set_objects(device, values) {:ok, result} = - ModBoss.read(schema, read_func(device), [:first_group, :second_group, :third_group], + ModBoss.read(schema, [:first_group, :second_group, :third_group], read_func(device), max_gap: 10 ) @@ -603,7 +603,7 @@ defmodule ModBossTest do set_objects(device, values) {:ok, result} = - ModBoss.read(schema, read_func(device), [:first_group, :second_group, :third_group], + ModBoss.read(schema, [:first_group, :second_group, :third_group], read_func(device), max_gap: [holding_registers: 10] ) @@ -640,7 +640,7 @@ defmodule ModBossTest do set_objects(device, values) {:ok, result} = - ModBoss.read(schema, read_func(device), [:first_group, :second_group, :third_group], + ModBoss.read(schema, [:first_group, :second_group, :third_group], read_func(device), max_gap: %{holding_registers: 10} ) @@ -695,7 +695,6 @@ defmodule ModBossTest do {:ok, result} = ModBoss.read( schema, - read_func(device), [ :first_group, :second_group, @@ -706,6 +705,7 @@ defmodule ModBossTest do :seventh_group, :eighth_group ], + read_func(device), max_gap: 2 ) @@ -727,7 +727,6 @@ defmodule ModBossTest do {:ok, ^result} = ModBoss.read( schema, - read_func(device), [ :first_group, :second_group, @@ -738,6 +737,7 @@ defmodule ModBossTest do :seventh_group, :eighth_group ], + read_func(device), max_gap: 0 ) @@ -768,7 +768,7 @@ defmodule ModBossTest do # max_gap as a map assert capture_log(fn -> - ModBoss.read(schema, read_func(device), [:first_group, :second_group], + ModBoss.read(schema, [:first_group, :second_group], read_func(device), max_gap: %{foo: 1} ) end) =~ "Invalid :foo gap size specified" @@ -777,7 +777,7 @@ defmodule ModBossTest do # max_gap as a keyword list assert capture_log(fn -> - ModBoss.read(schema, read_func(device), [:first_group, :second_group], + ModBoss.read(schema, [:first_group, :second_group], read_func(device), max_gap: [bar: 10] ) end) =~ "Invalid :bar gap size specified" @@ -808,7 +808,7 @@ defmodule ModBossTest do # max_gap as a map {_result, log} = with_log(fn -> - ModBoss.read(schema, read_func(device), [:first_group, :second_group], + ModBoss.read(schema, [:first_group, :second_group], read_func(device), max_gap: %{ holding_registers: "nope", input_registers: 3.14159, @@ -828,7 +828,7 @@ defmodule ModBossTest do # max_gap as a keyword list {_result, log} = with_log(fn -> - ModBoss.read(schema, read_func(device), [:first_group, :second_group], + ModBoss.read(schema, [:first_group, :second_group], read_func(device), max_gap: [ holding_registers: {:yikes, 1}, input_registers: %{}, @@ -866,7 +866,7 @@ defmodule ModBossTest do set_objects(device, values) # Even with max_gap: 4, the gap can't be bridged because :latch is gap_safe: false - {:ok, result} = ModBoss.read(schema, read_func(device), [:group_1, :group_2], max_gap: 4) + {:ok, result} = ModBoss.read(schema, [:group_1, :group_2], read_func(device), max_gap: 4) assert 2 = get_read_count(device) assert %{group_1: [0, 1], group_2: [4, 5]} = result @@ -892,7 +892,7 @@ defmodule ModBossTest do set_objects(device, values) # Since `:filler` is readable, it defaults to `gap_safe: true`; this should be a single read… - {:ok, result} = ModBoss.read(schema, read_func(device), [:group_1, :group_2], max_gap: 2) + {:ok, result} = ModBoss.read(schema, [:group_1, :group_2], read_func(device), max_gap: 2) assert 1 = get_read_count(device) assert %{group_1: [0, 1], group_2: [4, 5]} = result @@ -920,7 +920,7 @@ defmodule ModBossTest do # The gap spans addresses 2-3. Address 3 is gap_safe: false, so the gap can't be bridged. {:ok, result} = - ModBoss.read(schema, read_func(device), [:group_1, :group_2], max_gap: 4) + ModBoss.read(schema, [:group_1, :group_2], read_func(device), max_gap: 4) assert 2 = get_read_count(device) assert %{group_1: [0, 1], group_2: [4, 5]} = result @@ -943,7 +943,7 @@ defmodule ModBossTest do device = start_supervised!({Agent, fn -> @initial_state end}) set_objects(device, %{{:holding_register, 0} => 10, {:holding_register, 1} => 20}) - {:ok, result} = ModBoss.read(schema, read_func(device), [:safe, :latch]) + {:ok, result} = ModBoss.read(schema, [:safe, :latch], read_func(device)) assert %{safe: 10, latch: 20} = result end @@ -967,7 +967,7 @@ defmodule ModBossTest do set_objects(device, values) # Write-only mapping in the gap means the gap can't be bridged as a single read - {:ok, result} = ModBoss.read(schema, read_func(device), [:group_1, :group_2], max_gap: 4) + {:ok, result} = ModBoss.read(schema, [:group_1, :group_2], read_func(device), max_gap: 4) assert 2 = get_read_count(device) assert %{group_1: [0, 1], group_2: [4, 5]} = result @@ -986,7 +986,7 @@ defmodule ModBossTest do gap_safe: true, encoded_value: 123, value: 123 - }} = ModBoss.read(FakeSchema, read_func(device), :foo, debug: true) + }} = ModBoss.read(FakeSchema, :foo, read_func(device), debug: true) end test "debug mode returns a map of mapping details for a plural read" do @@ -1013,7 +1013,7 @@ defmodule ModBossTest do encoded_value: 22, value: 22 } - }} = ModBoss.read(FakeSchema, read_func(device), [:foo, :bar], debug: true) + }} = ModBoss.read(FakeSchema, [:foo, :bar], read_func(device), debug: true) end test "debug mode with decode: false omits value and includes encoded_value" do @@ -1039,7 +1039,7 @@ defmodule ModBossTest do }) assert {:ok, result} = - ModBoss.read(schema, read_func(device), :flag, debug: true, decode: false) + ModBoss.read(schema, :flag, read_func(device), debug: true, decode: false) assert %{ type: :holding_register, @@ -1095,7 +1095,7 @@ defmodule ModBossTest do encoded_value: 99, value: 99 } - }} = ModBoss.read(schema, read_func(device), :all, debug: true) + }} = ModBoss.read(schema, :all, read_func(device), debug: true) end test "debug mode shows encoded register values alongside the decoded value" do @@ -1129,7 +1129,7 @@ defmodule ModBossTest do gap_safe: true, encoded_value: [21587, 13104, 12336, 0], value: "TS3000" - }} = ModBoss.read(schema, read_func(device), :model_name, debug: true) + }} = ModBoss.read(schema, :model_name, read_func(device), debug: true) end test "max_attempts retries on error and succeeds" do @@ -1138,7 +1138,7 @@ defmodule ModBossTest do flaky_read = flakify(read_func(device), fn -> {:error, "flaky"} end, flakes: 2) - assert {:ok, 42} = ModBoss.read(FakeSchema, flaky_read, :foo, max_attempts: 3) + assert {:ok, 42} = ModBoss.read(FakeSchema, :foo, flaky_read, max_attempts: 3) end test "max_attempts returns error when all attempts exhausted" do @@ -1147,7 +1147,7 @@ defmodule ModBossTest do flaky_read = flakify(read_func(device), fn -> {:error, "flaky"} end, flakes: 2) - assert {:error, "flaky"} = ModBoss.read(FakeSchema, flaky_read, :foo, max_attempts: 2) + assert {:error, "flaky"} = ModBoss.read(FakeSchema, :foo, flaky_read, max_attempts: 2) end test "max_attempts retries are per-callback, not per-operation" do @@ -1176,7 +1176,7 @@ defmodule ModBossTest do # This is 2 batches (due to different objects types) that will each fail once # before succeeding. This means we'll make 2 attempts for each batch (4 total). assert {:ok, %{alpha: 10, bravo: 1}} = - ModBoss.read(schema, flaky_read, [:alpha, :bravo], max_attempts: 2) + ModBoss.read(schema, [:alpha, :bravo], flaky_read, max_attempts: 2) end test "max_attempts retries on raise and succeeds" do @@ -1185,43 +1185,43 @@ defmodule ModBossTest do raising_read = flakify(read_func(device), fn -> raise "raised!" end, flakes: 2) - assert {:ok, 42} = ModBoss.read(FakeSchema, raising_read, :foo, max_attempts: 3) + assert {:ok, 42} = ModBoss.read(FakeSchema, :foo, raising_read, max_attempts: 3) end test "max_attempts returns error when all attempts raise" do boom_func = fn _type, _addr, _count -> raise "boom!" end assert {:error, %RuntimeError{message: "boom!"}} = - ModBoss.read(FakeSchema, boom_func, :foo, max_attempts: 2) + ModBoss.read(FakeSchema, :foo, boom_func, max_attempts: 2) end test "max_attempts raises on invalid values" do device = start_supervised!({Agent, fn -> @initial_state end}) assert_raise RuntimeError, fn -> - ModBoss.read(FakeSchema, read_func(device), :foo, max_attempts: 0) + ModBoss.read(FakeSchema, :foo, read_func(device), max_attempts: 0) end assert_raise RuntimeError, fn -> - ModBoss.read(FakeSchema, read_func(device), :foo, max_attempts: -1) + ModBoss.read(FakeSchema, :foo, read_func(device), max_attempts: -1) end assert_raise RuntimeError, fn -> - ModBoss.read(FakeSchema, read_func(device), :foo, max_attempts: "foo") + ModBoss.read(FakeSchema, :foo, read_func(device), max_attempts: "foo") end end end - describe "ModBoss.write/3" do + describe "ModBoss.write/4" do test "writes objects 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}) + :ok = ModBoss.write(FakeSchema, %{baz: 1, corge: 1234}, write_func(device)) assert %{{:holding_register, 3} => 1, {:holding_register, 15} => 1234} = get_objects(device) end test "writes objects referenced by human-readable names from keyword" do device = start_supervised!({Agent, fn -> @initial_state end}) - :ok = ModBoss.write(FakeSchema, write_func(device), baz: 1, corge: 1234) + :ok = ModBoss.write(FakeSchema, [baz: 1, corge: 1234], write_func(device)) assert %{{:holding_register, 3} => 1, {:holding_register, 15} => 1234} = get_objects(device) end @@ -1229,7 +1229,7 @@ defmodule ModBossTest do device = start_supervised!({Agent, fn -> @initial_state end}) assert {:error, "Unknown mapping(s) :foobar, :bazqux for ModBossTest.FakeSchema."} = - ModBoss.write(FakeSchema, write_func(device), %{foobar: 1, bazqux: 2}) + ModBoss.write(FakeSchema, %{foobar: 1, bazqux: 2}, write_func(device)) end test "refuses to write unless all mappings are declared writable" do @@ -1244,17 +1244,17 @@ defmodule ModBossTest do set_objects(device, initial_values) assert {:error, "ModBoss Mapping(s) :foo, :bar in ModBossTest.FakeSchema are not writable."} = - ModBoss.write(FakeSchema, write_func(device), %{foo: 1, bar: 2, baz: 3}) + ModBoss.write(FakeSchema, %{foo: 1, bar: 2, baz: 3}, write_func(device)) assert get_objects(device) == initial_values - assert :ok = ModBoss.write(FakeSchema, write_func(device), %{baz: 3}) + assert :ok = ModBoss.write(FakeSchema, %{baz: 3}, write_func(device)) assert get_objects(device) == Map.put(initial_values, {:holding_register, 3}, 3) end test "writes named mappings that span more than one address" do device = start_supervised!({Agent, fn -> @initial_state end}) - :ok = ModBoss.write(FakeSchema, write_func(device), %{qux: [0, 10, 20], quux: [-1, -2]}) + :ok = ModBoss.write(FakeSchema, %{qux: [0, 10, 20], quux: [-1, -2]}, write_func(device)) assert %{ {:holding_register, 10} => 0, @@ -1289,7 +1289,7 @@ defmodule ModBossTest do device = start_supervised!({Agent, fn -> @initial_state end}) - :ok = ModBoss.write(schema, write_func(device), %{yep: true, nope: false, text: "Oh wow"}) + :ok = ModBoss.write(schema, %{yep: true, nope: false, text: "Oh wow"}, write_func(device)) assert %{ {:holding_register, 1} => 1, @@ -1305,7 +1305,7 @@ defmodule ModBossTest do assert {:error, "Failed to encode :qux. Encoded value [100, 200] for :qux does not match the number of mapped addresses."} = - ModBoss.write(FakeSchema, write_func(device), %{qux: [100, 200]}) + ModBoss.write(FakeSchema, %{qux: [100, 200]}, write_func(device)) end test "batches contiguous writes for each object type up to the Modbus protocol's maximum" do @@ -1332,19 +1332,19 @@ defmodule ModBossTest do single_batch = %{holding_1: values(122), holding_123: 1} double_double = %{holding_1: values(122), holding_123: 1, holding_124: 1} - assert :ok = ModBoss.write(schema, write_func(device), single_batch) + assert :ok = ModBoss.write(schema, single_batch, write_func(device)) assert 1 = get_write_count(device) - assert :ok = ModBoss.write(schema, write_func(device), double_double) + assert :ok = ModBoss.write(schema, double_double, write_func(device)) assert 2 = get_write_count(device) single_batch = %{coil_1001: values(1967), coil_2968: 1} double_batch = %{coil_1001: values(1967), coil_2968: 1, coil_2969: 1} - assert :ok = ModBoss.write(schema, write_func(device), single_batch) + assert :ok = ModBoss.write(schema, single_batch, write_func(device)) assert 1 = get_write_count(device) - assert :ok = ModBoss.write(schema, write_func(device), double_batch) + assert :ok = ModBoss.write(schema, double_batch, write_func(device)) assert 2 = get_write_count(device) end @@ -1368,7 +1368,7 @@ defmodule ModBossTest do device = start_supervised!({Agent, fn -> @initial_state end}) values = %{holding_1: 1, holding_2: 2, coil_1: 3, coil_2: 4} - :ok = ModBoss.write(schema, write_func(device), values) + :ok = ModBoss.write(schema, values, write_func(device)) assert 2 == get_write_count(device) @@ -1403,20 +1403,28 @@ defmodule ModBossTest do device = start_supervised!({Agent, fn -> @initial_state end}) assert :ok = - ModBoss.write(schema, write_func(device), %{ - holding_foo: 1, - holding_bar: [1, 1], - holding_baz: 1 - }) + ModBoss.write( + schema, + %{ + holding_foo: 1, + holding_bar: [1, 1], + holding_baz: 1 + }, + write_func(device) + ) assert 3 = get_write_count(device) assert :ok = - ModBoss.write(schema, write_func(device), %{ - coil_foo: 1, - coil_bar: [1, 1], - coil_baz: 1 - }) + ModBoss.write( + schema, + %{ + coil_foo: 1, + coil_bar: [1, 1], + coil_baz: 1 + }, + write_func(device) + ) assert 3 = get_write_count(device) end @@ -1438,7 +1446,7 @@ defmodule ModBossTest do device = start_supervised!({Agent, fn -> @initial_state end}) - :ok = ModBoss.write(schema, write_func(device), %{foo: 10, bar: 50}) + :ok = ModBoss.write(schema, %{foo: 10, bar: 50}, write_func(device)) assert 2 = get_write_count(device) assert %{{:holding_register, 1} => 10, {:holding_register, 3} => 50} = get_objects(device) @@ -1449,7 +1457,7 @@ defmodule ModBossTest do flaky_write = flakify(write_func(device), fn -> {:error, "flaky"} end, flakes: 2) - :ok = ModBoss.write(FakeSchema, flaky_write, [baz: 99], max_attempts: 3) + :ok = ModBoss.write(FakeSchema, [baz: 99], flaky_write, max_attempts: 3) assert %{{:holding_register, 3} => 99} = get_objects(device) end @@ -1458,7 +1466,7 @@ defmodule ModBossTest do flaky_write = flakify(write_func(device), fn -> {:error, "flaky"} end, flakes: 2) - {:error, "flaky"} = ModBoss.write(FakeSchema, flaky_write, [baz: 99], max_attempts: 2) + {:error, "flaky"} = ModBoss.write(FakeSchema, [baz: 99], flaky_write, max_attempts: 2) end test "max_attempts retries on raise and succeeds" do @@ -1466,7 +1474,7 @@ defmodule ModBossTest do raising_write = flakify(write_func(device), fn -> raise "raised!" end, flakes: 1) - :ok = ModBoss.write(FakeSchema, raising_write, [baz: 99], max_attempts: 2) + :ok = ModBoss.write(FakeSchema, [baz: 99], raising_write, max_attempts: 2) assert %{{:holding_register, 3} => 99} = get_objects(device) end @@ -1474,18 +1482,18 @@ defmodule ModBossTest do boom_func = fn _type, _addr, _values -> raise "kaboom!" end assert {:error, %RuntimeError{message: "kaboom!"}} = - ModBoss.write(FakeSchema, boom_func, [baz: 99], max_attempts: 2) + ModBoss.write(FakeSchema, [baz: 99], boom_func, max_attempts: 2) end test "max_attempts raises on invalid values" do device = start_supervised!({Agent, fn -> @initial_state end}) assert_raise RuntimeError, fn -> - ModBoss.write(FakeSchema, write_func(device), [baz: 99], max_attempts: 0) + ModBoss.write(FakeSchema, [baz: 99], write_func(device), max_attempts: 0) end assert_raise RuntimeError, fn -> - ModBoss.write(FakeSchema, write_func(device), [baz: 99], max_attempts: -1) + ModBoss.write(FakeSchema, [baz: 99], write_func(device), max_attempts: -1) end end end