Add Phoenix LiveView score display page and LV dashboard

This commit is contained in:
Mikko Ahlroth 2020-05-22 11:22:57 +03:00
parent 452557581e
commit c9237e2c58
21 changed files with 4901 additions and 36 deletions

View file

@ -48,7 +48,7 @@ config :ex_speed_game,
win_delay: 2_000,
# Interface to show IP for when using ShowIP
iface: "usb0",
iface: "wlan0",
# LED pattern to show when there is a crash
crash_pattern: {false, true, true, false},
@ -75,6 +75,20 @@ config :nerves, source_date_epoch: "1583172807"
config :logger, backends: [RingLogger]
config :ex_speed_game, ExSpeedGame.Web.Endpoint,
url: [host: "localhost"],
http: [port: 1337],
debug_errors: true,
check_origin: false,
secret_key_base: "s65CvlHa8RM4jVIgqCAbpAwbuiUFSJr7FUy5mh7kB0UrnSpOEcAcDu1Eajgtm912",
render_errors: [view: ExSpeedGame.Web.ErrorView, accepts: ~w(html json), layout: false],
pubsub_server: ExSpeedGame.Web.PubSub,
live_view: [signing_salt: "58shuVUu"],
server: true
config :phoenix, :json_library, Jason
config :phoenix, :stacktrace_depth, 20
if Mix.target() != :host do
import_config "target.exs"
end

View file

@ -42,7 +42,7 @@ config :nerves_firmware_ssh,
# Setting the node_name will enable Erlang Distribution.
# Only enable this for prod if you understand the risks.
node_name = if Mix.env() != :prod, do: "ex_speed_game"
node_name = "ex_speed_game"
config :nerves_init_gadget,
ifname: "usb0",
@ -51,6 +51,22 @@ config :nerves_init_gadget,
node_name: node_name,
node_host: :mdns_domain
config :nerves_network,
regulatory_domain: "FI"
key_mgmt = System.get_env("NERVES_NETWORK_KEY_MGMT") || "WPA-PSK"
config :nerves_network, :default,
wlan0: [
networks: [
[
ssid: System.get_env("NERVES_NETWORK_SSID"),
psk: System.get_env("NERVES_NETWORK_PSK"),
key_mgmt: String.to_atom(key_mgmt)
]
]
]
# Import target specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
# Uncomment to use target specific configurations

View file

@ -29,27 +29,25 @@ defmodule ExSpeedGame.Application do
end
# List all child processes to be supervised
def children(:host) do
[
# Children that only run on the host
# Starts a worker by calling: ExSpeedGame.Worker.start_link(arg)
# {ExSpeedGame.Worker, arg},
]
end
def children(_target) do
pins = Application.get_env(:ex_speed_game, :button_pins)
led_pins = Application.get_env(:ex_speed_game, :button_light_pins)
debounce_delay = Application.get_env(:ex_speed_game, :debounce_delay)
[
# Children for all targets except host
# Starts a worker by calling: ExSpeedGame.Worker.start_link(arg)
{Registry,
[name: ExSpeedGame.PubSub, keys: :duplicate, partitions: System.schedulers_online()]},
{ButtonInput,
%ButtonInput.Options{pins: pins, debounce_delay: debounce_delay, name: ButtonInput}},
{Lights, %Lights.Options{light_pins: led_pins, name: Lights}},
{Menu, %Menu.Options{name: Menu}},
{DynamicSupervisor, strategy: :one_for_one, name: GameSupervisor}
{DynamicSupervisor, strategy: :one_for_one, name: GameSupervisor},
# Web UI
ExSpeedGame.Web.Telemetry,
{Phoenix.PubSub, name: ExSpeedGame.Web.PubSub},
ExSpeedGame.Web.Endpoint
]
end

View file

