Update to Phoenix 1.5.

This commit is contained in:
James Harton 2020-04-28 21:09:54 +12:00
parent 9bd96f1120
commit 8381e2587c
52 changed files with 2299 additions and 1979 deletions

21
docker-compose.dev.yml Normal file
View file

@ -0,0 +1,21 @@
version: "3"
volumes:
history:
postgres_data:
node_modules:
elixir_deps:
elixir_build:
services:
webapp:
build:
context: ./webapp
dockerfile: Dockerfile.dev
volumes:
- node_modules:/app/assets/node_modules
- elixir_deps:/app/deps
- elixir_build:/app/_build
ports:
- 4000:4000
privileged: true

View file

@ -8,20 +8,17 @@ services:
build: ./webapp
network_mode: host
privileged: true
depends_on:
- firmware
environment:
HISTFILE: /history/bash_history
volumes:
- history:/bash_history
labels:
io.balena.features.supervisor-api: "1"
firmware:
build: ./firmware
privileged: true
environment:
HISTFILE: /history/bash_history
volumes:
- history:/bash_history
restart: on-failure
# firmware:
# build: ./firmware
# privileged: true
# environment:
# HISTFILE: /history/bash_history
# volumes:
# - history:/bash_history
# restart: on-failure

View file

@ -1,4 +1,4 @@
FROM python:latest AS firmware-builder
FROM balenalib/%%BALENA_MACHINE_NAME%%-debian-python:latest AS firmware-builder
RUN pip install -U platformio
RUN mkdir /firmware
WORKDIR /firmware

44
webapp/Dockerfile.dev Normal file
View file

@ -0,0 +1,44 @@
FROM ubuntu:latest
WORKDIR /tmp
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Pacific/Auckland
RUN apt-get update && \
apt-get -y --no-install-recommends install wget curl ca-certificates gnupg git build-essential && \
wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb && \
dpkg -i erlang-solutions_2.0_all.deb && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && \
apt-get -y --no-install-recommends install elixir erlang-dev erlang-parsetools inotify-tools nodejs yarn && \
rm -rf /var/lib/apt/lists/*
ENV LANG C.UTF-8
RUN mix local.hex --force
RUN mix local.rebar --force
WORKDIR /app
COPY assets /app/assets
COPY config /app/config
COPY lib /app/lib
COPY priv /app/priv
COPY rel /app/rel
COPY test /app/test
COPY mix.exs /app
COPY mix.lock /app
RUN mix deps.get
RUN mix deps.compile
RUN mix compile
WORKDIR /app/assets
RUN yarn install --ignore-optional --non-interactive --frozen-lockfile
WORKDIR /app
CMD mix deps.get && mix phx.server

View file

@ -1,4 +1,4 @@
FROM registry.gitlab.com/jimsy/balena-elixir/%%BALENA_ARCH%%-debian-build:1.10 as elixir
FROM registry.gitlab.com/jimsy/balena-elixir/%%BALENA_MACHINE_NAME%%-debian-build:1.10-latest as elixir
RUN install_packages libraspberrypi0 libraspberrypi-bin build-essential libraspberrypi-dev raspberrypi-kernel-headers
RUN mix local.hex --force
RUN mix local.rebar --force
@ -10,8 +10,7 @@ WORKDIR /app
RUN mix deps.get
RUN mix deps.compile
# FROM balenalib/%%BALENA_MACHINE_NAME%%-debian-node:latest-build as assets
FROM balenalib/amd64-debian-node:latest-build as assets
FROM balenalib/%%BALENA_MACHINE_NAME%%-debian-node:latest-build as assets
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
RUN install_packages yarn

View file

@ -2,8 +2,7 @@
To start your Phoenix server:
* Install dependencies with `mix deps.get`
* Install Node.js dependencies with `cd assets && yarn install`
* Setup the project with `mix setup`
* Start Phoenix endpoint with `mix phx.server`
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
@ -12,8 +11,8 @@ Ready to run in production? Please [check our deployment guides](https://hexdocs
## Learn more
* Official website: http://www.phoenixframework.org/
* Official website: https://www.phoenixframework.org/
* Guides: https://hexdocs.pm/phoenix/overview.html
* Docs: https://hexdocs.pm/phoenix
* Mailing list: http://groups.google.com/group/phoenix-talk
* Forum: https://elixirforum.com/c/phoenix-forum
* Source: https://github.com/phoenixframework/phoenix

View file

@ -5,4 +5,6 @@
@import "~foundation-sites/scss/foundation";
@include foundation-everything();
@include foundation-meter-element;
@import "sparkline.scss";
@import "../node_modules/nprogress/nprogress.css";

View file

@ -15,6 +15,7 @@ h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35}
h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5}
h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4}
h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2}
pre{padding: 1em;}
.container{
margin: 0 auto;
@ -27,53 +28,19 @@ select {
width: auto;
}
/* Alerts and form errors */
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-info {
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
}
.alert-warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
}
.alert-danger {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
.alert p {
margin-bottom: 0;
}
.alert:empty {
display: none;
}
.help-block {
color: #a94442;
display: block;
margin: -1rem 0 2rem;
}
/* Phoenix promo and logo */
.phx-hero {
text-align: center;
border-bottom: 1px solid #e3e3e3;
background: #eee;
border-radius: 6px;
padding: 3em;
padding: 3em 3em 1em;
margin-bottom: 3rem;
font-weight: 200;
font-size: 120%;
}
.phx-hero p {
margin: 0;
.phx-hero input {
background: #ffffff;
}
.phx-logo {
min-width: 300px;

View file

@ -1,31 +1,41 @@
// We need to import the CSS so that webpack will load it.
// The MiniCssExtractPlugin is used to separate it out into
// its own CSS file.
import css from "../css/app.scss"
import "../css/app.scss"
// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
//
// Import dependencies
// Import deps with the dep name or local files with a relative path, for example:
//
import "phoenix_html"
// Import local files
// import {Socket} from "phoenix"
// import socket from "./socket"
//
// Local files can be imported directly using relative paths, for example:
// import socket from "./socket"
import { Socket } from "phoenix"
import LiveSocket from "phoenix_live_view"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken } });
liveSocket.connect()
import jQuery from "jquery";
import jQuery from "jquery"
window.$ = jQuery;
import "what-input"
import "foundation-sites/js/foundation"
$(document).foundation();
import "phoenix_html"
import { Socket } from "phoenix"
import NProgress from "nprogress"
import { LiveSocket } from "phoenix_live_view"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken } })
// Show progress bar on live navigation and form submits
window.addEventListener("phx:page-loading-start", info => NProgress.start())
window.addEventListener("phx:page-loading-stop", info => NProgress.done())
// connect if there are any LiveViews on the page
liveSocket.connect()
// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)
window.liveSocket = liveSocket

View file

@ -1,30 +1,32 @@
{
"repository": {},
"description": " ",
"license": "MIT",
"scripts": {
"deploy": "webpack --mode production",
"watch": "webpack --mode development --watch"
},
"dependencies": {
"foundation-sites": "^6.5.3",
"jquery": "^3.4.1",
"foundation-sites": "^6.6.3",
"jquery": "^3.5.0",
"nprogress": "^0.2.0",
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view",
"what-input": "^5.2.6"
"what-input": "^5.2.7"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-loader": "^8.0.0",
"copy-webpack-plugin": "^5.0.5",
"css-loader": "^3.2.0",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.13.0",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.4.2",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"sass-loader": "^8.0.0",
"terser-webpack-plugin": "^2.2.1",
"webpack": "4.41.2",
"sass-loader": "^8.0.2",
"terser-webpack-plugin": "^2.3.2",
"webpack": "4.41.5",
"webpack-cli": "^3.3.2"
}
}

View file

