Update to Phoenix 1.5.
This commit is contained in:
parent
9bd96f1120
commit
8381e2587c
52 changed files with 2299 additions and 1979 deletions
21
docker-compose.dev.yml
Normal file
21
docker-compose.dev.yml
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
44
webapp/Dockerfile.dev
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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)$"
|
||||
]
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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+)
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
82
webapp/lib/augie_web/camera_stream.ex
Normal file
82
webapp/lib/augie_web/camera_stream.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
23
webapp/lib/augie_web/controllers/camera_controller.ex
Normal file
23
webapp/lib/augie_web/controllers/camera_controller.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %> — <%= 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
|
||||
|
|
38
webapp/lib/augie_web/live/dashboard_live.html.leex
Normal file
38
webapp/lib/augie_web/live/dashboard_live.html.leex
Normal 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 %> — <%= 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>
|
69
webapp/lib/augie_web/live/gps_live.ex
Normal file
69
webapp/lib/augie_web/live/gps_live.ex
Normal 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
|
36
webapp/lib/augie_web/live/gps_live.html.leex
Normal file
36
webapp/lib/augie_web/live/gps_live.html.leex
Normal 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>
|
|
@ -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
|
|
@ -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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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
|
48
webapp/lib/augie_web/live/orientation_live.ex
Normal file
48
webapp/lib/augie_web/live/orientation_live.ex
Normal 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
|
23
webapp/lib/augie_web/live/orientation_live.html.leex
Normal file
23
webapp/lib/augie_web/live/orientation_live.html.leex
Normal 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) %>°</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) %>°</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) %>°</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<p> No data available </p>
|
||||
<% end %>
|
||||
</div>
|
19
webapp/lib/augie_web/live/temperature_live.ex
Normal file
19
webapp/lib/augie_web/live/temperature_live.ex
Normal 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
|
17
webapp/lib/augie_web/live/temperature_live.html.leex
Normal file
17
webapp/lib/augie_web/live/temperature_live.html.leex
Normal 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) %>° C
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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
|
||||
|
|
55
webapp/lib/augie_web/telemetry.ex
Normal file
55
webapp/lib/augie_web/telemetry.ex
Normal 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
|
|
@ -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>
|
||||
|
|
28
webapp/lib/augie_web/templates/layout/live.html.leex
Normal file
28
webapp/lib/augie_web/templates/layout/live.html.leex
Normal 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>
|
15
webapp/lib/augie_web/templates/layout/root.html.eex
Normal file
15
webapp/lib/augie_web/templates/layout/root.html.eex
Normal 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>
|
|
@ -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 & 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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"},
|
||||
}
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in a new issue