Skip to content
Draft
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
87 changes: 86 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,69 @@ config :vintage_net_wizard, captive_portal: false
It is possible to write a smartphone app to configure your device using an API
endpoint. Documentation for the API is in [json-api.md](json-api.md).

## Using as a Plug

VintageNetWizard uses `Plug.Router` under the hood and is dynamically routed
which means you can plug it into an existing web app without running a separate
server and port. This will only run routing and delivery of assets and will not
put device into AP mode.

**Using Plug.Router**
```elixir
forward "/wizard", to: VintageNetWizard.Web.Router
```

**Using Phoenix**
```elixir
scope "/", VintageNetWizard do
forward "/wizard", Web.Router
end
```

```elixir
scope "/", VintageNetWizard do
forward "/wizard", Web.Router
end
```

You can also specify an `:on_complete` option to specify an action to perform
when completing WiFi setup. Accepted values are:

* a string path - This specifies the location to redirect to after completing
setup and is useful if you want it to route to a different page within your app
* `{module, function, args}` - tuple that defines a function to be run when
configuration is completed.

```elixir
# Plug.Router
forward "/wizard",
to: VintageNetWizard.Web.Router,
init_opts: [on_complete: {MyModule, :handle_complete, []}]
```

```elixir
# Phoenix.Router
scope "/", VintageNetWizard do
forward "/wizard", Web.Router, on_complete: "/my_app/index"
end
```

If you wish to supply your own frontend UI, you can also use the JSON
endpoints in `VintageNetWizard.Web.Api` to fetch the data as needed from
the scope of your app:

```elixir
# Plug.Router
forward "/wizard/api/v1", to: VintageNetWizard.Web.Api
```

```elixir
# Phoenix.Router
scope "/", VintageNetWizard do
forward "/wizard/api/v1", Web.Api
end
```

## Running the example

The example builds a Nerves firmware image for supported Nerves devices
Expand Down Expand Up @@ -209,7 +272,9 @@ config :vintage_net_wizard,
ssid: "MY_SSID"
```

## Stop callback
## Callbacks

**On Exit**

If your application runs a webserver or has other functionality that is
incompatible with the wizard, you can use the `:on_exit` option to
Expand All @@ -230,6 +295,26 @@ defmodule MyApp do
end
```

**On Complete**

You can also specify a callback to be run when the configuration backend has completed,
but before the server exits with the `:on_complete` option which functions the same way
as `:on_exit` requiring `{module, function, args}` format.

```elixir
defmodule MyApp do
def start_wizard() do
VintageNetWizard.run_wizard(
on_complete: {__MODULE__, :handle_on_complete, []}
)
end

def handle_on_complete() do
Logger.info("VintageNetWizard stopped")
end
end
```

## Development

It's possible to work on the wizard locally and without using Nerves or changing
Expand Down
3 changes: 3 additions & 0 deletions lib/vintage_net_wizard.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ defmodule VintageNetWizard do
- `:ssl` - A Keyword list of `:ssl.tls_server_options`
- `:on_exit` - `{module, function, args}` tuple specifying
callback to perform after stopping the server.
- `:on_complete` - `{module, function, args}` tuple specifying
callback to run after completing configuration, but before
server is shutdown. Expected to be an mfa

See `Plug.SSL.configure/1` for more information about the
SSL options.
Expand Down
8 changes: 7 additions & 1 deletion lib/vintage_net_wizard/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ defmodule VintageNetWizard.Application do

use Application

alias VintageNetWizard.{Backend, BackendServer, Callbacks}

@spec start(Application.start_type(), any()) :: {:error, any} | {:ok, pid()}
def start(_type, _args) do
backend = Application.get_env(:vintage_net_wizard, :backend, Backend.Default)

children = [
{Task.Supervisor, name: VintageNetWizard.TaskSupervisor},
VintageNetWizard.Web.Endpoint
VintageNetWizard.Web.Endpoint,
{BackendServer, backend},
Callbacks
]

opts = [strategy: :one_for_one, name: VintageNetWizard.Supervisor]
Expand Down
5 changes: 4 additions & 1 deletion lib/vintage_net_wizard/backend_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ defmodule VintageNetWizard.BackendServer do
alias VintageNetWizard.WiFiConfiguration
alias VintageNetWiFi.AccessPoint

# Hibernate after 5 minutes
@hibernate_after 5 * 60 * 1000

defmodule State do
@moduledoc false
defstruct subscriber: nil, backend: nil, backend_state: nil, configurations: []
end

@spec start_link(backend :: module()) :: GenServer.on_start()
def start_link(backend) do
GenServer.start_link(__MODULE__, backend, name: __MODULE__)
GenServer.start_link(__MODULE__, backend, name: __MODULE__, hibernate_after: @hibernate_after)
end

