diff --git a/README.md b/README.md index b861a93..5b56244 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,40 @@ def deps do end ``` +## Configuration + +Configure a SocketCAN interface through VintageNet: + +```elixir +config :vintage_net, + config: [ + {"can0", + %{ + type: VintageNetSocketCAN, + vintage_net_socket_can: %{ + bitrate: 250_000, + restart_ms: 100 + } + }} + ] +``` + +### Options + +- `:bitrate` (integer, required) - CAN bus bitrate in bits/sec (e.g. `250_000` + for NMEA 2000). +- `:sample_point` (float, default `0.825`) - bit sample point. +- `:loopback` (boolean, default `false`) - enable local loopback. +- `:listen_only` (boolean, default `false`) - passive mode; the controller + receives frames but never transmits or acknowledges. Useful for diagnostics. +- `:restart_ms` (integer, default `0`) - automatic bus-off recovery delay in + milliseconds. `0` (the kernel default) disables auto-recovery, so once the + controller goes bus-off it stays off until the interface is cycled. Set a + positive value (e.g. `100`) so the controller automatically rejoins the bus + after a bus-off condition. + +## Documentation + Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) and published on [HexDocs](https://hexdocs.pm). Once published, the docs can be found at . diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..1601e66 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,14 @@ +import Config + +# When running the test suite on a host, VintageNet starts as an OTP +# application and would otherwise try to write to system paths like +# /etc/resolv.conf. Redirect its filesystem writes to a temp directory and +# disable persistence so unit tests stay hermetic. +if config_env() == :test do + tmp_dir = Path.join(System.tmp_dir!(), "vintage_net_socket_can_test") + + config :vintage_net, + resolvconf: Path.join(tmp_dir, "resolv.conf"), + persistence: VintageNet.Persistence.Null, + persistence_dir: Path.join(tmp_dir, "persistence") +end diff --git a/lib/vintage_net_socket_can.ex b/lib/vintage_net_socket_can.ex index e2ecadb..3e99531 100644 --- a/lib/vintage_net_socket_can.ex +++ b/lib/vintage_net_socket_can.ex @@ -11,7 +11,8 @@ defmodule VintageNetSocketCAN do {:bitrate, :integer}, {:sample_point, :float}, {:loopback, :boolean}, - {:listen_only, :boolean} + {:listen_only, :boolean}, + {:restart_ms, :integer} ] @impl VintageNet.Technology @@ -22,7 +23,12 @@ defmodule VintageNetSocketCAN do %{ sample_point: 0.825, loopback: false, - listen_only: false + listen_only: false, + # Automatic bus-off recovery delay in milliseconds. 0 (the kernel + # default) disables auto-recovery, so the controller stays bus-off + # until the interface is manually cycled. Set a positive value (e.g. + # 100) to have the controller automatically rejoin the bus. + restart_ms: 0 } normalized = Map.merge(default, socket_can_config) @@ -76,7 +82,9 @@ defmodule VintageNetSocketCAN do "loopback", if(config[:loopback], do: "on", else: "off"), "listen-only", - if(config[:listen_only], do: "on", else: "off") + if(config[:listen_only], do: "on", else: "off"), + "restart-ms", + Integer.to_string(config[:restart_ms]) ]}, {:run, "ip", ["link", "set", ifname, "up"]} ] diff --git a/mix.exs b/mix.exs index 6c4df70..51e28fa 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule VintageNetSocketCAN.MixProject do use Mix.Project - @version "0.1.0" + @version "0.2.0" @github "https://github.com/protolux-electronics/vintage_net_socket_can" def project do diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..875153b 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,4 @@ -ExUnit.start() +# `to_raw_config/3` shells out to `ip` while building the config, so the tests +# that exercise it can only run where `ip` is on the PATH (Linux/CI/target). +exclude = if System.find_executable("ip"), do: [], else: [requires_ip: true] +ExUnit.start(exclude: exclude) diff --git a/test/vintage_net_socket_can_test.exs b/test/vintage_net_socket_can_test.exs index 8d8a006..72f951f 100644 --- a/test/vintage_net_socket_can_test.exs +++ b/test/vintage_net_socket_can_test.exs @@ -1,8 +1,81 @@ defmodule VintageNetSocketCANTest do use ExUnit.Case - doctest VintageNetSocketCAN - test "greets the world" do - assert VintageNetSocketCAN.hello() == :world + alias VintageNet.Interface.RawConfig + + defp socket_can_opts(raw_config) do + {:run, "ip", ["link", "set", "can0", "type", "can" | rest]} = + Enum.find(raw_config.up_cmds, fn + {:run, "ip", ["link", "set", "can0", "type", "can" | _]} -> true + _ -> false + end) + + rest + |> Enum.chunk_every(2) + |> Map.new(fn [k, v] -> {k, v} end) + end + + describe "normalize/1" do + test "applies defaults for omitted options" do + %{vintage_net_socket_can: normalized} = + VintageNetSocketCAN.normalize(%{ + type: VintageNetSocketCAN, + vintage_net_socket_can: %{bitrate: 250_000} + }) + + assert normalized.bitrate == 250_000 + assert normalized.sample_point == 0.825 + assert normalized.loopback == false + assert normalized.listen_only == false + assert normalized.restart_ms == 0 + end + + test "keeps an explicit restart_ms" do + %{vintage_net_socket_can: normalized} = + VintageNetSocketCAN.normalize(%{ + type: VintageNetSocketCAN, + vintage_net_socket_can: %{bitrate: 250_000, restart_ms: 100} + }) + + assert normalized.restart_ms == 100 + end + + test "raises when restart_ms is not an integer" do + assert_raise ArgumentError, fn -> + VintageNetSocketCAN.normalize(%{ + type: VintageNetSocketCAN, + vintage_net_socket_can: %{bitrate: 250_000, restart_ms: 1.5} + }) + end + end + end + + describe "to_raw_config/3" do + @describetag :requires_ip + + test "includes restart-ms in the up command" do + config = + VintageNetSocketCAN.normalize(%{ + type: VintageNetSocketCAN, + vintage_net_socket_can: %{bitrate: 250_000, restart_ms: 100} + }) + + raw_config = VintageNetSocketCAN.to_raw_config("can0", config, []) + + assert %RawConfig{} = raw_config + assert socket_can_opts(raw_config)["restart-ms"] == "100" + end + + test "defaults restart-ms to 0 (auto-recovery disabled)" do + config = + VintageNetSocketCAN.normalize(%{ + type: VintageNetSocketCAN, + vintage_net_socket_can: %{bitrate: 250_000} + }) + + raw_config = VintageNetSocketCAN.to_raw_config("can0", config, []) + + assert socket_can_opts(raw_config)["restart-ms"] == "0" + end end end