@ -1,12 +1,15 @@
defmodule ExSpeedGame.Game.Menu do
use GenServer
require Logger
alias ExSpeedGame.Game.{Types, ButtonInput, ShowIP, Lights}
alias ExSpeedGame.Game.Modes.{
Speed
}
alias ExSpeedGame.PubSub
@menu {
{"SpeedGame", Speed,
%Speed.Options{
@ -82,7 +85,9 @@ defmodule ExSpeedGame.Game.Menu do
end
display_index(new_index)
{:noreply, %State{state | index: new_index}}
new_state = %State{state | index: new_index}
PubSub.publish(new_state)
{:noreply, new_state}
end
@spec handle_info({:input, Types.pin()}, State.t()) :: {:noreply, State.t()}
@ -96,7 +101,9 @@ defmodule ExSpeedGame.Game.Menu do
ref = Process.monitor(pid)
{:noreply, %State{state | mode: :ingame, game: pid, game_ref: ref}}
new_state = %State{state | mode: :ingame, game: pid, game_ref: ref}
PubSub.publish(new_state)
{:noreply, new_state}
end
@spec handle_info({:DOWN, reference(), :process, any, any}, State.t()) :: {:noreply, State.t()}
@ -109,6 +116,8 @@ defmodule ExSpeedGame.Game.Menu do
} = state
)
when ref == game_ref do
PubSub.clear(state.game)
# Ensure input is released in case process crashed violently
ButtonInput.release(ButtonInput)
@ -120,11 +129,26 @@ defmodule ExSpeedGame.Game.Menu do
ButtonInput.acquire(ButtonInput)
display_index(index)
{:noreply, %State{state | mode: :menu, game: nil, game_ref: nil}}
new_state = %State{state | mode: :menu, game: nil, game_ref: nil}
PubSub.publish(new_state)
{:noreply, new_state}
end
@impl true
@spec handle_call({:esg_pubsub, :get_state}, GenServer.from(), State.t()) ::
{:reply, State.t(), State.t()}
def handle_call({:esg_pubsub, :get_state}, _from, %State{} = state) do
{:reply, state, state}
end
@spec index_name(integer()) :: String.t()
def index_name(index) do
@menu |> elem(index) |> elem(0)
end
defp display_index(index) do
Logger.debug("Menu viewing: #{@menu |> elem(index) |> elem(0)}")
Logger.debug("Menu viewing: #{index_name(index)}")
Lights.show_binary(Lights, index + 1)
end
end

View file

@ -1,7 +1,9 @@
defmodule ExSpeedGame.Game.Modes.Speed do
use GenServer, restart: :temporary
require Logger
alias ExSpeedGame.Game.{Types, Lights, ButtonInput, Randomiser}
alias ExSpeedGame.PubSub
@max_queue Application.get_env(:ex_speed_game, :max_queue)
@ -64,14 +66,16 @@ defmodule ExSpeedGame.Game.Modes.Speed do
next_delay = schedule_tick(state.delay)
{:noreply,
%State{
state
| delay: next_delay,
previous: choice,
queue: new_queue,
queue_len: state.queue_len + 1
}}
new_state = %State{
state
| delay: next_delay,
previous: choice,
queue: new_queue,
queue_len: state.queue_len + 1
}
PubSub.publish(new_state)
{:noreply, new_state}
end
@impl true
@ -96,19 +100,28 @@ defmodule ExSpeedGame.Game.Modes.Speed do
score = state.score + 1
Logger.debug("Score: #{score}")
{:noreply,
%State{
state
| queue: :queue.drop(state.queue),
queue_len: state.queue_len - 1,
score: score
}}
new_state = %State{
state
| queue: :queue.drop(state.queue),
queue_len: state.queue_len - 1,
score: score
}
PubSub.publish(new_state)
{:noreply, new_state}
_ ->
end_game(state)
end
end
@impl true
@spec handle_call({:esg_pubsub, :get_state}, GenServer.from(), State.t()) ::
{:reply, State.t(), State.t()}
def handle_call({:esg_pubsub, :get_state}, _from, %State{} = state) do
{:reply, state, state}
end
@impl true
@spec terminate(any(), any()) :: term()
def terminate(_reason, _state) do
@ -131,13 +144,17 @@ defmodule ExSpeedGame.Game.Modes.Speed do
@spec end_game(State.t()) :: {:noreply, State.t()}
defp end_game(state) do
new_state = %State{state | active: false}
# Don't listen to button inputs while showing failure lights
Logger.debug("Game ended with score #{state.score}.")
PubSub.publish(new_state)
ButtonInput.release(ButtonInput)
Lights.set(Lights, Application.get_env(:ex_speed_game, :fail_pattern))
Process.sleep(Application.get_env(:ex_speed_game, :win_delay))
ButtonInput.acquire(ButtonInput)
{:noreply, %State{state | active: false}}
{:noreply, new_state}
end
end

30
lib/utils/pubsub.ex Normal file
View file

@ -0,0 +1,30 @@
defmodule ExSpeedGame.PubSub do
@moduledoc """
Light pubsub system based on Registry so that processes can listen to state publishes from
others.
Processes must be GenServers and respond to state request.
"""
@spec subscribe(atom | pid) :: term
def subscribe(target) when is_atom(target) or is_pid(target) do
target = if is_atom(target), do: Process.whereis(target), else: target
{:ok, _} = Registry.register(__MODULE__, target, nil)
GenServer.call(target, {:esg_pubsub, :get_state})
end
@spec publish(any) :: :ok
def publish(data) do
Registry.dispatch(__MODULE__, self(), fn entries ->
for {pid, _} <- entries, do: send(pid, {:esg_pubsub, :pub, data})
end)
end
@spec clear(atom | pid) :: :ok
def clear(target) when is_atom(target) or is_pid(target) do
target = if is_atom(target), do: Process.whereis(target), else: target
:ok = Registry.unregister(__MODULE__, target)
end
end

44
lib/web/endpoint.ex Normal file
View file

@ -0,0 +1,44 @@
defmodule ExSpeedGame.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :ex_speed_game
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
@session_options [
store: :cookie,
key: "_ex_speed_game_web_key",
signing_salt: "XKy30H5R"
]
socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
plug(Plug.Static,
at: "/",
from: :ex_speed_game,
gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
)
plug(Phoenix.LiveDashboard.RequestLogger,
param_key: "request_logger",
cookie_key: "request_logger"
)
plug(Plug.RequestId)
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
plug(Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
)
plug(Plug.MethodOverride)
plug(Plug.Head)
plug(Plug.Session, @session_options)
plug(ExSpeedGame.Web.Router)
end

View file

@ -0,0 +1,52 @@
defmodule ExSpeedGame.Web.MainLive.Main do
use ExSpeedGame.Web, :live_view
require Logger
alias ExSpeedGame.PubSub
alias ExSpeedGame.Game.{Menu}
alias ExSpeedGame.Game.Modes.{Speed}
@impl true
def mount(_params, _session, socket) do
menu_state = PubSub.subscribe(ExSpeedGame.Game.Menu)
socket = assign(socket, score: 0, queue_len: 0)
{:ok, set_menu_assigns(socket, menu_state)}
end
@impl true
def handle_info(msg, socket)
def handle_info({:esg_pubsub, :pub, %Menu.State{} = menu_state}, socket) do
socket =
if socket.assigns.mode == :menu and menu_state.mode == :ingame do
set_game_assigns(socket, PubSub.subscribe(menu_state.game))
else
socket
end
{:noreply, set_menu_assigns(socket, menu_state)}
end
def handle_info({:esg_pubsub, :pub, %Speed.State{} = game_state}, socket) do
{:noreply, set_game_assigns(socket, game_state)}
end
def handle_info(msg, socket) do
Logger.debug("Got unknown message into LV: #{inspect(msg)}")
{:noreply, socket}
end
defp set_menu_assigns(socket, %Menu.State{mode: mode, index: index}) do
assign(socket, mode: mode, title: Menu.index_name(index))
end
defp set_game_assigns(socket, %Speed.State{} = game_state) do
assign(socket,
mode: :ingame,
score: game_state.score,
queue_len: game_state.queue_len
)
end
end

View file

@ -0,0 +1,13 @@
<h1><%= @title %></h1>
<%= if @mode == :ingame do %>
<div id="game_score">
<p>Score</p>
<p><%= @score %></p>
</div>
<div id="queue_len">
<p>Queue length</p>
<p><%= @queue_len %></p>
</div>
<% end %>

30
lib/web/router.ex Normal file
View file

@ -0,0 +1,30 @@
defmodule ExSpeedGame.Web.Router do
use ExSpeedGame.Web, :router
import Phoenix.LiveDashboard.Router
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_live_flash)
plug(:put_root_layout, {ExSpeedGame.Web.LayoutView, :root})
plug(:protect_from_forgery)
plug(:put_secure_browser_headers)
end
pipeline :api do
plug(:accepts, ["json"])
end
scope "/", ExSpeedGame.Web do
pipe_through(:browser)
live("/", MainLive.Main, :main)
live_dashboard("/dashboard", metrics: ExSpeedGame.Web.Telemetry)
end
# Other scopes may use custom stacks.
# scope "/api", ExSpeedGame.Web do
# pipe_through :api
# end
end

