はじめに
- Elixir楽しんでいますか
- 2020/12/31 大晦日です
-
#大晦日ハッカソン
なるものに参加しています- 初参加です
- @myasu さんに教えてもらいました
- 大晦日ハッカソン2020
-
#大晦日ハッカソン
とツイートすればよいようです
- 2020/12/27(日)に行われた「オンライン】豪華プレゼント付!Elixir/Nerves(ナーブス)体験ハンズオン!」で準備のお手伝い、講師(歌唱)をしたのですが、Phoenixでのグラフうねうねのみを担当し、温湿度センサーGrove - AHT20の取り扱いはコードをななめ読みしかできていません
- はたしてデータシート片手に、自力でコーディングできるのだろうか? という疑問がわいてきました
- そこで、大晦日ハッカソン2020を利用して、「オンライン】豪華プレゼント付!Elixir/Nerves(ナーブス)体験ハンズオン!」の復習をしつつ、
- 自分で書いてみる
- |> せっかくつくったのだから我が家の温湿度情報を世界へ発信してみる
- をやってみたいとおもいます
- 「オンライン】豪華プレゼント付!Elixir/Nerves(ナーブス)体験ハンズオン!」でははじめからNervesのファームウェアが焼き込まれたmicroSDカードからスタートしましたが、最初に焼き込むファームウェアの作り方から解説いたします
完成品
- https://aht20.torifuku-kaiou.tokyo/aht20-dashboard
- 自宅の温度・湿度を全世界に発信し続けています
What is Elixir?
Elixir is a dynamic, functional language designed for building scalable and maintainable applications.
Elixir leverages the Erlang VM, known for running low-latency, distributed and fault-tolerant systems, while also being successfully used in web development, embedded software, data ingestion, and multimedia processing domains across a wide range of industries.
- 不老不死の霊薬っちいうことですね
What is Nerves?
- @takasehideki 先生の「ElixirでIoT!?ナウでヤングでcoolなNervesフレームワーク」をご参照ください
- Elixirというプログラミング言語でIoTできちゃうんです
What is Phoenix?
Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
- Elixirというプログラミング言語でのWebアプリケーションフレームワークです
準備
使用するハードウェア
- Raspberry Pi 4GBモデル
- Grove Base HAT for Raspberry Pi
- Grove AHT20 I2C (温湿度センサ)
- AC-DCアダプタ(Type-C, 5V3A)
- microSDカード(16 GB Class10)
- 不器用ですからな私でも、はめ込み式で簡単に工作を楽しめます 🚀🚀🚀
組み立て方
-
ALGYAN x Seeed x NervesJPハンズオン基本編
- 7ページまでご参照ください
- Grove Button(P)とGrove LEDはこの記事では使いません
-
ALGYAN x Seeed x NervesJPハンズオン応用編
- 7ページをご参照ください
開発マシン
- Nerves開発環境のInstallation
- Phoenix開発環境のInstallation
- ここが一番たいへんで、謎につまったりしますがここを乗り越えるとあとは楽しみだけですのでがんばりましょう🚀🚀🚀
- 私はmacOSを使っています
- Windows 10でも大丈夫です
開発外観
- 開発マシンでNervesアプリを作成・ビルドしてファームウェアをつくります
- 開発マシンからmicroSDカードにファームウェアを焼き込みます(初回のみ)
- ハードウェアにmicroSDカードを挿入して電源ON!!!
- 開発マシン(Host)とハードウェア(Target)が同じネットワークにいるようにします
- 2回目以降のビルドしたファームウェアは、ネットワーク経由でアップロードすることができます
- microSDカードを抜き差しする必要はないのです!
- Nervesアプリは別途用意するWebアプリケーションへ向けて取得したデータを打ち上げ(HTTP POST)して、データを表示します
Nervesアプリの作成
mix nerves.new
$ export MIX_TARGET=rpi4
$ mix nerves.new temperature_and_humidity_nerves
$ cd temperature_and_humidity_nerves
mix deps.get
defp deps do
[
...
{:nerves_system_x86_64, "~> 1.13", runtime: false, targets: :x86_64},
# add
{:circuits_i2c, "~> 0.1"},
{:httpoison, "~> 1.7"},
{:jason, "~> 1.2"},
{:timex, "~> 3.6"}
]
end
- 上の変更を行ったあとに
$ mix deps.get
- 必要なライブラリをインストールしています
config :tzdata, :data_dir, "/data/tzdata"
config :tzdata, :data_dir, "/data/tzdata"
- @nishiuchikazumaさんの「Nervesで出続けるtzdataのエラーを止めた」をご参照ください
WiFi設定(オプション)
- 有線LANケーブルでネットワークに接続する場合は不要です
- 手前味噌の「NervesをWiFiを使って固定IPでネットワークに追加します(Elixir)」をご参照ください
mix firmware && mix burn
- ここまでやったら一度、ファームウェアを作ってmicroSDカードに焼き込んでおきましょう
$ mix firmware
- ファームウェアがビルドされます
- microSDカードを開発マシンに差し込んで以下のコマンドで焼き込みます
$ mix burn
- こんがり焼き上がったら、ハードウェア(この記事の場合はRaspberry Pi 4)に差し込んで電源ON!!!
- 30秒ほど瞑想して
ping nerves.local
が通ることを確認したら
$ ssh nerves.local
iex>
-
Nervesの中に入れて、
IEx
(Elixir's Interactive Shell)が立ち上がっています - ここまで確認できたらとりあえず
exit
で抜けておきましょう - もしsshできない場合は、VintageNet Cookbookに従って、固定IP(例
192.168.1.200
)を設定するなどして、ssh 192.168.1.200
とかするとつながりやすいです
AHT20から温度湿度を取得するソースコードを書く
- https://github.com/NervesJP/nervesjp_basis/blob/f92aa586acbda53d87846f3cd0909380296a4ff1/lib/sensor/aht20.ex
- に答えがあるので、データシートを片手に書いていきます
defmodule TemperatureAndHumidityNerves.Aht20 do
use Bitwise
alias Circuits.I2C
@i2c_bus "i2c-1"
@i2c_addr 0x38
@initialization_command <<0xBE, 0x08, 0x00>>
@trigger_measurement_command <<0xAC, 0x33, 0x00>>
@two_pow_20 :math.pow(2, 20)
def read do
{:ok, ref} = I2C.open(@i2c_bus)
I2C.write(ref, @i2c_addr, @initialization_command)
Process.sleep(10)
I2C.write(ref, @i2c_addr, @trigger_measurement_command)
Process.sleep(80)
ret =
case I2C.read(ref, @i2c_addr, 7) do
{:ok, val} -> {:ok, val |> convert()}
{:error, :i2c_nak} -> {:error, "Sensor is not connected"}
_ -> {:error, "An error occurred"}
end
I2C.close(ref)
ret
end
defp convert(<<_, raw_humi::20, raw_temp::20, _>>) do
humi = Float.round(raw_humi / @two_pow_20 * 100.0, 1)
temp = Float.round(raw_temp / @two_pow_20 * 200.0 - 50.0, 1)
{temp, humi}
end
end
- 少し変えた部分はありますがほんのわずかでして、
- $\huge{お手本をみながら一文字一文字彫るように写しました}$
-
convert/1
関数は、@mnishiguchi さんのコメントのおかげで複雑なビット演算が不要になりました!
$ mix firmware
$ mix upload
- microSDカードを抜かずにネットワーク越しにファームウェアの書き換えが可能です!
- やってみればわかりますがものすごく便利です!
-
mix upload
がうまくいかない場合にはたとえばmix upload 192.168.1.200
等、ハードウェア(今回の場合はRaspberry Pi 4)に割り当たっているIPアドレスを指定してください - また30秒ほど瞑想をして
$ ssh nerves.local
iex> TemperatureAndHumidityNerves.Aht20.read
{:ok, {16.4, 32.0}}
- 温度湿度が取得できたことを確認したら
exit
で抜けておきましょう
1秒に一回、温度湿度を取得するようにする
defmodule TemperatureAndHumidityNerves.Ticker do
use GenServer
require Logger
def start_link(state) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init(state) do
:timer.send_interval(1000, self(), :tick)
{:ok, state}
end
def handle_info(:tick, state) do
Logger.debug("tick")
spawn(TemperatureAndHumidityNerves.Worker, :run, [])
{:noreply, state}
end
end
defmodule TemperatureAndHumidityNerves.Worker do
require Logger
@name "awesome"
@headers [{"Content-Type", "application/json"}]
def run do
{:ok, {temperature, humidity}} = TemperatureAndHumidityNerves.Aht20.read()
inspect({temperature, humidity}) |> Logger.debug()
post_temperature(temperature)
post_humidity(humidity)
end
defp post_temperature(value) do
post(value, "https://phx.japaneast.cloudapp.azure.com/temperatures")
end
defp post_humidity(value) do
post(value, "https://phx.japaneast.cloudapp.azure.com/humidities")
end
defp post(value, url) do
time = Timex.now() |> Timex.to_unix()
json = Jason.encode!(%{value: %{name: @name, value: value, time: time}})
HTTPoison.post(url, json, @headers)
end
end
defmodule TemperatureAndHumidityNerves.Application do
...
def children(_target) do
[
# Children for all targets except host
# Starts a worker by calling: TemperatureAndHumidityNerves.Worker.start_link(arg)
# {TemperatureAndHumidityNerves.Worker, arg},
TemperatureAndHumidityNerves.Ticker # add
]
end
-
@name
はお好きなお名前に変更してください - 「オンライン】豪華プレゼント付!Elixir/Nerves(ナーブス)体験ハンズオン!」で用意したデータ打ち上げ先にHTTP POSTしています
- $\huge{Don't think, feeeeeeeeeeeeel !!!}$
- Visit: https://phx.japaneast.cloudapp.azure.com/chart-temperature
- Visit: https://phx.japaneast.cloudapp.azure.com/chart-humidity
- 温度湿度ってそんなコロコロかわるものじゃないでしょうから、まっつぐなんですよねー
- ということで後半は打ち上げ先のWebアプリケーションも作ってしまいましょう
Phoenixアプリの作成
$ mix phx.new aht20 --live
$ cd aht20
$ mix setup
APIを作ります
mix phx.gen.json
$ mix phx.gen.json Measurements Value values temperature:float humidity:float time:integer
- このコマンドで下記のファイルができます
- このコマンドでだいたいできあがっています
lib/aht20/measurements.ex
lib/aht20/measurements/value.ex
lib/aht20_web/controllers/fallback_controller.ex
lib/aht20_web/controllers/value_controller.ex
lib/aht20_web/views/changeset_view.ex
lib/aht20_web/views/value_view.ex
priv/repo/migrations/20201231063807_create_values.exs
test/aht20/
test/aht20_web/controllers/
- このうち
lib/aht20/measurements.ex
とpriv/repo/migrations/20201231063807_create_values.exs
(コマンド実行時のタイムスタンプでファイル名はかわります)を少し変更します
def last do
Value |> last() |> Repo.one()
end
defmodule Aht20.Repo.Migrations.CreateValues do
use Ecto.Migration
def change do
create table(:values) do
add :temperature, :float, null: false
add :humidity, :float, null: false
add :time, :integer, null: false
timestamps()
end
end
end
-
null: false
を追加しています
パスの追加
scope "/api", Aht20Web do
pipe_through :api
resources "/values", ValueController, only: [:create, :show]
end
マイグレーション
$ mix ecto.migrate
- ここまでやって、下記のコマンドでWebアプリケーションを開始します
$ mix phx.server
- たとえばcurlで以下のようにするとデータが保存されます
$ curl -X POST -H "Content-Type: application/json" -d '{"value": {"temperature": "23.5", "humidity": 40.123, "time": 1605097502}}' http://localhost:4000/values
画面
defmodule Aht20Web.DashboardLive do
use Aht20Web, :live_view
alias Aht20.Values
def mount(_params, _session, socket) do
if connected?(socket) do
:timer.send_interval(1000, self(), :tick)
end
socket = assign_stats(socket)
{:ok, socket}
end
def render(assigns) do
~L"""
<h1>Dashboard</h1>
<div id="dashboard">
<div class="stats">
<div class="stat">
<span class="value">
<%= @temperature %>度
</span>
<span class="name">
温度
</span>
</div>
<div class="stat">
<span class="value">
<%= @humidity %>%
</span>
<span class="name">
湿度
</span>
</div>
</div>
</div>
"""
end
def handle_info(:tick, socket) do
socket = assign_stats(socket)
{:noreply, socket}
end
defp assign_stats(socket) do
{temperature, humidity} = Values.get()
assign(socket,
temperature: temperature,
humidity: humidity
)
end
end
defmodule Aht20.Values do
def get do
Aht20.Measurements.last()
|> handle_get()
end
defp handle_get(nil), do: {0, 0}
defp handle_get(%Aht20.Measurements.Value{temperature: temperature, humidity: humidity}) do
{temperature, humidity}
end
end
scope "/", Aht20Web do
pipe_through :browser
live "/", PageLive, :index
live "/aht20-dashboard", DashboardLive
end
- 素っ気ない画面がでるとおもいます
makeup
- この節はオプションです
- 私自身はフロントエンドまわりに疎いし弱いしあんまりちゃんとわかっていません
- 以下は私がこんなふうにやったら格好良くなりました(私にはこれが精一杯で、それなりに格好良くみえた)よという記録です
-
さらによくないことに、私は以下の部分は雰囲気で書いてしまっているので間違っているところもあるとおもいます
- 何かお気づきの方はご指摘ください
$\huge{スタイリングはあなたのお好みで}$
$\huge{もっと格好良くしてください}$
$ cd assets
$ npm install @tailwindcss/ui@0.5.0 tailwindcss@1.7.3 postcss-import@12.0.1 postcss-loader@3.0.0 postcss-nested@4.2.1 autoprefixer@9.8.6 --save-dev
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
{
test: /\.[s]?css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"sass-loader",
"postcss-loader",
],
},
],
},
/* This file is for your main application css. */
@import "../node_modules/nprogress/nprogress.css";
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "./custom.css";
@import "./phoenix.css";
@import "./live_view.css";
@import "tailwindcss/utilities";
- 以下追加
body {
@apply antialiased relative bg-cool-gray-200 font-sans;
}
header {
@apply mb-8;
}
.container {
@apply max-w-6xl mx-auto px-6;
}
h1 {
@apply text-center text-cool-gray-900 font-extrabold text-4xl mb-8;
}
ul.examples {
@apply mt-8 text-2xl mx-auto max-w-xs list-disc;
li {
@apply mt-3;
a {
@apply underline;
}
}
}
.icon {
fill: currentColor;
}
/* Light */
#light {
@apply max-w-xl mx-auto text-center;
button {
@apply bg-transparent mx-1 py-2 px-4 border border-cool-gray-400 border-2 rounded-lg shadow-sm transition ease-in-out duration-150 outline-none;
}
button:hover {
@apply bg-cool-gray-300;
}
img {
@apply w-10;
}
.meter {
@apply flex h-12 overflow-hidden text-base bg-cool-gray-300 rounded-lg mb-8;
}
.meter > span {
@apply flex flex-col justify-center text-cool-gray-900 text-center whitespace-no-wrap bg-yellow-300 font-bold;
transition: width 2s ease;
}
}
/* License */
#license {
@apply max-w-lg mx-auto text-center;
.card {
@apply bg-white shadow rounded-lg shadow-lg;
.content {
@apply p-6;
}
}
.seats {
@apply inline-flex items-center mb-8;
img {
@apply w-10 pr-2;
}
span {
@apply text-xl font-semibold text-cool-gray-700;
}
}
.amount {
@apply text-4xl leading-none font-extrabold text-cool-gray-900 mt-4;
}
}
/* Dashboard */
#dashboard {
@apply max-w-2xl mx-auto;
.stats {
@apply mb-8 rounded-lg bg-white shadow-lg grid grid-cols-3;
.stat {
@apply p-6 text-center;
.name {
@apply block mt-2 text-lg leading-6 font-medium text-cool-gray-500;
}
.value {
@apply block text-5xl leading-none font-extrabold text-indigo-600;
}
}
}
button {
@apply inline-flex items-center px-4 py-2 border border-indigo-300 text-sm shadow-sm leading-6 font-medium rounded-md text-indigo-700 bg-indigo-100 transition ease-in-out duration-150 outline-none;
img {
@apply mr-2 h-4 w-4;
}
}
button:active {
@apply bg-indigo-200;
}
button:hover {
@apply bg-white;
}
.controls {
@apply flex items-center justify-end;
form {
.group {
@apply flex items-baseline;
}
label {
@apply uppercase tracking-wide text-indigo-800 text-xs font-semibold mr-2;
}
select {
@apply appearance-none bg-cool-gray-200 border-indigo-300 border text-indigo-700 py-2 px-4 rounded-lg leading-tight font-semibold cursor-pointer mr-2 h-10;
}
select:focus {
@apply outline-none bg-white border-indigo-500;
}
}
}
}
/* Search */
#search {
@apply max-w-3xl mx-auto text-center;
form {
@apply inline-flex items-center;
input[type="text"] {
@apply h-10 border border-cool-gray-400 rounded-l-md bg-white px-5 text-base;
}
input[type="text"]:focus {
@apply outline-none;
}
input[name*='city'] {
@apply ml-4;
}
}
button {
@apply h-10 px-4 py-2 bg-transparent border border-cool-gray-400 border-l-0 rounded-r-md transition ease-in-out duration-150 outline-none;
img {
@apply h-4 w-4;
}
}
button:hover {
@apply bg-cool-gray-300;
}
.open {
@apply inline-flex items-center px-3 py-1 rounded-md text-xs font-medium leading-5 bg-green-200 text-green-800 rounded-full;
}
.closed {
@apply inline-flex items-center px-3 py-1 rounded-md text-xs font-medium leading-5 bg-red-200 text-red-800 rounded-full;
}
.stores {
@apply mt-8 bg-white shadow overflow-hidden rounded-md;
li {
@apply border-t border-cool-gray-300 px-6 py-4;
.first-line {
@apply flex items-center justify-between;
.name {
@apply text-lg leading-5 font-medium text-indigo-600 truncate;
}
.status {
@apply ml-2 flex-shrink-0 flex;
}
}
.second-line {
@apply mt-2 flex justify-between;
.street {
@apply mt-0 flex items-center text-base leading-5 text-cool-gray-400;
img {
@apply flex-shrink-0 mr-1 h-5 w-5;
}
}
.phone_number {
@apply mt-0 flex items-center text-sm leading-5 text-cool-gray-400;
img {
@apply flex-shrink-0 mr-2 h-5 w-5;
}
}
}
}
}
.flights {
@apply mt-8 bg-white shadow overflow-hidden rounded-md;
li {
@apply border-t border-cool-gray-300 px-6 py-4;
.first-line {
@apply flex items-center justify-between;
.number {
@apply text-lg leading-5 font-semibold text-indigo-600 truncate;
}
.origin-destination {
@apply mt-0 flex items-center text-base leading-5 text-indigo-600;
img {
@apply flex-shrink-0 mr-1 h-5 w-5;
}
}
}
.second-line {
@apply mt-2 flex justify-between;
.departs {
@apply text-cool-gray-500;
}
.arrives {
@apply text-cool-gray-500;
}
}
}
}
li:hover {
@apply bg-indigo-100;
}
}
/* Filter */
#filter {
.boats {
@apply flex flex-wrap;
}
.card {
@apply m-6 max-w-sm rounded bg-white overflow-hidden shadow-lg;
img {
@apply w-full;
}
.content {
@apply px-6 py-4;
}
.model {
@apply pb-3 text-center text-cool-gray-900 font-bold text-xl;
}
.details {
@apply px-6 mt-2 flex justify-between;
}
.price {
@apply text-cool-gray-700 font-semibold text-xl;
}
.type {
@apply inline-block bg-cool-gray-300 rounded-full px-3 py-1 text-sm font-semibold text-cool-gray-700;
}
}
form {
@apply max-w-xl mx-auto mb-4;
.filters {
@apply flex items-baseline justify-around;
select {
@apply appearance-none w-1/3 bg-cool-gray-200 border border-cool-gray-400 text-cool-gray-700 py-3 px-4 rounded-lg leading-tight font-semibold cursor-pointer;
}
select:focus {
@apply outline-none bg-cool-gray-200 border-cool-gray-500;
}
.prices {
@apply flex;
input[type="checkbox"] {
@apply opacity-0 fixed w-0;
}
input[type="checkbox"]:checked + label {
@apply bg-indigo-300 border-indigo-500;
}
label {
@apply inline-block border-t border-b border-cool-gray-400 bg-cool-gray-300 py-3 px-4 text-lg font-semibold leading-5;
}
label:hover {
@apply bg-cool-gray-400 cursor-pointer;
}
label:first-of-type {
@apply border-l border-r rounded-l-lg ;
}
label:last-of-type {
@apply border-l border-r rounded-r-lg ;
}
}
a {
@apply inline underline text-lg;
}
}
}
}
/* Donations */
#donations {
@apply max-w-4xl mx-auto;
.wrapper {
@apply mb-4 align-middle inline-block min-w-full shadow overflow-hidden rounded-lg border-b border-cool-gray-300;
}
a {
@apply underline text-indigo-500 font-semibold;
}
.fresh {
@apply px-4 py-2 rounded-md text-lg font-medium leading-5 bg-green-200 text-green-800 rounded-full;
}
.stale {
@apply px-4 py-2 rounded-md text-lg font-medium leading-5 bg-red-200 text-red-800 rounded-full;
}
table {
@apply min-w-full;
th {
@apply bg-transparent px-6 py-4 border-b border-cool-gray-300 bg-indigo-700 text-base leading-4 font-medium uppercase tracking-wider text-center text-white;
a {
@apply no-underline text-white;
}
a:hover {
@apply underline;
}
}
td {
@apply px-6 py-4 whitespace-no-wrap border-b border-cool-gray-300 text-lg leading-5 font-medium text-cool-gray-900 text-center;
}
tbody {
@apply bg-white;
}
th.item {
@apply pl-12 text-left;
}
td.item {
@apply pl-12 font-semibold text-left;
}
}
.footer {
@apply text-center bg-white max-w-4xl mx-auto text-lg py-8;
.pagination {
@apply inline-flex shadow-sm;
}
a {
@apply -ml-px inline-flex items-center px-3 py-2 border border-cool-gray-300 bg-white text-base leading-5 font-medium text-cool-gray-600 no-underline;
}
a:hover {
@apply bg-cool-gray-300;
}
a.active {
@apply bg-indigo-700 text-white;
}
a.previous {
@apply rounded-l-md;
}
a.next {
@apply rounded-r-md;
}
}
}
/* Incidents */
#incidents {
@apply max-w-3xl mx-auto;
.incident {
@apply flex items-center justify-between mt-2 border-r border-b border-l border-cool-gray-300 border-l-0 border-t bg-white rounded-b rounded-b-none rounded p-4 leading-normal w-full;
}
.priority {
@apply px-4;
}
.description {
@apply flex-1 px-4 text-cool-gray-800 font-bold text-lg;
}
.location {
@apply flex-1 px-4 font-semibold text-lg text-cool-gray-600 text-base;
}
.status {
@apply px-4;
}
button {
@apply bg-indigo-500 text-white font-semibold py-2 px-4 rounded outline-none;
}
button:hover {
@apply bg-indigo-700;
}
.resolved {
@apply text-lg font-bold leading-5 text-indigo-600 bg-transparent text-indigo-600 px-2;
}
.priority-1 {
@apply px-4 py-2 rounded-md text-lg font-bold leading-5 bg-red-500 text-white;
}
.priority-2 {
@apply px-4 py-2 rounded-md text-lg font-bold leading-5 bg-orange-500 text-white;
}
.priority-3 {
@apply px-4 py-2 rounded-md text-lg font-bold leading-5 bg-yellow-500 text-white;
}
}
/* New Incident */
#new-incident {
@apply mx-auto w-full max-w-md;
.wrapper {
@apply bg-white py-6 shadow rounded-lg px-10;
}
form {
input[type="text"],
textarea {
@apply appearance-none block w-full px-3 py-2 border border-cool-gray-400 rounded-md transition duration-150 ease-in-out text-base leading-5;
}
input[type="text"]:focus,
textarea:focus {
@apply outline-none border-indigo-300 shadow-outline-indigo;
}
.group:not(:first-of-type) {
@apply mt-6;
}
label {
@apply block text-sm font-bold leading-5 text-cool-gray-700 mb-1;
}
button {
@apply mt-6 w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 transition duration-150 ease-in-out outline-none;
}
button:hover {
@apply bg-indigo-500
}
button:focus {
@apply outline-none border-indigo-700 shadow-outline-indigo
}
.help-block {
@apply text-orange-600 mt-4;
}
}
}
/*
* Range Input
*
* Generated by:
* http://danielstern.ca/range.css
*
*/
input[type=range] {
-webkit-appearance: none;
width: 100%;
margin: 13.8px 0;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 8.4px;
cursor: pointer;
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
background: #7f9cf5;
border-radius: 0px;
border: 0px solid #7f9cf5;
}
input[type=range]::-webkit-slider-thumb {
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
border: 1px solid #4c51bf;
height: 36px;
width: 17px;
border-radius: 35px;
background: #4c51bf;
cursor: pointer;
-webkit-appearance: none;
margin-top: -13.8px;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: #97aef7;
}
input[type=range]::-moz-range-track {
width: 100%;
height: 8.4px;
cursor: pointer;
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
background: #7f9cf5;
border-radius: 0px;
border: 0px solid #7f9cf5;
}
input[type=range]::-moz-range-thumb {
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
border: 1px solid #4c51bf;
height: 36px;
width: 17px;
border-radius: 35px;
background: #4c51bf;
cursor: pointer;
}
input[type=range]::-ms-track {
width: 100%;
height: 8.4px;
cursor: pointer;
background: transparent;
border-color: transparent;
color: transparent;
}
input[type=range]::-ms-fill-lower {
background: #678af3;
border: 0px solid #7f9cf5;
border-radius: 0px;
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
}
input[type=range]::-ms-fill-upper {
background: #7f9cf5;
border: 0px solid #7f9cf5;
border-radius: 0px;
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
}
input[type=range]::-ms-thumb {
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
border: 1px solid #4c51bf;
height: 36px;
width: 17px;
border-radius: 35px;
background: #4c51bf;
cursor: pointer;
height: 8.4px;
}
input[type=range]:focus::-ms-fill-lower {
background: #7f9cf5;
}
input[type=range]:focus::-ms-fill-upper {
background: #97aef7;
}
/*
* Loading Spinner
*
* Copied from:
* https://projects.lukehaas.me/css-loaders/
*/
.loader,
.loader:before,
.loader:after {
border-radius: 50%;
width: 2.5em;
height: 2.5em;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation: load7 1.8s infinite ease-in-out;
animation: load7 1.8s infinite ease-in-out;
}
.loader {
color: #5a67d8;
font-size: 10px;
margin: 20px auto;
position: relative;
text-indent: -9999em;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
.loader:before,
.loader:after {
content: '';
position: absolute;
top: 0;
}
.loader:before {
left: -3.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.loader:after {
left: 3.5em;
}
@-webkit-keyframes load7 {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
@keyframes load7 {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
#repos {
@apply max-w-3xl mx-auto text-center;
form {
@apply inline-flex items-center px-2;
.filters {
@apply flex items-baseline;
select {
@apply appearance-none bg-cool-gray-200 border border-cool-gray-400 text-cool-gray-700 py-3 px-4 mr-4 rounded-lg leading-tight font-semibold cursor-pointer;
}
select:focus {
@apply outline-none bg-cool-gray-200 border-cool-gray-500;
}
a {
@apply inline underline text-lg;
}
}
}
.repos {
@apply mt-8 bg-white shadow overflow-hidden rounded-md;
li {
@apply px-6 py-4 border-t border-cool-gray-300;
.first-line {
@apply flex items-center justify-between;
.group {
@apply font-medium text-lg text-gray-800;
img {
@apply mr-1 h-6 w-6 inline;
}
}
button {
@apply flex items-center py-1 px-3 ml-2 text-base bg-transparent border border-cool-gray-300 font-medium rounded outline-none shadow-sm;
img {
@apply mr-1 h-4 w-4 inline;
}
}
button:hover {
@apply bg-cool-gray-300 border border-cool-gray-400;
}
}
.second-line {
@apply flex items-center justify-between mt-3;
.group {
@apply mt-0 flex items-center;
img {
@apply h-4 w-4 inline;
}
}
.language {
@apply px-3 py-1 rounded-full font-medium text-sm text-gray-600;
}
.language.elixir {
@apply bg-purple-300;
}
.language.ruby {
@apply bg-red-300;
}
.license {
@apply ml-4 mr-4 text-sm font-medium text-gray-600;
}
.stars {
@apply mr-1;
}
}
}
}
}
/* LiveView specific classes for your customizations */
.invalid-feedback {
color: #a94442;
display: block;
margin: -1rem 0 2rem;
}
.phx-no-feedback.invalid-feedback, .phx-no-feedback .invalid-feedback {
display: none;
}
.phx-click-loading {
opacity: 0.5;
transition: opacity 1s ease-out;
}
.phx-disconnected{
cursor: wait;
}
.phx-disconnected *{
pointer-events: none;
}
.phx-modal {
opacity: 1!important;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.4);
}
.phx-modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
}
.phx-modal-close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.phx-modal-close:hover,
.phx-modal-close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
/* 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;
}
module.exports = {
plugins: [
require("postcss-import"),
require("tailwindcss"),
require("autoprefixer"),
require('postcss-nested')
]
};
module.exports = {
theme: {
fontFamily: {
sans: [
// "system-ui",
"-apple-system",
// "BlinkMacSystemFont",
"Segoe UI",
"Roboto",
"Helvetica Neue",
"Arial",
"Noto Sans",
"sans-serif",
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji",
],
},
},
variants: {
//backgroundColor: ["responsive", "hover", "focus", "active"]
},
plugins: [require("@tailwindcss/ui")],
future: {
removeDeprecatedGapUtilities: true,
},
};
参考
- https://github.com/pragmaticstudio/live_view_studio
- Adding Tailwind CSS to Phoenix 1.4 and 1.5
- ごめん、私は全く解説できないです
Nervesアプリがデータを打ち上げる先が変わったので変更してファームウェアをアップデート
defmodule TemperatureAndHumidityNerves.Worker do
require Logger
@url "https://aht20.torifuku-kaiou.tokyo/api/values"
@headers [{"Content-Type", "application/json"}]
def run do
{:ok, {temperature, humidity}} = TemperatureAndHumidityNerves.Aht20.read()
inspect({temperature, humidity}) |> Logger.debug()
post(temperature, humidity)
end
defp post(temperature, humidity) do
time = Timex.now() |> Timex.to_unix()
json = Jason.encode!(%{value: %{temperature: temperature, humidity: humidity, time: time}})
HTTPoison.post(@url, json, @headers)
end
end
$ mix firmware
$ mix upload
ソースコード
Wrapping Up
- (NervesとPhoenixをいっぺんに書くと長くなるなあ〜)
- Enjoy Elixir
- ありがとう! 大晦日ハッカソン2020