@doc """
Expand Down
29 changes: 27 additions & 2 deletions lib/vintage_net_wizard/callbacks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,35 @@ defmodule VintageNetWizard.Callbacks do

require Logger

def start_link(callbacks) do
callbacks = Enum.reduce(callbacks, [], &validate_callback/2)
def start_link(callbacks \\ []) do
callbacks =
starting_callbacks(callbacks)
|> Enum.reduce([], &validate_callback/2)

Agent.start_link(fn -> callbacks end, name: __MODULE__)
end

def list() do
Agent.get(__MODULE__, & &1)
end

def on_complete() do
list()
|> Keyword.get(:on_complete)
|> apply_callback()
end

def on_exit() do
list()
|> Keyword.get(:on_exit)
|> apply_callback()
end

def set_callbacks(callbacks) do
cbs = Enum.reduce(callbacks, [], &validate_callback/2)
Agent.update(__MODULE__, &Keyword.merge(&1, cbs))
end

defp apply_callback({mod, fun, args}) do
try do
apply(mod, fun, args)
Expand All @@ -29,6 +43,17 @@ defmodule VintageNetWizard.Callbacks do

defp apply_callback(invalid), do: {:error, "invalid callback: #{inspect(invalid)}"}

defp starting_callbacks(initial) do
cbs = Application.get_env(:vintage_net_wizard, :callbacks, [])

if Keyword.keyword?(cbs) do
Keyword.merge(cbs, initial)
else
Logger.warn("[VintageNetWizard] invalid callbacks defined in config:\n\t#{inspect(cbs)}\n")
initial
end
end

defp validate_callback({_key, {mod, fun, args}} = callback, acc)
when is_atom(mod) and is_atom(fun) and is_list(args) do
[callback | acc]
Expand Down
18 changes: 8 additions & 10 deletions lib/vintage_net_wizard/web/api.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
defmodule VintageNetWizard.Web.Api do
@moduledoc false

alias VintageNetWizard.{WiFiConfiguration, BackendServer}
alias VintageNetWizard.Web.Endpoint
alias VintageNetWizard.{Callbacks, WiFiConfiguration, BackendServer}
alias Plug.Conn

use Plug.Router

plug(Plug.Parsers, parsers: [:json], json_decoder: Jason)
plug(:match)
plug(:dispatch)
plug(:dispatch, builder_opts())

get "/configuration/status" do
with status <- BackendServer.configuration_status(),
Expand Down Expand Up @@ -37,13 +36,12 @@ defmodule VintageNetWizard.Web.Api do
get "/complete" do
:ok = BackendServer.complete()

_ =
Task.Supervisor.start_child(VintageNetWizard.TaskSupervisor, fn ->
# We don't want to stop the server before we
# send the response back.
:timer.sleep(3000)
Endpoint.stop_server()
end)
# Set the callback if provided as a Plug opt
if callback = opts[:on_complete] do
Callbacks.set_callbacks(on_complete: callback)
end

_ = Callbacks.on_complete()

send_json(conn, 202, "")
end
Expand Down
50 changes: 30 additions & 20 deletions lib/vintage_net_wizard/web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ defmodule VintageNetWizard.Web.Endpoint do
@moduledoc """
Supervisor for the Web part of the VintageNet Wizard.
"""
alias VintageNetWizard.{BackendServer, Callbacks, Web.Router, Web.RedirectRouter}
alias VintageNetWizard.{Callbacks, Web.Router, Web.RedirectRouter}
alias VintageNetWizard.TaskSupervisor, as: Tasks
use DynamicSupervisor

@type opt :: {:ssl, :ssl.tls_server_option()} | {:on_exit, {module(), atom(), list()}}
Expand All @@ -23,15 +24,15 @@ defmodule VintageNetWizard.Web.Endpoint do
:ok | {:error, :already_started | :no_keyfile | :no_certfile}
def start_server(opts \\ []) do
use_ssl? = Keyword.has_key?(opts, :ssl)
use_captive_portal? = Application.get_env(:vintage_net_wizard, :captive_portal, true)
backend = Application.get_env(:vintage_net_wizard, :backend, VintageNetWizard.Backend.Default)
callbacks = Keyword.take(opts, [:on_exit])

use_captive_portal? =
opts[:captive_portal] || Application.get_env(:vintage_net_wizard, :captive_portal, true)

_ = set_callbacks(opts)

with spec <- maybe_use_ssl(use_ssl?, opts),
{:ok, _pid} <- DynamicSupervisor.start_child(__MODULE__, spec),
{:ok, _pid} <- maybe_with_redirect(use_captive_portal?, use_ssl?),
{:ok, _pid} <- DynamicSupervisor.start_child(__MODULE__, {BackendServer, backend}),
{:ok, _pid} <- DynamicSupervisor.start_child(__MODULE__, {Callbacks, callbacks}) do
{:ok, _pid} <- maybe_with_redirect(use_captive_portal?, use_ssl?) do
:ok
else
{:error, :max_children} -> {:error, :already_started}
Expand All @@ -49,26 +50,17 @@ defmodule VintageNetWizard.Web.Endpoint do
{:error, :not_found}

children ->
# Ensure we terminate callbacks last after all other children
# and the callbacks have been executed
callbacks_child =
Enum.reduce(children, nil, fn {_, child, _, [mod]}, _acc ->
if mod == Callbacks do
child
else
DynamicSupervisor.terminate_child(__MODULE__, child)
end
end)
Enum.each(children, fn {_, child, _, _} ->
DynamicSupervisor.terminate_child(__MODULE__, child)
end)

_ = Callbacks.on_exit()

_ = DynamicSupervisor.terminate_child(__MODULE__, callbacks_child)
end
end

@impl DynamicSupervisor
def init(_) do
DynamicSupervisor.init(strategy: :one_for_one, max_children: 4)
DynamicSupervisor.init(strategy: :one_for_one, max_children: 2)
end

defp dispatch do
Expand Down Expand Up @@ -131,4 +123,22 @@ defmodule VintageNetWizard.Web.Endpoint do
]
)
end

defp set_callbacks(opts) do
on_complete =
{Task.Supervisor, :start_child,
[
Tasks,
fn ->
# We don't want to stop the server before we
# send the response back.
:timer.sleep(3000)
__MODULE__.stop_server()
end
]}

Keyword.take(opts, [:on_exit])
|> Keyword.put(:on_complete, on_complete)
|> Callbacks.set_callbacks()
end
end
Loading