@ -5,36 +5,47 @@ const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = (env, options) => ({
optimization: {
minimizer: [
new TerserPlugin({ cache: true, parallel: true, sourceMap: false }),
new OptimizeCSSAssetsPlugin({})
]
},
entry: {
'./js/app.js': glob.sync('./vendor/**/*.js').concat(['./js/app.js'])
},
output: {
filename: 'app.js',
path: path.resolve(__dirname, '../priv/static/js')
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
module.exports = (env, options) => {
const devMode = options.mode !== 'production';
return {
optimization: {
minimizer: [
new TerserPlugin({ cache: true, parallel: true, sourceMap: devMode }),
new OptimizeCSSAssetsPlugin({})
]
},
entry: {
'app': glob.sync('./vendor/**/*.js').concat(['./js/app.js'])
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../priv/static/js'),
publicPath: '/js/'
},
devtool: devMode ? 'source-map' : undefined,
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.[s]?css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
}
},
{
test: /\.s?css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin({ filename: '../css/app.css' }),
new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
]
},
plugins: [
new MiniCssExtractPlugin({ filename: '../css/app.css' }),
new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
]
});
}
};

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,7 @@ config :augie, AugieWeb.Endpoint,
url: [host: "localhost"],
secret_key_base: "QPhpYWd10BTjgc2nnA/hPn0a19UG6xpAm5fnnNUQh7u5UCG6uW3jz1PNM0p583TF",
render_errors: [view: AugieWeb.ErrorView, accepts: ~w(html json)],
pubsub: [name: Augie.PubSub, adapter: Phoenix.PubSub.PG2],
pubsub_server: Augie.PubSub,
live_view: [
signing_salt: "not-used-in-production"
]

View file

@ -51,7 +51,7 @@ config :augie, AugieWeb.Endpoint,
patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
~r"lib/augie_web/{live,views}/.*(ex)$",
~r"lib/augie_web/(live|views)/.*(ex)$",
~r"lib/augie_web/templates/.*(eex)$"
]
]

View file

@ -25,11 +25,11 @@ config :logger, level: :info
# ...
# url: [host: "example.com", port: 443],
# https: [
# :inet6,
# port: 443,
# cipher_suite: :strong,
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
# transport_options: [socket_opts: [:inet6]]
# ]
#
# The `cipher_suite` is set to `:strong` to support only the

View file

