Skip to content
Open
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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://hexdocs.pm/vintage_net_socket_can>.
Expand Down
14 changes: 14 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -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
14 changes: 11 additions & 3 deletions lib/vintage_net_socket_can.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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"]}
]
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 4 additions & 1 deletion test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -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)
79 changes: 76 additions & 3 deletions test/vintage_net_socket_can_test.exs
Original file line number Diff line number Diff line change
@@ -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