46
lib/web/telemetry.ex Normal file
View file

@ -0,0 +1,46 @@
defmodule ExSpeedGame.Web.Telemetry do
use Supervisor
import Telemetry.Metrics
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
@impl true
def init(_arg) do
children = [
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
# Add reporters as children of your supervision tree.
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
]
Supervisor.init(children, strategy: :one_for_one)
end
def metrics do
[
# Phoenix Metrics
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),
# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io")
]
end
defp periodic_measurements do
[
# A module, function and arguments to be invoked periodically.
# This function must call :telemetry.execute/3 and a metric must be added above.
# {ExSpeedGame.Web, :count_users, []}
]
end
end

View file

@ -0,0 +1,11 @@
<main role="main" class="container">
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
<p class="alert alert-danger" role="alert"
phx-click="lv:clear-flash"
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
<%= @inner_content %>
</main>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "ExSpeedGame", suffix: " · Phoenix Framework" %>
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
<script defer type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</head>
<body>
<%= @inner_content %>
</body>
</html>

View file

@ -0,0 +1,16 @@
defmodule ExSpeedGame.Web.ErrorView do
use ExSpeedGame.Web, :view
# If you want to customize a particular status code
# for a certain format, you may uncomment below.
# def render("500.html", _assigns) do
# "Internal Server Error"
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.html" becomes
# "Not Found".
def template_not_found(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end

View file

@ -0,0 +1,3 @@
defmodule ExSpeedGame.Web.LayoutView do
use ExSpeedGame.Web, :view
end

98
lib/web/web.ex Normal file
View file

@ -0,0 +1,98 @@
defmodule ExSpeedGame.Web do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, views, channels and so on.
This can be used in your application as:
use ExSpeedGame.Web, :controller
use ExSpeedGame.Web, :view
The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below. Instead, define any helper function in modules
and import those modules here.
"""
def controller do
quote do
use Phoenix.Controller, namespace: ExSpeedGame.Web
import Plug.Conn
alias ExSpeedGame.Web.Router.Helpers, as: Routes
end
end
def view do
quote do
use Phoenix.View,
root: "lib/web/templates",
namespace: ExSpeedGame.Web
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]
# Include shared imports and aliases for views
unquote(view_helpers())
end
end
def live_view do
quote do
use Phoenix.LiveView,
layout: {ExSpeedGame.Web.LayoutView, "live.html"}
unquote(view_helpers())
end
end
def live_component do
quote do
use Phoenix.LiveComponent
unquote(view_helpers())
end
end
def router do
quote do
use Phoenix.Router
import Plug.Conn
import Phoenix.Controller
import Phoenix.LiveView.Router
end
end
def channel do
quote do
use Phoenix.Channel
import ExSpeedGame.Web.Gettext
end
end
defp view_helpers do
quote do
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
# Import LiveView helpers (live_render, live_component, live_patch, etc)
import Phoenix.LiveView.Helpers
# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View
alias ExSpeedGame.Web.Router.Helpers, as: Routes
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end

20
mix.exs
View file

@ -2,7 +2,7 @@ defmodule ExSpeedGame.MixProject do
use Mix.Project
@app :ex_speed_game
@version "0.1.0"
@version "0.2.0"
@all_targets [:rpi0]
def project do
@ -10,13 +10,14 @@ defmodule ExSpeedGame.MixProject do
app: @app,
version: @version,
elixir: "~> 1.10",
archives: [nerves_bootstrap: "~> 1.7"],
archives: [nerves_bootstrap: "~> 1.8"],
start_permanent: Mix.env() == :prod,
build_embedded: true,
aliases: [loadconfig: [&bootstrap/1]],
deps: deps(),
releases: [{@app, release()}],
preferred_cli_target: [run: :host, test: :host]
preferred_cli_target: [run: :host, test: :host],
compilers: [:phoenix] ++ Mix.compilers()
]
end
@ -31,7 +32,7 @@ defmodule ExSpeedGame.MixProject do
def application do
[
mod: {ExSpeedGame.Application, []},
extra_applications: [:logger, :runtime_tools]
extra_applications: [:logger, :runtime_tools, :os_mon]
]
end
@ -45,9 +46,20 @@ defmodule ExSpeedGame.MixProject do
{:toolshed, "~> 0.2"},
{:circuits_gpio, "~> 0.4"},
# Web UI stuff
{:phoenix, "~> 1.5.1"},
{:phoenix_live_view, "~> 0.12.0"},
{:phoenix_html, "~> 2.11"},
{:phoenix_live_dashboard, "~> 0.2.0"},
{:telemetry_metrics, "~> 0.4.0"},
{:telemetry_poller, "~> 0.4.0"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},
# Dependencies for all targets except :host
{:nerves_runtime, "~> 0.6", targets: @all_targets},
{:nerves_init_gadget, "~> 0.4", targets: @all_targets},
{:nerves_network, "~> 0.5", targets: @all_targets},
# Dependencies for specific targets
{:nerves_system_rpi0, "~> 1.10", runtime: false, targets: :rpi0}

View file

@ -1,8 +1,12 @@
%{
"circuits_gpio": {:hex, :circuits_gpio, "0.4.5", "4d5b0f707c425fc56f03086232259f65482a3d1f1cf15335253636d0bb846446", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "b42d28d60a6cfdfb6b21b66ab0b8c5de0ea5a32b390b61d2fe86a2ad8edb90ad"},
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
"dns": {:hex, :dns, "2.1.2", "81c46d39f7934f0e73368355126e4266762cf227ba61d5889635d83b2d64a493", [:mix], [{:socket, "~> 0.3.13", [hex: :socket, repo: "hexpm", optional: false]}], "hexpm", "6818589d8e59c03a2c73001e5cd7a957f99c30a796021aa32445ea14d0f3356b"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
"mdns": {:hex, :mdns, "1.0.3", "f08414daf5636bf5cd364611e838818e9250c91a3282a817ad9174b03e757401", [:mix], [{:dns, "~> 2.0", [hex: :dns, repo: "hexpm", optional: false]}], "hexpm", "6cb44eac9d1d71d0c5b400a383ccdc2b474d2e89a49a1e049e496637a6bab4c1"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"muontrap": {:hex, :muontrap, "0.5.1", "98fe96d0e616ee518860803a37a29eb23ffc2ca900047cb1bb7fd37521010093", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3c11b7f151b202148912c73cbdd633b76fa68fabc26cc441c9d6d140e22290dc"},
"nerves": {:hex, :nerves, "1.5.4", "d5a2a29a642e92277d5680f40d0fadff17796e75faa82de87ba0bc920ffcf818", [:mix], [{:distillery, "~> 2.1", [hex: :distillery, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "283ce855f369ff209f3358d25e58f1ac941d58aef3ce7e8cc0be9919d25bf4f5"},
"nerves_firmware_ssh": {:hex, :nerves_firmware_ssh, "0.4.4", "12b0d9c84ec9f79c1b0ac0de1c575372ef972d0c58ce21c36bf354062c6222d9", [:mix], [{:nerves_runtime, "~> 0.6", [hex: :nerves_runtime, repo: "hexpm", optional: false]}], "hexpm", "98c40104d0d2c6e6e8cce22f8c8fd8ad5b4b97f8694e42a9101ca44befac38f0"},
@ -27,10 +31,22 @@
"nerves_toolchain_x86_64_unknown_linux_musl": {:hex, :nerves_toolchain_x86_64_unknown_linux_musl, "1.2.0", "fbe688fa561b03190765e269d4336333c4961a48d2acd3f6cb283443a058e138", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.6.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm", "9cb676b7fb7a4564b18e1e16f72bc64f06f77346a477033fde6e4c2d1cc2ecff"},
"nerves_wpa_supplicant": {:hex, :nerves_wpa_supplicant, "0.5.2", "4ec392fc08faf35f50d1070446c2e5019f6b85bd53f5716f904e3f75716d9596", [:make, :mix], [{:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "c286ed5397fa185ab986226596eeb72d8f5f9f1b1156103418193f85905da713"},
"one_dhcpd": {:hex, :one_dhcpd, "0.2.4", "2664f2e1ac72cbae0474a88d1a3d55019ccc3ee582ded382589511627a8ed516", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "38f08c228d066153dbe352b150910345e395eacd2db3c19085d54c0baeeebacb"},
"phoenix": {:hex, :phoenix, "1.5.1", "95156589879dc69201d5fc0ebdbfdfc7901a09a3616ea611ec297f81340275a2", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc272b38e79d2881790fccae6f67a9fbe9b790103d6878175ea03d23003152eb"},
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.2.3", "aba80790b5358877c39e89d4e9a28e65817c22f01601c2eea64749e18e5c9b6e", [:mix], [{:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.12.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3871e507e2811022d5af80b45aa61ef2c20d3e7b0465b3a64d1669fffb92fd67"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.12.1", "42f591c781edbf9fab921319076b7ac635d43aa23e6748d2644563326236d7e4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.16 or ~> 1.5.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "585321e98df1cd5943e370b9784e950a37ca073744eb534660c9048967c52ab6"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"plug": {:hex, :plug, "1.10.1", "c56a6d9da7042d581159bcbaef873ba9d87f15dce85420b0d287bca19f40f9bd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b5cd52259817eb8a31f2454912ba1cff4990bca7811918878091cb2ab9e52cb8"},
"plug_cowboy": {:hex, :plug_cowboy, "2.2.1", "fcf58aa33227a4322a050e4783ee99c63c031a2e7f9a2eb7340d55505e17f30f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b43de24460d87c0971887286e7a20d40462e48eb7235954681a20cee25ddeb6"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"ring_logger": {:hex, :ring_logger, "0.8.0", "b1baddc269099b2afe2ea3a87b8e2b71e57331c0000038ae55090068aac679db", [:mix], [], "hexpm", "9b2f482e4346c13c11ef555f898202d0ddbfda6e2354e5c6e0559d2b4e0cf781"},
"shoehorn": {:hex, :shoehorn, "0.6.0", "f9a1b7d6212cf18ba91c4f71c26076059df33cea4db2eb3c098bfa6673349412", [:mix], [{:distillery, "~> 2.1", [hex: :distillery, repo: "hexpm", optional: true]}], "hexpm", "e54a1f58a121caf8f0f3a355686b2661258b1bc0d4fffef8923bd7b11c2f9d79"},
"socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm", "f82ea9833ef49dde272e6568ab8aac657a636acb4cf44a7de8a935acb8957c2e"},
"system_registry": {:hex, :system_registry, "0.8.2", "df791dc276652fcfb53be4dab823e05f8269b96ac57c26f86a67838dbc0eefe7", [:mix], [], "hexpm", "f7acdede22c73ab0b3735eead7f2095efb2a7a6198366564205274db2ca2a8f8"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.4.2", "1de986fad9aa6bf81f8a33ddfd16e5d8ab0dec6272e624eb517c1a92a44d41a9", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e56ffed2dbe293ab6cf7c94980faeb368cb360662c1927f54fc634a4ca55362e"},
"telemetry_poller": {:hex, :telemetry_poller, "0.4.1", "50d03d976a3b8ab4898d9e873852e688840df47685a13af90af40e1ba43a758b", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c5bacbbcd62c1fe4e4517485bd64312622e9b83683273dcf2627ff224d7d485b"},
"toolshed": {:hex, :toolshed, "0.2.11", "0cd5312bd6a48f5b654b6ffa9239a63af9f3d200da414790fe25f066e14842a9", [:mix], [{:nerves_runtime, "~> 0.8", [hex: :nerves_runtime, repo: "hexpm", optional: true]}], "hexpm", "f22ae95d77136f9f7db93cddd40d42bc8252d825f15a772a17d4c7947b6faad5"},
"uboot_env": {:hex, :uboot_env, "0.1.1", "b01e3ec0973e99473234f27839e29e63b5b81eba6a136a18a78d049d4813d6c5", [:mix], [], "hexpm", "f7b82da0cb40c8db9c9fb1fc977780ab0c28d961ec1f3c7ab265c4352e4141ae"},
}

272
priv/static/css/app.css Normal file

File diff suppressed because one or more lines are too long

4137
priv/static/js/app.js Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long