@ -12,9 +12,11 @@ secret_key_base =
"""
config :augie, AugieWeb.Endpoint,
http: [:inet6, port: String.to_integer(System.get_env("PORT") || "4000")],
secret_key_base: secret_key_base,
server: true
http: [
port: String.to_integer(System.get_env("PORT") || "4000"),
transport_options: [socket_opts: [:inet6]]
],
secret_key_base: secret_key_base
# ## Using releases (Elixir v1.9+)
#

View file

@ -7,12 +7,21 @@ defmodule Augie.Application do
def start(_type, _args) do
children = [
# Start the Telemetry supervisor
AugieWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Augie.PubSub},
# Start the Endpoint (http/https)
AugieWeb.Endpoint,
# Start a worker by calling: Augie.Worker.start_link(arg)
# {Augie.Worker, arg}
{Circuits.UART, [name: Circuits.UART]},
Augie.Telemetry,
Augie.SerialTelemetry,
Augie.Sensor.Camera
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Augie.Supervisor]
Supervisor.start_link(children, opts)
end

View file

@ -1,6 +1,10 @@
defmodule Augie.Sensor.Camera do
use GenServer
alias __MODULE__
alias Phoenix.PubSub
@moduledoc """
Read frames from the camera and deposit them on the "camera" pubsub topic.
"""
@default_width 640
@default_height 480
@ -17,18 +21,18 @@ defmodule Augie.Sensor.Camera do
GenServer.cast(self(), :get_frame)
Picam.set_size(width, height)
Picam.set_exposure_mode(:auto)
Picam.set_fps(24)
{:ok, %{width: width, height: height, camera: camera_pid, last_frame: ""}}
{:ok, %{width: width, height: height, camera: camera_pid, last_frame: "", from: nil}}
end
def handle_cast(:get_frame, %{camera: pid} = state) do
jpg = GenServer.call(pid, :next_frame)
CommunityTheatre.publish(Camera, jpg)
PubSub.broadcast(Augie.PubSub, "camera", {:frame, jpg})
GenServer.cast(self(), :get_frame)
# Don't do more than about 24 frames per second
Process.sleep(40)
{:noreply, %{state | last_frame: jpg}}
end

View file

@ -1,7 +1,8 @@
defmodule Augie.Telemetry do
defmodule Augie.SerialTelemetry do
use GenServer
alias Circuits.UART
alias Augie.TelemetryParser
alias Circuits.UART
alias Phoenix.PubSub
require Logger
@moduledoc """
@ -9,7 +10,7 @@ defmodule Augie.Telemetry do
"""
@baud_rate 115_200
@retry_timeout 10_000
@retry_timeout 1_000
@doc false
def start_link(_opts), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
@ -18,25 +19,22 @@ defmodule Augie.Telemetry do
@impl true
def init(_) do
case connect() do
{:ok, uart} ->
{:ok, uart}
{:error, _reason} ->
{:ok, nil, @retry_timeout}
end
PubSub.broadcast(Augie.PubSub, "serial_telemetry", {:connected, false})
send(self(), :try_connect)
{:ok, %{uart: nil, warned: false}}
end
@impl true
def handle_call(:connected?, _from, nil), do: {:reply, false, nil}
def handle_call(:connected?, _from, uart), do: {:reply, true, uart}
def handle_call(:connected?, _from, %{uart: nil} = state), do: {:reply, false, state}
def handle_call(:connected?, _from, state), do: {:reply, true, state}
@impl true
def handle_info({:circuits_uart, _dev, message}, state) do
with {:ok, {module, data}} <- TelemetryParser.parse(message),
with {:ok, {name, data}} <- TelemetryParser.parse(message),
module <- Module.concat([Augie.Sensor, name]),
true <- function_exported?(module, :build, 1),
{:ok, data} <- apply(module, :build, [data]) do
CommunityTheatre.publish(module, data)
PubSub.broadcast(Augie.PubSub, name, data)
else
{:error, reason} -> Logger.warn("Failed to parse telemetry: #{reason}: #{inspect(message)}")
false -> Logger.warn("Unhandled telemetry message: #{inspect(message)}")
@ -45,15 +43,21 @@ defmodule Augie.Telemetry do
{:noreply, state}
end
def handle_info(:timeout, nil) do
def handle_info(:try_connect, %{uart: nil} = state) do
case connect() do
{:ok, uart} -> {:noreply, uart}
{:error, _reason} -> {:noreply, nil, @retry_timeout}
end
end
{:ok, uart} ->
{:noreply, %{state | uart: uart}}
def handle_info(:timeout, uart) when not is_nil(uart) do
{:noreply, uart}
{:error, :no_teensy_detected} ->
unless state.warned, do: Logger.warn("No Teensy detected")
Process.send_after(self(), :try_connect, @retry_timeout)
{:noreply, %{state | warned: true}}
{:error, reason} ->
Logger.warn("Error connecting to Teensy: #{inspect(reason)}")
Process.send_after(self(), :try_connect, @retry_timeout)
{:noreply, state}
end
end
# Look for a serial device that is made by the lovely folks at PJRC.
@ -78,14 +82,14 @@ defmodule Augie.Telemetry do
) do
Logger.info("Connected to Teensy on #{uart}")
PubSub.broadcast(Augie.PubSub, "serial_telemetry", {:connected, uart})
{:ok, uart}
else
nil ->
Logger.warn("No Teensy detected")
{:error, "No teensy detected"}
{:error, :no_teensy_detected}
{:error, reason} ->
Logger.warn("Error connecting to Teensy UART: #{inspect(reason)}")
{:error, reason}
end
end

View file

@ -12,7 +12,7 @@ defmodule Augie.TelemetryParser do
def parse(input) do
case parse_telemetry_line(input) do
{:ok, [{:sensor_name, name} | rest], _, _, _, _} ->
{:ok, {Module.concat(["Augie.Sensor", name]), reduce_literals(rest)}}
{:ok, {name, reduce_literals(rest)}}
{:error, reason, _, _, _, _} ->
{:error, reason}

View file

@ -1,4 +1,6 @@
defmodule Augie.Utils do
@moduledoc false
def pad(data, length, _pad) when is_list(data) and length(data) == length, do: data
def pad(data, length, pad) when is_list(data) and length(data) < length,

View file

@ -37,13 +37,8 @@ defmodule AugieWeb do
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
import AugieWeb.ErrorHelpers
import AugieWeb.Gettext
alias AugieWeb.Router.Helpers, as: Routes
# Include shared imports and aliases for views
unquote(view_helpers())
import Phoenix.LiveView.Helpers
end
end
@ -51,6 +46,7 @@ defmodule AugieWeb do
def router do
quote do
use Phoenix.Router
import Plug.Conn
import Phoenix.Controller
import Phoenix.LiveView.Router
@ -64,6 +60,20 @@ defmodule AugieWeb do
end
end
defp view_helpers do
quote do
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View
import AugieWeb.ErrorHelpers
import AugieWeb.Gettext
alias AugieWeb.Router.Helpers, as: Routes
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""

View file

@ -0,0 +1,82 @@
defmodule AugieWeb.CameraStream do
use GenServer
alias Phoenix.PubSub
import Plug.Conn
@moduledoc """
Subscribes to updated frames from the Camera.
Sends them over the `conn` at a maximum of around 24fps, dropping skipped
frames.
"""
# Maximum frame rate is around 24fps.
@frame_delay 40
def start_link, do: GenServer.start_link(__MODULE__, [])
@impl true
def init(_), do: {:ok, %{boundary: generate_boundary(), conn: nil, last_frame: nil, from: nil}}
@doc """
Call the camera streamer and ask it to start streaming to this `conn`.
The server doesn't actually reply to the call until the `terminate/2` callback
is called.
"""
@spec stream(GenServer.server(), Plug.Conn.t()) :: Plug.Conn.t()
def stream(server, conn), do: GenServer.call(server, {:start_stream, conn}, :infinity)
@impl true
def handle_call({:start_stream, conn}, from, %{conn: nil, boundary: boundary} = state) do
conn =
conn
|> put_resp_header("Age", "0")
|> put_resp_header("Cache-Control", "no-cache, private")
|> put_resp_header("Pragma", "no-cache")
|> put_resp_header("Content-Type", "multipart/x-mixed-replace; boundary=#{boundary}")
|> send_chunked(200)
PubSub.subscribe(Augie.PubSub, "camera")
Process.send_after(self(), :send_frame, @frame_delay)
{:noreply, %{state | from: from, conn: conn}}
end
@impl true
def handle_info({:frame, frame}, state), do: {:noreply, %{state | last_frame: frame}}
def handle_info(:send_frame, %{last_frame: nil} = state) do
Process.send_after(self(), :send_frame, @frame_delay)
{:noreply, state}
end
def handle_info(
:send_frame,
%{last_frame: frame, conn: conn, boundary: boundary} = state
) do
Process.send_after(self(), :send_frame, @frame_delay)
size = byte_size(frame)
header = "------#{boundary}\r\nContent-Type: image/jpeg\r\nContent-length: #{size}\r\n\r\n"
footer = "\r\n"
conn =
with {:ok, conn} <- chunk(conn, header),
{:ok, conn} <- chunk(conn, frame),
{:ok, conn} <- chunk(conn, footer),
do: conn
Process.send_after(self(), :send_frame, 40)
{:noreply, %{state | conn: conn, last_frame: nil}}
end
@impl true
def terminate(_reason, %{conn: conn, from: from}) do
GenServer.reply(from, conn)
end
defp generate_boundary do
:crypto.strong_rand_bytes(16) |> Base.encode64() |> binary_part(0, 16)
end
end

View file

@ -15,6 +15,7 @@ defmodule AugieWeb.UserSocket do
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
@impl true
def connect(_params, socket, _connect_info) do
{:ok, socket}
end
@ -29,5 +30,6 @@ defmodule AugieWeb.UserSocket do
# AugieWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
@impl true
def id(_socket), do: nil
end

View file

@ -1,102 +0,0 @@
defmodule AugieWeb.SparklineComponent do
use Phoenix.LiveComponent
import Augie.Utils
@default_width 325
@default_height 24
@default_pad 0
def render(assigns) do
~L"""
<svg width="<%= width_from(@socket) %>" height="<%= height_from(@socket) %>" viewBox="0 0 <%= width_from(@socket) %> <%= height_from(@socket) %>" class="sparkline">
<%= if fill?(@socket) do %>
<polygon points="
<%= for {datum, index} <- Enum.with_index(padded_data(@socket)) do %>
<%= if index == 0 do %>
<%= scale_x(index, @socket) %>,<%= height_from(@socket) %>
<% end %>
<%= scale_x(index, @socket) %>,<%= scale_y(datum, @socket) %>
<% end %>
<%= width_from(@socket) %>,<%= height_from(@socket) %>
" class="sparkline--filled-area" />
<% end %>
<path d="
<%= for {datum, index} <- Enum.with_index(padded_data(@socket)) do %>
<%= if index == 0 do %>
M <%= scale_x(index, @socket) %> <%= scale_y(datum, @socket) %>
<% else %>
L <%= scale_x(index, @socket) %> <%= scale_y(datum, @socket) %>
<% end %>
<% end %>
" class="sparkline--line">
</svg>
"""
end
defp fill?(%{assigns: %{fill: false}}), do: false
defp fill?(socket) do
Enum.count(data(socket)) > 1
end
defp width_from(%{assigns: %{width: value}}), do: value
defp width_from(_socket), do: @default_width
defp height_from(%{assigns: %{height: value}}), do: value
defp height_from(_socket), do: @default_height
defp sample_count(%{assigns: %{sample_count: value}}), do: value
defp sample_count(%{assigns: %{data: value}}), do: Enum.count(value)
defp pad_with(%{assigns: %{pad_with: value}}), do: value
defp pad_with(_socket), do: @default_pad
defp data(%{assigns: %{data: value}}), do: value
defp data(_socket), do: []
defp padded_data(socket) do
data = data(socket)
count = sample_count(socket)
pad_with = pad_with(socket)
pad(data, count, pad_with)
end
defp min_from(%{assigns: %{min: value}}), do: value
defp min_from(%{assigns: %{data: data}}), do: Enum.min(data)
defp max_from(%{assigns: %{max: value}}), do: value
defp max_from(%{assigns: %{data: data}}), do: Enum.max(data)
defp safe_min(i, min) when i >= min, do: i
defp safe_min(i, min) when i < min, do: min
defp scale_x(index, socket) do
width = width_from(socket)
count = sample_count(socket)
sample_width = width / safe_min(count - 1, 1)
sample_width * index
end
defp scale_y(datum, socket) do
min = min_from(socket)
max = max_from(socket)
scale_y(datum, socket, min, max)
end
defp scale_y(datum, socket, min, max) when min < 0 do
abs_min = abs(min)
scale_y(datum + abs_min, socket, 0, max + abs_min)
end
defp scale_y(datum, socket, min, max) when max - min <= 0 do
scale_y(datum, socket, 0, 1)
end
defp scale_y(datum, socket, min, max) do
height = height_from(socket)
range = max - min
per_pixel = height / range
# IO.inspect(%{height: height, range: range, per_pixel: per_pixel})
height - per_pixel * datum
end
end

View file

@ -0,0 +1,23 @@
defmodule AugieWeb.CameraController do
alias Augie.Sensor.Camera
alias AugieWeb.CameraStream
use AugieWeb, :controller
def stream(conn, _params) do
{:ok, pid} = CameraStream.start_link()
CameraStream.stream(pid, conn)
end
def static(conn, _params) do
frame = Camera.last_frame(Camera)
size = byte_size(frame)
conn
|> put_resp_header("Age", "0")
|> put_resp_header("Cache-Control", "no-cache, private")
|> put_resp_header("Pragma", "no-cache")
|> put_resp_header("Content-Type", "image/jpeg")
|> put_resp_header("Content-Length", "#{size}")
|> resp(200, frame)
end
end

View file

@ -1,50 +1,53 @@
defmodule AugieWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :augie
@session_options [store: :cookie, key: "_augie_key", signing_salt: "g3Ev0ToK"]
# 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: "_augie_key",
signing_salt: "UrcJoQua"
]
socket("/socket", AugieWeb.UserSocket,
socket "/socket", AugieWeb.UserSocket,
websocket: true,
longpoll: false
)
socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
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,
plug Plug.Static,
at: "/",
from: :augie,
gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket)
plug(Phoenix.LiveReloader)
plug(Phoenix.CodeReloader)
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
end
plug(Plug.RequestId)
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
plug Phoenix.LiveDashboard.RequestLogger,
param_key: "request_logger",
cookie_key: "request_logger"
plug(Plug.Parsers,
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)
# 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.
plug(Plug.Session, @session_options)
plug(AugieWeb.Router)
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug AugieWeb.Router
end

View file

@ -1,6 +1,5 @@
defmodule AugieWeb.CameraLive do
use Phoenix.LiveView
alias Augie.Sensor.Camera
@moduledoc """
A Liveview which streams images from the raspberry pi camera.
@ -12,27 +11,19 @@ defmodule AugieWeb.CameraLive do
<div class="card-divider">
<h4>Camera</h4>
</div>
<img src="<%= @frame %>">
<img src="<%= @img_src %>">
</div>
"""
end
def mount(_params, _context, socket) do
if connected?(socket), do: CommunityTheatre.subscribe(Camera, 4)
socket =
if connected?(socket) do
assign(socket, img_src: "/camera/stream")
else
assign(socket, img_src: "/camera/static")
end
jpg = Camera.last_frame(Camera)
{:ok, assign(socket, frame: encode_frame(jpg))}
end
def handle_info({CommunityTheatre, %{topic: Camera, payload: jpg}}, socket) do
{:noreply, assign(socket, frame: encode_frame(jpg))}
end
defp encode_frame(jpg) do
[
"data:image/jpeg;base64,",
Base.encode64(jpg)
]
{:ok, socket}
end
end

View file

@ -1,66 +1,25 @@
defmodule AugieWeb.DashboardLive do
use Phoenix.LiveView
alias Augie.Telemetry
use Phoenix.LiveView, layout: {AugieWeb.LayoutView, "live.html"}
alias Augie.SerialTelemetry
alias Phoenix.PubSub
@moduledoc """
A live view which shows or hides the dashboard based on whether the Teensy USB
serial connection was found.
"""
def render(assigns) do
~L"""
<%= if @connected do %>
<div class="grid-x grid-padding-x">
<div class="cell small-1 large-4">
<%= live_render(@socket, AugieWeb.IMUSensorLive, id: :imu) %>
</div>
<div class="cell small-1 large-4">
<%= live_render(@socket, AugieWeb.PowerSensorLive, id: :power) %>
<%= live_render(@socket, AugieWeb.GPSSensorLive, id: :gps) %>
</div>
<div class="cell small-1 large-4">
<%= live_render(@socket, AugieWeb.CameraLive, id: :camera) %>
<%= live_render(@socket, AugieWeb.LoggerLive, id: :logger) %>
</div>
</div>
<% else %>
<div class="callout alert">
<h5> No Teensy USB Connection Found</h5>
<%= if Enum.any?(@uarts) do %>
<p>
Found the following UART devices:
<ul>
<%= for {device, properties} <- @uarts do %>
<li>
<code><%= device %></code>
<ul>
<%= for {name, value} <- properties do %>
<%= name %> &mdash; <%= value %>
<% end %>
</ul>
</li>
<% end %>
</ul>
</p>
<% end %>
</div>
<% end %>
"""
end
def mount(_params, _context, socket) do
if connected?(socket), do: :timer.send_interval(1000, :tick)
if connected?(socket), do: PubSub.subscribe(Augie.PubSub, "serial_telemetry")
{:ok, detect(socket)}
{:ok,
assign(socket, connected: SerialTelemetry.connected?(), uarts: Circuits.UART.enumerate())}
end
def handle_info(:tick, socket) do
{:noreply, detect(socket)}
def handle_info({:connected, false}, socket) do
{:noreply, assign(socket, connected: false, uarts: Circuits.UART.enumerate())}
end
defp detect(socket) do
assign(socket, connected: Telemetry.connected?(), uarts: Circuits.UART.enumerate())
def handle_info({:connected, _}, socket) do
{:noreply, assign(socket, connected: true, uarts: [])}
end
end

View file

@ -0,0 +1,38 @@
<div class="grid-x grid-padding-x grid-padding-y">
<div class="cell small-12 medium-6 large-4">
<%= unless @connected do %>
<div class="cell">
<div class="callout alert">
<h5>⚠️ No Teensy USB Connection Found</h5>
<%= if Enum.any?(@uarts) do %>
<p>
Found the following UART devices:
<ul>
<%= for {device, properties} <- @uarts do %>
<li>
<code><%= device %></code>
<ul>
<%= for {name, value} <- properties do %>
<%= name %> &mdash; <%= value %>
<% end %>
</ul>
</li>
<% end %>
</ul>
</p>
<% end %>
</div>
</div>
<% end %>
<%= live_render(@socket, AugieWeb.OrientationLive, id: :orientation) %>
<%= live_render(@socket, AugieWeb.TemperatureLive, id: :temperature) %>
</div>
<div class="cell small-12 medium-6 large-4">
<%= live_render(@socket, AugieWeb.GpsLive, id: :gps) %>
</div>
<div class="cell small-12 medium-6 large-4">
<%= live_render(@socket, AugieWeb.CameraLive, id: :camera) %>
</div>
</div>

View file

@ -0,0 +1,69 @@
defmodule AugieWeb.GpsLive do
use Phoenix.LiveView
alias Augie.Sensor.GPS
alias Phoenix.PubSub
@moduledoc """
A LiveView with displaus the data from the GPS.
"""
@empty_sample %{
status: nil,
latitude: nil,
longitude: nil,
altitude: nil,
heading: nil,
speed: nil,
satellites: nil
}
@map_base_uri "https://maps.googleapis.com/maps/api/staticmap"
@map_opts [size: "640x320", key: "AIzaSyBibH_1Yibm3gshxsQDUKw7mjaH9SyMrgw", maptype: "hybrid"]
def mount(_params, _context, socket) do
if connected?(socket), do: PubSub.subscribe(Augie.PubSub, "GPS")
socket =
socket
|> assign(sample_to_assigns(@empty_sample))
|> assign(map_src: map_url(zoom: 1))
{:ok, socket}
end
def handle_info(%GPS{} = sample, socket) do
socket =
socket
|> assign(sample_to_assigns(sample))
|> assign(map_url(sample_to_map_opts(sample)))
{:noreply, socket}
end
defp sample_to_assigns(sample) do
sample
|> Enum.map(fn
{key, nil} -> {key, "-"}
{key, value} when is_float(value) -> {key, Float.round(value, 2)}
{key, value} -> {key, value}
end)
end
defp sample_to_map_opts(sample) do
location = "#{sample.latitude},#{sample.longitude}"
[
center: location,
marker: "color:red|#{location}",
zoom: 15
]
end
defp map_url(opts) do
query =
@map_opts
|> Keyword.merge(opts)
|> URI.encode_query()
"#{@map_base_uri}?#{query}"
end
end

View file

@ -0,0 +1,36 @@
<div class="card">
<div class="card-divider">
<h4>GPS</h4>
</div>
<img src="<%= @map_src %>">
<div class="card-section">
<div class="grid-x">
<div class="cell auto"><strong>Status</strong></div>
<div class="cell auto text-right"><%= @status %></div>
</div>
<div class="grid-x">
<div class="cell auto"><strong>Latitude</strong></div>
<div class="cell auto text-right"><%= @latitude %></div>
</div>
<div class="grid-x">
<div class="cell auto"><strong>Longitude</strong></div>
<div class="cell auto text-right"><%= @longitude %></div>
</div>
<div class="grid-x">
<div class="cell auto"><strong>Altitude</strong></div>
<div class="cell auto text-right"><%= @altitude %></div>
</div>
<div class="grid-x">
<div class="cell auto"><strong>Heading</strong></div>
<div class="cell auto text-right"><%= @heading %></div>
</div>
<div class="grid-x">
<div class="cell auto"><strong>Speed</strong></div>
<div class="cell auto text-right"><%= @speed %></div>
</div>
<div class="grid-x">
<div class="cell auto"><strong>Satellites</strong></div>
<div class="cell auto text-right"><%= @satellites %></div>
</div>
</div>
</div>

View file

@ -1,123 +0,0 @@
defmodule AugieWeb.GPSSensorLive do
use Phoenix.LiveView
alias Augie.Sensor.GPS
@moduledoc """
A LiveView with displaus the data from the GPS.
"""
@sample_count 60
def render(assigns) do
~L"""
<div class="card">
<div class="card-divider">
<h4>GPS</h4>
</div>
<%= if @data_ready do %>
<div class="card-section">
<div class="grid-x">
<div class="cell auto"><strong>Status</strong></div>
<div class="cell auto text-right"><%= @status %></div>
</div>
<div class="grid-x">
<div class="cell auto"><strong>Latitude</strong></div>
<div class="cell auto text-right"><%= Float.round(CircularBuffer.newest(@latitude), 4) %>º</div>
</div>
<div class="grid-x">
<div class="cell"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @latitude, sample_count: 60, fill: false, min: -90, max: 90) %></div>
</div>
<div class="grid-x">
<div class="cell auto"><strong>Longitude</strong></div>
<div class="cell auto text-right"><%= Float.round(CircularBuffer.newest(@longitude), 4) %>º</div>
</div>
<div class="grid-x">
<div class="cell"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @longitude, sample_count: 60, fill: false, min: 0, max: 180) %></div>
</div>
<div class="grid-x">
<div class="cell auto"><strong>Altitude</strong></div>
<div class="cell auto text-right"><%= Float.round(CircularBuffer.newest(@altitude), 4) %>m</div>
</div>
<div class="grid-x">
<div class="cell"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @altitude, sample_count: 60, fill: false, min: 0) %></div>
</div>
<div class="grid-x">
<div class="cell auto"><strong>Heading</strong></div>
<div class="cell auto text-right"><%= Float.round(CircularBuffer.newest(@heading), 4) %>º</div>
</div>
<div class="grid-x">
<div class="cell"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @heading, sample_count: 60, fill: false, min: 0, max: 360) %></div>
</div>
<div class="grid-x">
<div class="cell auto"><strong>Speed</strong></div>
<div class="cell auto text-right"><%= Float.round(CircularBuffer.newest(@speed), 4) %>m/s</div>
</div>
<div class="grid-x">
<div class="cell"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @speed, sample_count: 60, fill: false, min: 0) %></div>
</div>
<div class="grid-x">
<div class="cell auto"><strong>Satellites</strong></div>
<div class="cell auto text-right"><%= CircularBuffer.newest(@satellites) %></div>
</div>
<div class="grid-x">
<div class="cell"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @satellites, sample_count: 60, fill: false, min: 0) %></div>
</div>
</div>
<% else %>
<div class="card-section">
No data available.
</div>
<% end %>
</div>
"""
end
def mount(_params, _context, socket) do
if connected?(socket),
do: CommunityTheatre.subscribe(GPS, 1)
socket =
socket
|> assign(
data_ready: false,
latitude: CircularBuffer.new(@sample_count),
longitude: CircularBuffer.new(@sample_count),
altitude: CircularBuffer.new(@sample_count),
heading: CircularBuffer.new(@sample_count),
speed: CircularBuffer.new(@sample_count),
satellites: CircularBuffer.new(@sample_count),
status: :none
)
{:ok, socket}
end
def handle_info(
{CommunityTheatre, %{topic: GPS, payload: sample}},
%{
assigns: %{
latitude: latitude,
longitude: longitude,
altitude: altitude,
heading: heading,
speed: speed,
satellites: satellites
}
} = socket
) do
socket =
socket
|> assign(
data_ready: true,
latitude: CircularBuffer.insert(latitude, sample.latitude),
longitude: CircularBuffer.insert(longitude, sample.longitude),
altitude: CircularBuffer.insert(altitude, sample.altitude),
heading: CircularBuffer.insert(heading, sample.heading),
speed: CircularBuffer.insert(speed, sample.speed),
satellites: CircularBuffer.insert(satellites, sample.satellites),
status: sample.status
)
{:noreply, socket}
end
end

View file

@ -1,257 +0,0 @@
defmodule AugieWeb.IMUSensorLive do
use Phoenix.LiveView
alias Augie.Sensor.IMU
@moduledoc """
A Liveview which displays the data from the IMU.
"""
@sample_count 60
def render(assigns) do
~L"""
<div class="card">
<div class="card-divider">
<h4>IMU</h4>
</div>
<%= if @data_ready do %>
<div class="card-section">
<h5>Orientation</h5>
<div class="grid-x">
<div class="cell auto shrink"><strong>X</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @orientation_x, min: -1, max: 1, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>Y</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @orientation_y, min: -1, max: 1, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>Z</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @orientation_z, min: -1, max: 1, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>W</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @orientation_w, min: -1, max: 1, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
</div>
<div class="card-section">
<h5>Accelerometer</h5>
<div class="grid-x">
<div class="cell auto shrink"><strong>X</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @accelerometer_x, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>Y</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @accelerometer_y, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>Z</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @accelerometer_z, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
</div>
<div class="card-section">
<h5>Magnetometer</h5>
<div class="grid-x">
<div class="cell auto shrink"><strong>X</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @magnetometer_x, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>Y</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @magnetometer_y, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>Z</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @magnetometer_z, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
</div>
<div class="card-section">
<h5>Gyroscope</h5>
<div class="grid-x">
<div class="cell auto shrink"><strong>X</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @gyroscope_x, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>Y</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @gyroscope_y, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>Z</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @gyroscope_z, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
</div>
<div class="card-section">
<h5>Gravity</h5>
<div class="grid-x">
<div class="cell auto shrink"><strong>X</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @gravity_x, fill: false, height: 20, sample_count: @sample_count, min: -10, max: 10) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>Y</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @gravity_y, fill: false, height: 20, sample_count: @sample_count, min: -10, max: 10) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>Z</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @gravity_z, fill: false, height: 20, sample_count: @sample_count, min: -10, max: 10) %></div>
</div>
</div>
<div class="card-section">
<h5>Linear Acceleration</h5>
<div class="grid-x">
<div class="cell auto shrink"><strong>X</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @linear_acceleration_x, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>Y</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @linear_acceleration_y, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
<div class="grid-x">
<div class="cell auto shrink"><strong>Z</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @linear_acceleration_z, fill: false, height: 20, sample_count: @sample_count) %></div>
</div>
</div>
<div class="card-section">
<h5>Temperature</h5>
<div class="grid-x">
<div class="cell auto shrink"><strong><%= CircularBuffer.newest(@temperature) %>ºC</strong>&nbsp;</div>
<div class="cell auto"><%= live_component(@socket, AugieWeb.SparklineComponent, data: @temperature, height: 20, sample_count: @sample_count, min: 0, max: 100) %></div>
</div>
</div>
<% else %>
<div class="card-section">
No data available.
</div>
<% end %>
</div>
"""
end
def mount(_params, _context, socket) do
if connected?(socket),
do: CommunityTheatre.subscribe(IMU, 2)
socket =
socket
|> assign(
data_ready: false,
sample_count: @sample_count,
orientation_x: CircularBuffer.new(@sample_count),
orientation_y: CircularBuffer.new(@sample_count),
orientation_z: CircularBuffer.new(@sample_count),
orientation_w: CircularBuffer.new(@sample_count),
accelerometer_x: CircularBuffer.new(@sample_count),
accelerometer_y: CircularBuffer.new(@sample_count),
accelerometer_z: CircularBuffer.new(@sample_count),
magnetometer_x: CircularBuffer.new(@sample_count),
magnetometer_y: CircularBuffer.new(@sample_count),
magnetometer_z: CircularBuffer.new(@sample_count),
gyroscope_x: CircularBuffer.new(@sample_count),
gyroscope_y: CircularBuffer.new(@sample_count),
gyroscope_z: CircularBuffer.new(@sample_count),
gravity_x: CircularBuffer.new(@sample_count),
gravity_y: CircularBuffer.new(@sample_count),
gravity_z: CircularBuffer.new(@sample_count),
linear_acceleration_x: CircularBuffer.new(@sample_count),
linear_acceleration_y: CircularBuffer.new(@sample_count),
linear_acceleration_z: CircularBuffer.new(@sample_count),
temperature: CircularBuffer.new(@sample_count)
)
{:ok, socket}
end
def handle_info(
{CommunityTheatre, %{topic: IMU, payload: sample}},
%{
assigns: %{
orientation_x: orientation_x,
orientation_y: orientation_y,
orientation_z: orientation_z,
orientation_w: orientation_w,
accelerometer_x: accelerometer_x,
accelerometer_y: accelerometer_y,
accelerometer_z: accelerometer_z,
magnetometer_x: magnetometer_x,
magnetometer_y: magnetometer_y,
magnetometer_z: magnetometer_z,
gyroscope_x: gyroscope_x,
gyroscope_y: gyroscope_y,
gyroscope_z: gyroscope_z,
gravity_x: gravity_x,
gravity_y: gravity_y,
gravity_z: gravity_z,
linear_acceleration_x: linear_acceleration_x,
linear_acceleration_y: linear_acceleration_y,
linear_acceleration_z: linear_acceleration_z,
temperature: temperature
}
} = socket
) do
orientation_x = orientation_x |> CircularBuffer.insert(sample.orientation.x)
orientation_y = orientation_y |> CircularBuffer.insert(sample.orientation.y)
orientation_z = orientation_z |> CircularBuffer.insert(sample.orientation.z)
orientation_w = orientation_w |> CircularBuffer.insert(sample.orientation.w)
accelerometer_x = accelerometer_x |> CircularBuffer.insert(sample.accelerometer.x)
accelerometer_y = accelerometer_y |> CircularBuffer.insert(sample.accelerometer.y)
accelerometer_z = accelerometer_z |> CircularBuffer.insert(sample.accelerometer.z)
magnetometer_x = magnetometer_x |> CircularBuffer.insert(sample.magnetometer.x)
magnetometer_y = magnetometer_y |> CircularBuffer.insert(sample.magnetometer.y)
magnetometer_z = magnetometer_z |> CircularBuffer.insert(sample.magnetometer.z)
gyroscope_x = gyroscope_x |> CircularBuffer.insert(sample.gyroscope.x)
gyroscope_y = gyroscope_y |> CircularBuffer.insert(sample.gyroscope.y)
gyroscope_z = gyroscope_z |> CircularBuffer.insert(sample.gyroscope.z)
gravity_x = gravity_x |> CircularBuffer.insert(sample.gravity.x)
gravity_y = gravity_y |> CircularBuffer.insert(sample.gravity.y)
gravity_z = gravity_z |> CircularBuffer.insert(sample.gravity.z)
linear_acceleration_x =
linear_acceleration_x |> CircularBuffer.insert(sample.linear_acceleration.x)
linear_acceleration_y =
linear_acceleration_y |> CircularBuffer.insert(sample.linear_acceleration.y)
linear_acceleration_z =
linear_acceleration_z |> CircularBuffer.insert(sample.linear_acceleration.z)
temperature = temperature |> CircularBuffer.insert(sample.temperature)
socket =
socket
|> assign(
data_ready: true,
orientation_x: orientation_x,
orientation_y: orientation_y,
orientation_z: orientation_z,
orientation_w: orientation_w,
accelerometer_x: accelerometer_x,
accelerometer_y: accelerometer_y,
accelerometer_z: accelerometer_z,
magnetometer_x: magnetometer_x,
magnetometer_y: magnetometer_y,
magnetometer_z: magnetometer_z,
gyroscope_x: gyroscope_x,
gyroscope_y: gyroscope_y,
gyroscope_z: gyroscope_z,
gravity_x: gravity_x,
gravity_y: gravity_y,
gravity_z: gravity_z,
linear_acceleration_x: linear_acceleration_x,
linear_acceleration_y: linear_acceleration_y,
linear_acceleration_z: linear_acceleration_z,
temperature: temperature
)
{:noreply, socket}
end
end

View file

@ -0,0 +1,48 @@
defmodule AugieWeb.OrientationLive do
use Phoenix.LiveView
alias Augie.Sensor.IMU
alias Kinemat.Orientation
alias Kinemat.Orientations.Quaternion
alias Phoenix.PubSub
use Angle
@moduledoc false
def mount(_params, _context, socket) do
if connected?(socket), do: PubSub.subscribe(Augie.PubSub, "IMU")
{:ok, assign(socket, data_ready: false)}
end
def handle_info(%IMU{} = sample, socket) do
euler =
Quaternion.init(
~a[#{sample.orientation.w}]r,
sample.orientation.x,
sample.orientation.y,
sample.orientation.z
)
|> Orientation.to_euler()
orientation = [
roll: to_degrees(euler.x),
pitch: to_degrees(euler.y),
yaw: to_degrees(euler.z)
]
socket =
socket
|> assign(data_ready: true)
|> assign(orientation)
{:noreply, socket}
end
defp to_degrees(angle) do
case Angle.to_degrees(angle) do
{_, degrees} when is_integer(degrees) -> degrees / 1.0
{_, degrees} when is_float(degrees) -> degrees
{_, _} -> 0.0
end
end
end

View file

@ -0,0 +1,23 @@
<div class="card">
<div class="card-divider">
<h4>Orientation</h4>
</div>
<%= if @data_ready do %>
<div class="card-section">
<div class="grid-x">
<div class="cell small-6"><strong>Pitch</strong></div>
<div class="cell small-6 text-right"><%= Float.round(@pitch, 2) %>&deg;</div>
</div>
<div class="grid-x">
<div class="cell small-6"><strong>Roll</strong></div>
<div class="cell small-6 text-right"><%= Float.round(@roll, 2) %>&deg;</div>
</div>
<div class="grid-x">
<div class="cell small-6"><strong>Yaw</strong></div>
<div class="cell small-6 text-right"><%= Float.round(@yaw, 2) %>&deg;</div>
</div>
</div>
<% else %>
<p> No data available </p>
<% end %>
</div>

View file

@ -0,0 +1,19 @@
defmodule AugieWeb.TemperatureLive do
use Phoenix.LiveView
alias Augie.Sensor.IMU
alias Phoenix.PubSub
@moduledoc false
def mount(_params, _context, socket) do
if connected?(socket), do: PubSub.subscribe(Augie.PubSub, "IMU")
{:ok, assign(socket, temperature: nil)}
end
def handle_info(%IMU{temperature: temperature}, socket) when is_float(temperature),
do: {:noreply, assign(socket, temperature: temperature)}
def handle_info(%IMU{temperature: temperature}, socket) when is_integer(temperature),
do: {:noreply, assign(socket, temperature: temperature / 1.0)}
end

View file

@ -0,0 +1,17 @@
<div class="card">
<div class="card-divider">
<h4>Temperature</h4>
</div>
<div class="card-section">
<div class="grid-x">
<div class="cell auto"><strong>Temperature</strong></div>
<div class="cell auto text-right">
<%= if @temperature do %>
<%= Float.round(@temperature, 2) %>&deg; C
<% else %>
-
<% end %>
</div>
</div>
</div>
</div>

View file

@ -1,5 +1,6 @@
defmodule AugieWeb.Router do
use AugieWeb, :router
import Phoenix.LiveDashboard.Router
pipeline :browser do
plug(:accepts, ["html"])
@ -7,15 +8,24 @@ defmodule AugieWeb.Router do
plug(:fetch_live_flash)
plug(:protect_from_forgery)
plug(:put_secure_browser_headers)
plug(:put_root_layout, {AugieWeb.LayoutView, :root})
end
pipeline :api do
plug(:accepts, ["json"])
end
scope "/" do
scope "/", AugieWeb do
pipe_through(:browser)
get("/", AugieWeb.PageController, :index)
live("/", DashboardLive)
live_dashboard("/dashboard", metrics: AugieWeb.Telemetry)
get("/camera/static", CameraController, :static)
get("/camera/stream", CameraController, :stream)
end
# Other scopes may use custom stacks.
# scope "/api", AugieWeb do
# pipe_through :api
# end
end

View file

@ -0,0 +1,55 @@
defmodule AugieWeb.Telemetry do
use Supervisor
import Telemetry.Metrics
@moduledoc false
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}
),
# Database Metrics
# summary("augie.repo.query.total_time", unit: {:native, :millisecond}),
# summary("augie.repo.query.decode_time", unit: {:native, :millisecond}),
# summary("augie.repo.query.query_time", unit: {:native, :millisecond}),
# summary("augie.repo.query.queue_time", unit: {:native, :millisecond}),
# summary("augie.repo.query.idle_time", 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.
# {AugieWeb, :count_users, []}
]
end
end

View file

@ -1,44 +1,28 @@
<!doctype html>
<html class="no-js" 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" />
<title>Augie the hexapod robot</title>
<%= csrf_meta_tag() %>
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
</head>
<body>
<div class="top-bar">
<div class="top-bar-left">
<ul class="dropdown menu" data-dropdown-menu>
<li class="menu-text">Augie <span class="label"><%= app_version() %></span></li>
<li>
<%= link("Dashboard", to: Routes.page_path(@conn, :index)) %>
</li>
</ul>
</div>
</div>
<header class="top-bar">
<div class="top-bar-left">
<ul class="dropdown menu" data-dropdown-menu>
<li class="menu-text">Augie <span class="label"><%= app_version() %></span></li>
<li>
<%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %>
</li>
</ul>
</div>
</header>
<div class="grid-container">
<div class="grid-x grid-margin-x">
<div class="cell">
<%= if get_flash(@conn, :info) do %>
<div class="callout warning">
<%= get_flash(@conn, :info) %>
</div>
<% end %>
<%= if get_flash(@conn, :error) do %>
<div class="callout alert">
<%= get_flash(@conn, :error) %>
</div>
<% end %>
<main role="main" class="grid-container">
<div class="grid-x grid-margin-x">
<div class="cell">
<%= if get_flash(@conn, :info) do %>
<div class="callout warning">
<%= get_flash(@conn, :info) %>
</div>
</div>
<%= render @view_module, @view_template, assigns %>
<% end %>
<%= if get_flash(@conn, :error) do %>
<div class="callout alert">
<%= get_flash(@conn, :error) %>
</div>
<% end %>
</div>
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</body>
</html>
</div>
<%= @inner_content %>
</main>

View file

@ -0,0 +1,28 @@
<header class="top-bar">
<div class="top-bar-left">
<ul class="dropdown menu" data-dropdown-menu>
<li class="menu-text">Augie <span class="label"><%= app_version() %></span></li>
<li>
<%= link "LiveDashboard", to: Routes.live_dashboard_path(@socket, :home) %>
</li>
</ul>
</div>
</header>
<main role="main" class="grid-container">
<div class="grid-x grid-margin-x">
<div class="cell">
<%= if live_flash(@flash, :info) do %>
<div class="callout warning">
<%= live_flash(@flash, :info) %>
</div>
<% end %>
<%= if live_flash(@flash, :info) do %>
<div class="callout alert">
<%= live_flash(@flash, :info) %>
</div>
<% end %>
</div>
</div>
<%= @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"/>
<title>Augie</title>
<%= csrf_meta_tag() %>
<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

@ -1,2 +1,38 @@
<br />
<%= live_render(@conn, AugieWeb.DashboardLive) %>
<section class="phx-hero">
<h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
<p>Peace-of-mind from prototype to production</p>
</section>
<section class="row">
<article class="column">
<h2>Resources</h2>
<ul>
<li>
<a href="https://hexdocs.pm/phoenix/overview.html">Guides &amp; Docs</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix">Source</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md">v1.5 Changelog</a>
</li>
</ul>
</article>
<article class="column">
<h2>Help</h2>
<ul>
<li>
<a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
</li>
<li>
<a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
</li>
<li>
<a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
</li>
<li>
<a href="https://elixir-slackin.herokuapp.com/">Elixir on Slack</a>
</li>
</ul>
</article>
</section>

View file

@ -10,7 +10,10 @@ defmodule AugieWeb.ErrorHelpers do
"""
def error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error), class: "help-block")
content_tag(:span, translate_error(error),
class: "invalid-feedback",
phx_feedback_for: input_id(form, field)
)
end)
end

