LoginSignup
14
8

More than 3 years have passed since last update.

AHT20で温度湿度を取得して全世界に惜しげもなくあたい(値)を公開する(Elixir/Nerves/Phoenix)

Last updated at Posted at 2020-12-31

はじめに

完成品

IMG_20210102_173324.jpg

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?

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アプリケーションフレームワークです

準備

使用するハードウェア

組み立て方

開発マシン

開発外観

Nerves.png

  • 開発マシンで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
  • rpi4のところはお手持ちのハードウェアにあわせてください

mix deps.get

mix.exs
  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/target.exs
config :tzdata, :data_dir, "/data/tzdata"

WiFi設定(オプション)

mix firmware && mix burn

  • ここまでやったら一度、ファームウェアを作ってmicroSDカードに焼き込んでおきましょう :fire:
$ mix firmware
  • ファームウェアがビルドされます
  • microSDカードを開発マシンに差し込んで以下のコマンドで焼き込みます:fire:
$ 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から温度湿度を取得するソースコードを書く

lib/temperature_and_humidity_nerves/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
$ 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}}
  • :tada::tada::tada:
  • 温度湿度が取得できたことを確認したらexitで抜けておきましょう

1秒に一回、温度湿度を取得するようにする

lib/temperature_and_humidity_nerves/ticker.ex
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
lib/temperature_and_humidity_nerves/worker.ex
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
lib/temperature_and_humidity_nerves/application.ex
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

スクリーンショット 2020-12-31 14.15.14.png

  • :sweat_smile::sweat_smile::sweat_smile::sweat_smile::sweat_smile::sweat_smile:
  • 温度湿度ってそんなコロコロかわるものじゃないでしょうから、まっつぐなんですよねー
  • ということで後半は打ち上げ先のWebアプリケーションも作ってしまいましょう:rocket::rocket::rocket:

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.expriv/repo/migrations/20201231063807_create_values.exs(コマンド実行時のタイムスタンプでファイル名はかわります)を少し変更します
lib/aht20/measurements.ex
  def last do
    Value |> last() |> Repo.one()
  end
priv/repo/migrations/20201231063807_create_values.exs
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を追加しています

パスの追加

lib/aht20_web/router.ex
  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

画面

lib/aht20_web/live/dashboard_live.ex
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
lib/aht20/values.ex
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
lib/aht20_web/router.ex
   scope "/", Aht20Web do
     pipe_through :browser

     live "/", PageLive, :index
     live "/aht20-dashboard", DashboardLive
   end

スクリーンショット 2020-12-31 16.24.29.png

  • 素っ気ない画面がでるとおもいます :rocket::rocket::rocket:

makeup

  • この節はオプションです
  • 私自身はフロントエンドまわりに疎いし弱いしあんまりちゃんとわかっていません
  • 以下は私がこんなふうにやったら格好良くなりました(私にはこれが精一杯で、それなりに格好良くみえた)よという記録です
  • さらによくないことに、私は以下の部分は雰囲気で書いてしまっているので間違っているところもあるとおもいます
    • 何かお気づきの方はご指摘ください :bow::bow_tone1::bow_tone2::bow_tone3::bow_tone4::bow_tone5:

$\huge{スタイリングはあなたのお好みで}$
$\huge{もっと格好良くしてください}$
:tada::tada::tada:

スクリーンショット 2020-12-31 17.37.57.png

$ 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
assets/webpack.config.js
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: "babel-loader",
          },
        },
        {
          test: /\.[s]?css$/,
          use: [
            MiniCssExtractPlugin.loader,
            "css-loader",
            "sass-loader",
            "postcss-loader",
          ],
        },
      ],
    },
assets/css/app.scss
/* 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";

  • 以下追加
assets/css/custom.css
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;
        }
      }
    }
  }
}
assets/css/live_view.css
/* 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;
}
assets/postcss.config.js
module.exports = {
  plugins: [
    require("postcss-import"),
    require("tailwindcss"),
    require("autoprefixer"),
    require('postcss-nested')
  ]
};
assets/tailwind.config.js
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,
  },
};

参考

Nervesアプリがデータを打ち上げる先が変わったので変更してファームウェアをアップデート

lib/temperature_and_humidity_nerves/worker.ex
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 :christmas_tree::santa::santa_tone1::santa_tone2::santa_tone3::santa_tone4::santa_tone5::christmas_tree:

14
8
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8