Skip to content

Commit

Permalink
Support modem discovery via automatic modem implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
mattludwigs committed Feb 25, 2022
1 parent c4856bc commit f0bae8c
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 0 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,56 @@ Here's an example with a service provider list:
}
```

## Automatic modem detection

If it is not known ahead of time what modem will be installed, the
`VintageNetMobile.Modem.Automatic` implementation can detect and configure it
for you. It works by waiting for the system to report a USB device with a known
vendor and product ID. Once one is found, it calls `VintageNet.configure/3` to
change the configuration from using the `Automatic` modem to the actual modem
implementation. It tells `VintageNet.configure/3` to not persist the
configuration so that the detection process runs again on the next boot.

Here's an example:

```elixir
%{
type: VintageNetMobile,
vintage_net_mobile: %{
modem: VintageNetMobile.Modem.Automatic,
service_providers: [
%{apn: "wireless.twilio.com"}
]
}
}
```

See [VintageNetMobile.Modem.Automatic.Discovery](lib/vintage_net_mobile/modem/automatic/discovery.ex)
for a list of modems that can be detected.

To check if your modem has been detected correctly you can run
`VintageNet.info()` and check the output configuration's `:modem` field:

```elixir
iex> VintageNet.info()
Interface ppp0
Type: VintageNetMobile
Power: Starting up/on (248378 ms left)
Present: true
State: :configured (0:01:13)
Connection: :internet (21.8 s)
Addresses: 111.11.111.111/32
Configuration:
%{
type: VintageNetMobile,
vintage_net_mobile: %{
modem: VintageNetMobile.Modem.TelitLE910,
service_providers: [%{apn: "thebestapn"}]
}
}

```

## VintageNet Properties

In addition to the common `vintage_net` properties for all interface types, this
Expand Down
27 changes: 27 additions & 0 deletions lib/vintage_net_mobile/modem/automatic.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule VintageNetMobile.Modem.Automatic do
@moduledoc """
Modem for automatic detection and configuration of USB-based modems
This is useful for when you need to support different types of modems and you
cannot provide a static configuration for one particular modem.
"""
@behaviour VintageNetMobile.Modem

alias VintageNet.Interface.RawConfig
alias VintageNetMobile.Modem.Automatic.Discovery

@impl VintageNetMobile.Modem
def normalize(config), do: config

@impl VintageNetMobile.Modem
def add_raw_config(raw_config, %{vintage_net_mobile: _mobile}, _opts) do
child_specs = [
{Discovery, [raw_config: raw_config]}
]

%RawConfig{
raw_config
| child_specs: child_specs
}
end
end
96 changes: 96 additions & 0 deletions lib/vintage_net_mobile/modem/automatic/discovery.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
defmodule VintageNetMobile.Modem.Automatic.Discovery do
@moduledoc false

# A process for trying to discover a the type of modem and some basic
# information to allow dynamic configuration items

use GenServer

alias Circuits.UART

# the poll time for trying to enumerate the UARTS
@enumeration_poll 3_000

# To discover a which modem to use this process looks at the vendor ids and
# manufacturer id to map to a `VintageNetMobile.Modem` implementation.

# A source to find vendor and manufacturer ids is
# https://elixir.bootlin.com/linux/latest/source/drivers/usb/serial/option.c#L245

@quectel_vender_id 0x2C7C
@telit_vendor_id 0x1BC7

@modems %{
{@telit_vendor_id, 0x1201} => VintageNetMobile.Modem.TelitLE910,
{@quectel_vender_id, 0x0125} => VintageNetMobile.Modem.QuectelEC25,
# map the EC21 to the EC25 as they work the same
{@quectel_vender_id, 0x0121} => VintageNetMobile.Modem.QuectelEC25,
{@quectel_vender_id, 0x0296} => VintageNetMobile.Modem.QuectelBG96
}

@typedoc """
Init args
- `:raw_config` - The RawConfig that contains configuration information to
pass the discovered modem
"""
@type init_arg() :: {:raw_config, VintageNet.Interface.RawConfig.t()}

@doc """
Start the discovery server
"""
@spec start_link([init_arg()]) :: GenServer.on_start()
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end

@impl GenServer
def init(args) do
{:ok, %{modem: nil, raw_config: args[:raw_config]}, {:continue, :detect_modem}}
end

@impl GenServer
def handle_continue(:detect_modem, state) do
{:noreply, do_detect_modem(state)}
end

@impl GenServer
def handle_info(:detect_modem, state) do
{:noreply, do_detect_modem(state)}
end

defp do_detect_modem(state) do
case detect_modem() do
nil ->
Process.send_after(self(), :detect_modem, @enumeration_poll)
state

modem ->
state = %{state | modem: modem}
:ok = configure_vintage_net(state)

state
end
end

defp configure_vintage_net(state) do
config = state.raw_config.source_config.vintage_net_mobile |> Map.put(:modem, state.modem)

VintageNet.configure(
state.raw_config.ifname,
%{type: VintageNetMobile, vintage_net_mobile: config},
persist: false
)
end

defp detect_modem() do
UART.enumerate()
|> Enum.find_value(&known_modem/1)
end

defp known_modem({_tty, %{vendor_id: vid, product_id: pid}}) do
Map.get(@modems, {vid, pid})
end

defp known_modem(_), do: nil
end
49 changes: 49 additions & 0 deletions test/vintage_net_mobile/modem/automatic_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule VintageNetMobile.Modem.AutomaticTest do
use ExUnit.Case, async: true

alias VintageNetMobile.Modem.Automatic
alias VintageNetMobile.Modem.Automatic.Discovery
alias VintageNet.Interface.RawConfig

test "create LTE configuration" do
input = %{
type: VintageNetMobile,
vintage_net_mobile: %{
modem: Automatic,
service_providers: [%{apn: "choosethislteitissafe"}, %{apn: "wireless.twilio.com"}]
}
}

# RawConfig that will be passed to the discovered modem
base_raw_config = %RawConfig{
ifname: "ppp0",
type: VintageNetMobile,
source_config: input,
required_ifnames: ["wwan0"],
up_cmds: [],
down_cmds: [],
files: [],
child_specs: []
}

output = %RawConfig{
ifname: "ppp0",
type: VintageNetMobile,
source_config: input,
required_ifnames: ["wwan0"],
up_cmds: [
{:run_ignore_errors, "mknod", ["/dev/ppp", "c", "108", "0"]}
],
down_cmds: [
{:fun, VintageNet.PropertyTable, :clear_prefix,
[VintageNet, ["interface", "ppp0", "mobile"]]}
],
files: [],
child_specs: [
{Discovery, [raw_config: base_raw_config]}
]
}

assert output == VintageNetMobile.to_raw_config("ppp0", input, Utils.default_opts())
end
end

0 comments on commit f0bae8c

Please sign in to comment.