View file

@ -1,16 +1,15 @@
defmodule Augie.MixProject do
use Mix.Project
@moduledoc false
def project do
[
app: :augie,
version: "0.1.0",
elixir: "~> 1.5",
elixir: "~> 1.7",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
]
end
@ -37,8 +36,6 @@ defmodule Augie.MixProject do
{:calendar, "~> 1.0"},
{:circuits_uart, "~> 1.4"},
{:circular_buffer, "~> 0.2.0"},
# {:contex, "~> 0.2.0"},
# {:community_theatre, "~> 0.1.1"},
{:community_theatre, git: "https://gitlab.com/jimsy/community-theatre"},
{:contex, github: "mindok/contex"},
{:credo, "~> 1.1", only: [:dev, :test], runtime: false},
@ -46,15 +43,30 @@ defmodule Augie.MixProject do
{:floki, ">= 0.0.0", only: :test},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:kinemat, git: "https://gitlab.com/jimsy/kinemat.git"},
{:nimble_parsec, "~> 0.5.3"},
{:observer_cli, "~> 1.5"},
{:phoenix_html, "~> 2.11"},
{:phoenix_live_dashboard, "~> 0.2.0"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_view, "~> 0.8.0"},
{:phoenix_pubsub, "~> 1.1"},
{:phoenix, "~> 1.4.11"},
{:phoenix_live_view, "~> 0.12.1"},
{:phoenix_pubsub, "~> 2.0"},
{:phoenix, "~> 1.5.1"},
{:picam, "~> 0.4.1"},
{:plug_cowboy, "~> 2.0"}
{:plug_cowboy, "~> 2.0"},
{:postgrex, ">= 0.0.0"},
{:telemetry_metrics, "~> 0.4"},
{:telemetry_poller, "~> 0.4"}
]
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to install project dependencies and perform other setup tasks, run:
#
# $ mix setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[]
end
end

