diff --git a/README.md b/README.md index 35f9231..2101a31 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/vintage_net_mobile/modem/automatic.ex b/lib/vintage_net_mobile/modem/automatic.ex new file mode 100644 index 0000000..3541ecc --- /dev/null +++ b/lib/vintage_net_mobile/modem/automatic.ex @@ -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 diff --git a/lib/vintage_net_mobile/modem/automatic/discovery.ex b/lib/vintage_net_mobile/modem/automatic/discovery.ex new file mode 100644 index 0000000..c6fd7ea --- /dev/null +++ b/lib/vintage_net_mobile/modem/automatic/discovery.ex @@ -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 diff --git a/test/vintage_net_mobile/modem/automatic_test.exs b/test/vintage_net_mobile/modem/automatic_test.exs new file mode 100644 index 0000000..14d0ba3 --- /dev/null +++ b/test/vintage_net_mobile/modem/automatic_test.exs @@ -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