View file

@ -1,27 +1,30 @@
%{
"angle": {:hex, :angle, "0.3.0", "00405e6f80a7c34210a031ab1d13dd5222e0651f6abe4350a568375faebaefae", [:mix], [], "hexpm", "3c5ad1253d761fcb6234ccaafae004efdf4ee8297687bb4b08e2b93f2e1cf208"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
"circuits_uart": {:hex, :circuits_uart, "1.4.1", "f8151bfb5ac29fe2e791eec00bc6ec298fdb8fa81ddd80bdb0f9f2828f20d7b8", [:mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "8cc3065de6d9b859f76d10fa438e243103eea1ba53b9749e4c63005c6d89780b"},
"circular_buffer": {:hex, :circular_buffer, "0.2.0", "c42be0740855831d04e22e17318a4e186acc3b9ebe9aad8db90b0a08ac0ff589", [:mix], [], "hexpm", "b2a05705485c7576373eeff99b2f76b0048731d9c80c7fa42d6c7b71eb69521e"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
"community_theatre": {:git, "https://gitlab.com/jimsy/community-theatre", "d2f33767e212ee3231702deace103042c66de549", []},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"contex": {:git, "https://github.com/mindok/contex.git", "544fe5c3d7aeaf2aa84109ba0e3361919f3fd7d9", []},
"context": {:git, "https://github.com/mindok/contex.git", "a602e7751abbbf7f099f383c45acab4a1948ea9c", []},
"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"},
"credo": {:hex, :credo, "1.3.2", "08d456dcf3c24da162d02953fb07267e444469d8dad3a2ae47794938ea467b3a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b11d28cce1f1f399dddffd42d8e21dcad783309e230f84b70267b1a5546468b6"},
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
"ex_erlstats": {:hex, :ex_erlstats, "0.1.6", "f06af6136e67c2ba65848d411f63de8d672f972907e2e357c14ab3d067a6269c", [:mix], [], "hexpm", "7ef6f411ab18301f1b519a74578b5a6bee67fe5ad9025ffbd04d5613a451db33"},
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
"floki": {:hex, :floki, "0.26.0", "4df88977e2e357c6720e1b650f613444bfb48c5acfc6a0c646ab007d08ad13bf", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e7b66ce7feef5518a9cd9fc7b52dd62a64028bd9cb6d6ad282a0f0fc90a4ae52"},
"gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm", "3c75b5ea8288e2ee7ea503ff9e30dfe4d07ad3c054576a6e60040e79a801e14d"},
"graphmath": {:hex, :graphmath, "1.0.7", "70d89f05df69d5001f82bbaa81ca37eb56a3b899c1abddfd5db0cabf16bd93ca", [:mix], [], "hexpm", "5da449df39f856eae8170d536f9e8bbcdff3cd4d1a280a13601414cdd03fe109"},
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
"heap": {:hex, :heap, "2.0.2", "d98cb178286cfeb5edbcf17785e2d20af73ca57b5a2cf4af584118afbcf917eb", [:mix], [], "hexpm", "ba9ea2fe99eb4bcbd9a8a28eaf71cbcac449ca1d8e71731596aace9028c9d429"},
"html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
"httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "116747dbe057794c3a3e4e143b7c8390b29f634e16c78a7f59ba75bfa6852e7f"},
"kinemat": {:git, "https://gitlab.com/jimsy/kinemat.git", "46a9c1ed96badf8487dcbb9419f1c12615f2eb01", []},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
@ -29,24 +32,23 @@
"nimble_strftime": {:hex, :nimble_strftime, "0.1.1", "b988184d1bd945bc139b2c27dd00a6c0774ec94f6b0b580083abd62d5d07818b", [:mix], [], "hexpm", "89e599c9b8b4d1203b7bb5c79eb51ef7c6a28fbc6228230b312f8b796310d755"},
"observer_cli": {:hex, :observer_cli, "1.5.3", "d42e20054116c49d5242d3ff9e1913acccebe6015f449d6e312a5bc160e79a62", [:mix, :rebar3], [{:recon, "~>2.5.0", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "3d2de7a710b9bed4cfbdae0419d98b1985634bd8cc1f26ef9576c2eb9aa6b35e"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"phoenix": {:hex, :phoenix, "1.4.16", "2cbbe0c81e6601567c44cc380c33aa42a1372ac1426e3de3d93ac448a7ec4308", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "856cc1a032fa53822737413cf51aa60e750525d7ece7d1c0576d90d7c0f05c24"},
"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.1", "7dabafadedb552db142aacbd1f11de1c0bbaa247f90c449ca549d5e30bbc66b4", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "536d5200ad37fecfe55b3241d90b7a8c3a2ca60cd012fc065f776324fa9ab0a9"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.2.0", "dbb1c6c81ab47484b7dd8fb648a91c3d953deac1cf052c4e96c6d27f0a21b5b6", [: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", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "439628cab6b7f9a66010df685b676198e9d3c426696ea106d6de2a7a24eab6bb"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.1", "274a4b07c4adbdd7785d45a8b0bb57634d0b4f45b18d2c508b26c0344bd59b8f", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "41b4103a2fa282cfd747d377233baf213c648fdcc7928f432937676532490eee"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.8.1", "fd514b86312d8e363fabf68be5abf966c21fb8ee62d8115a1c14cf0e3bfbd13b", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.14", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "8d90abcced4ffa52226b12b627e549082cd16b29fe982373e6e9997a238df018"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"},
"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"},
"picam": {:hex, :picam, "0.4.1", "a2d0d980cf8a18a0123477ac0c12ca11e6f060904d642d1d0672e8a43251576f", [:make, :mix], [{:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2b99fd77152d409617951f5dea6b13c54d5220516531f9a762a937ef7316bba0"},
"plug": {:hex, :plug, "1.10.0", "6508295cbeb4c654860845fb95260737e4a8838d34d115ad76cd487584e2fc4d", [: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", "422a9727e667be1bf5ab1de03be6fa0ad67b775b2d84ed908f3264415ef29d4a"},
"plug_cowboy": {:hex, :plug_cowboy, "2.1.2", "8b0addb5908c5238fac38e442e81b6fcd32788eaa03246b4d55d147c47c5805e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "7d722581ce865a237e14da6d946f92704101740a256bd13ec91e63c0b122fc70"},
"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"},
"poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"},
"postgrex": {:hex, :postgrex, "0.15.3", "5806baa8a19a68c4d07c7a624ccdb9b57e89cbc573f1b98099e3741214746ae4", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4737ce62a31747b4c63c12b20c62307e51bb4fcd730ca0c32c280991e0606c90"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"},
"reverse_proxy_plug": {:hex, :reverse_proxy_plug, "1.2.1", "f950a7ba212182b36fb44d06b8c6a347951863fd22d2201452c5233ce23701b1", [:mix], [{:cowboy, "~> 2.4", [hex: :cowboy, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.2", [hex: :httpoison, repo: "hexpm", optional: false]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "74d76efd874cb504678f4a884e258c4ff495bd984f5444bc91d1aab6e4424ca7"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"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.5.0", "4770888ef85599ead39c7f51d6b4b62306e602d96c69b2625d54dea3d9a5204b", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69e4e8e65b0ae077c9e14cd5f42c7cc486de0e07ac6e3409e6f0e52699a7872c"},
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
"websocket_client": {:hex, :websocket_client, "1.3.0", "2275d7daaa1cdacebf2068891c9844b15f4fdc3de3ec2602420c2fb486db59b6", [:rebar3], [], "hexpm", "b864fa076f059b615da4ab99240e515b26132ce4d2d0f9df5d7f22f01fa04b65"},
"wobserver": {:hex, :web_observer, "0.1.10", "a8d66acd2b15553e7ee2e93c80a7aa4f9774db6b07150dad72e80dafcbab1e90", [:mix], [{:cowboy, ">= 1.1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:httpoison, ">= 0.11.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:plug, ">= 1.3.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:websocket_client, ">= 1.2.0", [hex: :websocket_client, repo: "hexpm", optional: false]}], "hexpm", "71ab857398c9b4f6b0ace6f1e86a9070ee10a6432d52999f149f87e5f5a27bee"},
}

View file

@ -9,3 +9,89 @@
msgid ""
msgstr ""
"Language: en\n"
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""
## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted"
msgstr ""
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""
msgid "are still associated with this entry"
msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""
msgid "must be greater than %{number}"
msgstr ""
msgid "must be less than or equal to %{number}"
msgstr ""
msgid "must be greater than or equal to %{number}"
msgstr ""
msgid "must be equal to %{number}"
msgstr ""

View file

@ -8,3 +8,88 @@
## date. Leave `msgstr`s empty as changing them here has no
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""
## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted"
msgstr ""
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""
msgid "are still associated with this entry"
msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""
msgid "must be greater than %{number}"
msgstr ""
msgid "must be less than or equal to %{number}"
msgstr ""
msgid "must be greater than or equal to %{number}"
msgstr ""
msgid "must be equal to %{number}"
msgstr ""

View file

@ -1,3 +1,8 @@
defmodule AugieWeb.LayoutViewTest do
use AugieWeb.ConnCase, async: true
# When testing helpers, you may want to import Phoenix.HTML and
# use functions such as safe_to_string() to convert the helper
# result into an HTML string.
# import Phoenix.HTML
end

View file

@ -8,9 +8,11 @@ defmodule AugieWeb.ChannelCase do
to build common data structures and query the data layer.
Finally, if the test case interacts with the database,
it cannot be async. For this reason, every test runs
inside a transaction which is reset at the beginning
of the test unless the test case is marked as async.
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use AugieWeb.ChannelCase, async: true`, although
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
@ -18,7 +20,8 @@ defmodule AugieWeb.ChannelCase do
using do
quote do
# Import conveniences for testing with channels
use Phoenix.ChannelTest
import Phoenix.ChannelTest
import AugieWeb.ChannelCase
# The default endpoint for testing
@endpoint AugieWeb.Endpoint

View file

@ -12,7 +12,7 @@ defmodule AugieWeb.ConnCase do
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use AugieWeb.ConnCase, async: true`, although
this option is not recommendded for other databases.
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
@ -20,7 +20,10 @@ defmodule AugieWeb.ConnCase do
using do
quote do
# Import conveniences for testing with connections
use Phoenix.ConnTest
import Plug.Conn
import Phoenix.ConnTest
import AugieWeb.ConnCase
alias AugieWeb.Router.Helpers, as: Routes
# The default endpoint for testing