LoginSignup
17
8

More than 1 year has passed since last update.

団長! いつでもどこでも温度・湿度が測れるのであります! (Elixir, SORACOM Air for セルラー)

Last updated at Posted at 2021-05-09

はじめに

IoTシステムを作った背景

  • $\huge{いつでもどこでも}$
  • $\huge{温度・湿度を測りたくなりました}$
  • I wantなのです
  • I enjoyなのです
  • それ以上の理由はありません
  • やりたくなってしまったことはもう仕方ありません
  • 誰にも止められません
  • なにか問題があってそれを解決しましたというようなことは私の場合にはありません
  • この記事がヒントとなって、どなたかのsomething badを改善することになればそれは望外の喜びです
  • あえて探すとすれば、私はElixirというプログラミング言語が好きです
    • 分類すると関数型言語にあたるのだそうです
    • そんな小難しそうなことを私は説明できませんが、そんなことを意識することなしに、楽しめる素敵なプログラミング言語ーーそれがElixirなのです
  • そのElixir製のIoTフレームワークであるナウでヤングでcoolなNervesというものがあります
  • 当面の目標は、このNervesにデータ通信端末を差し込んで、いつでもどこでも通信を楽しみたいとおもっています
  • I want to enjoyなのです
  • ただ、いきなりNervesアプリとして作るのはいろいろハードルがありそうでまずは先人の方々の記事が豊富にあるRaspberry Pi OSにてはじめてみたいとおもいます
    • (そうしないとコンテストの締め切りに間に合う気がしないので……)

実際の使用例

  • Raspberry Pi 4(with SORACOM Air for セルラー)を持って外へ出よう
  • 私は進撃の巨人Netflixでみました
  • みてしまいました
    • 巨人の方々に注目してみています
  • 日田の大山ダムというところに超大型巨人が出現するということを聞きました1
  • 私は調査兵団に期待する善良な一般市民です
  • もしかすると巨人の出現と気温・湿度にはなにかしらの関連があるのではないかという仮説をたてました
  • 塀の内側からではありますが測定を実施したいとおもいました
  • この記事は言わば地下室の資料に相当するものであり、この記録が人類滅亡の危機を救う一助となることを切に願っています

02-Anywhere-can-be-measuredのコピー.png

スクリーンショット 2021-05-08 18.38.49.png

  • 出不精のデブな私がdevをして、そして外にでたということで、そうです! まさに普段の(※私の)生活を豊かにした作品なのです

必要なパーツの紹介

パーツ 必須? 価格(おおよそ) 備考
特定地域向け IoT SIM (plan-D) Yes 903円 いの一番に持ってきました。もちろん必須です。L-02Cで使う場合には標準サイズです。価格は初期費用です。別途、月額費用(基本料金とデータ通信量に応じた従量課金)が必要です。詳細は、 https://soracom.jp/services/air/cellular/price_specific_area_sim/ をご参照ください。あとでSORACOM BEAMも使ってみますので必須です。まずSORACOM IoTストアで買い求めましょう。
L-02C Yes 生産終了(600円〜6,000円) 他のデータ通信端末でも可。むしろSORACOM IoTストアで取り扱っているものを使ったほうがいろいろ詰まるところは少ないのかもしれません。そういう意味では必須ではないのですがこの記事ではL-02Cでのことのみを取り扱いますので必須としておきます。発売初期のころに買いました。なぜ買ったのかは忘れました。その後、SIMは解約しました。父に言われたことを思い出して、地下室の奥に眠っていたものを引っ張り出してきました。
Raspberry Pi 4 / 4GB Yes 7,700円
microSD Yes 1,500円 8GBを使いました。 Class 10。
Anker PowerCore 10000 Yes 2,799円 モバイルバッテリー
USB-C & USB-A ケーブル Yes 1,000円 Raspberry Pi 4とモバイルバッテリーを接続
Grove AHT20 I2C温度および湿度センサー 工業用グレード Yes 513円
Raspberry Pi用Grove Base Hat Yes 1,037円 手先が器用な方には不要だったりするのかもしれませんが、不器用ですからな私でも、はめ込み式でIoTを楽しめます!
Azure 仮想マシン Yes データ打ち上げ先を自作しました。もちろん、Elixir製のWebアプリケーションフレームワークPhoenixを使いました。
ヒートシンク No 374円 ある意味、必須なのかも。まじRaspberry Pi 4ってアチチ:fire:になるので。
ケース No 1,298円 外に連れ出すならケースをつけておいたほうが持ち運びやすいかも。お好みで。
スピーカー No 外に持ち出さないときに目覚ましアラームとしてがんばってもらいます。音は、お家のネットワークにつないでおいてニュースと天気予報を取得して、それを音声データにして流します。

開発時にあったほうがよいもの

  • パソコン
    • 私はmacOS 10.15.7を使いました
    • パソコンは必須です
  • SDカードリーダー
  • AC-DCアダプタ(Type-C, 5V3A)
    • 開発時にRaspberry Pi 4の給電に使います
  • LANケーブル
    • 開発時にRaspberry Pi 4をお家LANにつながってもらうために使います

IoTシステムの構成図

スクリーンショット 2021-05-08 17.35.08.png

  • Raspberry Pi OS上ではElixirのプログラムが、Grove AHT20 I2C温度および湿度センサー 工業用グレードを使って温度・湿度を取得しています
  • 温度・湿度データは、SORACOM Air for セルラーの回線を使ってSORACOM Beamへhttpで送ります
  • Azure仮想マシンではPhoenixを使って自作したWebアプリケーションがhttpsのAPIを公開しています
  • SORACOM Beam -> Azure仮想マシン間は、SORACOM Beamのおかげでhttps通信となります
  • Phoenixアプリケーションは表示用のURLを公開していてそこにアクセスすると、温度・湿度のデータを読み取ることができます
    • Phoenixアプリとブラウザ(図ではスマホ)の間はWebSocketで通信しています
    • そんなまわりくどいことせんでRaspberry Piに表示モジュールくっつけたらいいんじゃないの? というご指摘はごもっともです
    • 繰り返しになりますが、私はI enjoyI wantでやっているだけでなにかの解決を目指しているわけではありません
    • IoT機器は測定だけしてそこからのさきの分析やらなんとかやらはクラウドに任せるという構成はよくある例だとおもいますのでそれをやってみたかったのです
    • またなんの自慢にもなりませんが、表示モジュールを使いこなせていないという事情もあります……
      • 自分のできることを組み合わせて作品にする
      • 「そこがいいんじゃない!」と聞こえてきそうです2

画像

簡単な手順書

  • 募集要項に従ってつけたタイトルです
    • だって参加賞は絶対にもらいたいですもん
  • ここから少々長いですが、できあがりを持っている私には間違いなく、すべからく簡単な手順です

手順のもくじ(たったの8ステップ!!!)

  • ① パーツを組み立てる
  • ② Raspberry Pi OSを焼く
  • ③ ssh接続、I2C有効化
  • ④ Elixirのインストール
  • SORACOM Air for セルラーのアクティベート
  • ⑥ PhoenixアプリをAzureにデプロイ
  • ⑦ Raspberry Pi 4 で動かすElixirプログラムを書く
  • ⑧ Run (イゴかす)

① パーツを組み立てる

  • Raspberry Pi用Grove Base Hatは真っ直ぐグイっとさしてください
    • ピンを折りやしないかと不安になってしまうかもしれませんが迷わず、真っ直ぐグイっとさしてください
    • 首の裏にたたきこむ要領です

IMG_20201203_212332.jpg

② Raspberry Pi OSを焼く

  • パソコンでの操作です
  • Raspberry Pi OS
  • :point_up::point_up_tone1::point_up_tone2::point_up_tone3::point_up_tone4::point_up_tone5:にRaspberry Pi Imagerというツールがあるのでインストールします
  • あとはmicroSDカードをパソコンに挿して、Raspberry Pi Imagerをなんとなく雰囲気で触ってもらえればこんがり焼き上がります
  • OSはRaspberry Pi OS (32-bit)を選びました
    • Recommendedと書いてある一番上のやつです

HDMIケーブルを持っていないのでsshでつないで作業ができるようにします

  • 焼き上がったら一度microSDカードを抜いてまた挿します
  • 私のmacOSではmicroSDカードが/Volumes/bootというところにマウントされました
  • sshという空のファイルをつくっておきます
    • Could you please refer to SSH (Secure Shell) ?
    • 空のsshファイルをつくるというのは、:point_up::point_up_tone1::point_up_tone2::point_up_tone3::point_up_tone4::point_up_tone5: をご参照ください
$ cd /Volumes/boot
$ touch ssh
  • microSDカードをRaspberry Pi 4に挿しこみ、LANケーブルもつないで電源ON!
  • この時点では、はやる気持ちをおさえて、L-02Cをまだ取り付けないほうが吉です
    • 私はこわくて試してはいませんが、うまくブートできないといった記事を見ました

③ ssh接続、I2C有効化

  • パソコンでの操作です
  • ping raspberrypi.localで反応があったらssh接続をします
$ ssh pi@raspberrypi.local

pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ uname -a
Linux raspberrypi 5.10.17-v7l+ #1403 SMP Mon Feb 22 11:33:35 GMT 2021 armv7l GNU/Linux
  • 初期パスワードはraspberryです
  • このまま進めるのもアレなのでsshの設定を変更しておきます

sshの設定変更

  • この記事を読んでいる人ならお持ちであろうid_rsaで接続できるようにします
    • お持ちではない場合は、
    • macOSの場合、man ssh-keygenと聞いてみてください
    • けっこうながいので、ssh-keygenでググってみるとよいです
    • man ssh-keygenと聞いてみだくだい」と一次情報にあたることをすすめておきながら、私自身はちゃんと読んだことはありません :sweat:
  • まずパソコン側で
$ cat ~/.ssh/id_rsa.pub | pbcopy
  • とでもして、公開カギをコピーします
  • この内容をRaspberry Piのほうの/home/pi/.ssh/authorized_keysに書き込みます
pi@raspberrypi:~ $ cd /home/pi
pi@raspberrypi:~ $ mkdir .ssh
pi@raspberrypi:~ $ nano .ssh/authorized_keys
pi@raspberrypi:~ $ chmod 600 ~/.ssh/authorized_keys 
pi@raspberrypi:~ $ chmod 700 ~/.ssh
pi@raspberrypi:~ $ sudo nano /etc/ssh/sshd_config
  • /etc/ssh/sshd_configにはたくさんの設定があるのですが以下のような設定をしました
/etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no 
PermitEmptyPasswords no
UsePAM no 
pi@raspberrypi:~ $ sudo service ssh reload
  • これで/home/pi/.ssh/authorized_keysに書き込まれた公開カギに対応する秘密鍵をもっているマシンからのみsshできるようになりました
  • 詳しくはSecuring your Raspberry Piをご参照ください
    • よく読むとpiユーザーも消して、新しくユーザを作るともっと安全だよとかいろいろ書いてあります
    • この記事ではこのままpiで行きます

I2Cを有効化

pi@raspberrypi:~ $ sudo raspi-config
  • 使い方は、raspi-configをご参照ください
  • [3 Interface Options] > [P5 I2C] > [Yes]
  • 横→キーを2回おしてFinish

スピーカーから音が鳴ることを確かめておく(オプション)

pi@raspberrypi:~ $ cd /opt/vc/src/hello_pi
pi@raspberrypi:/opt/vc/src/hello_pi $ ./rebuild.sh

pi@raspberrypi:/opt/vc/src/hello_pi $ cd hello_audio/
pi@raspberrypi:/opt/vc/src/hello_pi/hello_audio $ ./hello_audio.bin
  • 鳴りました :speaker:
  • Ctl + C

④ Elixirのインストール

pi@raspberrypi:~ $ sudo apt-get update
pi@raspberrypi:~ $ sudo apt-get install m4 libncurses5-dev libssl-dev git fop openjdk-8-jdk xsltproc libxml2-utils unixodbc-dev libwxgtk3.0-dev
pi@raspberrypi:~ $ wget http://erlang.org/download/otp_src_23.3.tar.gz
pi@raspberrypi:~ $ tar xzvf otp_src_23.3.tar.gz
pi@raspberrypi:~ $ cd otp_src_23.3/
pi@raspberrypi:~/otp_src_23.3 $ ./configure --enable-hipe
pi@raspberrypi:~/otp_src_23.3 $ make -j4
pi@raspberrypi:~/otp_src_23.3 $ sudo make install
pi@raspberrypi:~/otp_src_23.3 $ cd ../
pi@raspberrypi:~ $ git clone https://github.com/elixir-lang/elixir.git
pi@raspberrypi:~ $ cd elixir/
pi@raspberrypi:~/elixir $ git checkout v1.11.4
pi@raspberrypi:~/elixir $ make clean test
pi@raspberrypi:~/elixir $ sudo make install

SORACOM Air for セルラーのアクティベート

soracom-0.png

soracom-1.png

  • 速度クラスは一番料金のかからない「s1.minimum」にしておきました
pi@raspberrypi:~ $ sudo apt-get install wvdial cu
  • usb_modeswitchのインストールが必要とされている記事もありますが、最近のRaspberry Pi OSの場合は必要ないようです
  • /etc/wvdial.confを編集します
pi@raspberrypi:~ $ sudo nano /etc/wvdial.conf
/etc/wvdial.conf
[Dialer Defaults]
Init1 = ATH
Init2 = ATZ
Init2 = ATQ0 V1 E1 S0=0 &C1 &D2
Init3 = AT+CGDCONT=1,"IP","soracom.io"
Modem Type = Analog Modem
Baud = 9600
Modem = /dev/ttyUSB2
Phone = *99***1#
Username = sora
Password = sora
New PPPD = yes
Dial Attempts = 3
Dial Command = ATD
Stupid Mode = yes
Carrier Check = no
Auto DNS = yes
Check DNS = yes
Check Def Route = yes
Auto Reconnect = yes
pi@raspberrypi:~ $ cu -l /dev/ttyUSB2
ATZ

AT+COPS=1,2,"44010"
  • AT+COPS=1,2,"44010"は反応が戻ってくるまで少し時間がかかります
  • これを紹介している記事は見当たらなかったので効果のほどは怪しいのですが、AT+COPS=1,2,"44010"をしておくと後ほど行う接続にてIPアドレスが振られることが多くなったようにおもいます
    • 私の家の電波状況とかそういうものが関係しているのかもしれませんが、AT+COPS=1,2,"44010"をしていない場合はちっともIPアドレスがふられる気配はありませんでした
    • AT+COPS=1,2,"44010"を行ったあとからは、IPアドレスがすぐにふられるようになったので私には意味のあるもののようにみえています
  • ちなみに440がMCC(Mobile Country Code)=日本で、10がMNC(Mobile Network Code)=docomoの意味です
  • しばらくまってOKが返ってきたら、~.と入力してください
  • これでATコマンドを入力するモードから抜けます
    • sshがきれてしまう場合がありましたがまたsshで入り直しましょう
pi@raspberrypi:~ $ sudo wvdial &
pi@raspberrypi:~ $ --> WvDial: Internet dialer version 1.61
--> Initializing modem.
--> Sending: ATH
ATH
OK
--> Sending: ATQ0 V1 E1 S0=0 &C1 &D2
ATQ0 V1 E1 S0=0 &C1 &D2
OK
--> Sending: AT+CGDCONT=1,"IP","soracom.io"
AT+CGDCONT=1,"IP","soracom.io"
OK
--> Modem initialized.
--> Sending: ATD*99***1#
--> Waiting for carrier.
ATD*99***1#
CONNECT
--> Carrier detected.  Starting PPP immediately.
--> Starting pppd at Sat May  8 14:18:42 2021
--> Pid of pppd: 13193
--> Using interface ppp0
--> local  IP address xx.xx.xx.xx
--> remote IP address xx.xx.xx.xx
--> primary   DNS address xx.xx.xx.xx
--> secondary DNS address xx.xx.xx.xx
  • こんな感じの表示がでたら成功です
pi@raspberrypi:~ $ sudo route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         192.168.1.1     0.0.0.0         UG    202    0        0 eth0
10.64.64.64     0.0.0.0         255.255.255.255 UH    0      0        0 ppp0
192.168.1.0     0.0.0.0         255.255.255.0   U     202    0        0 eth0
  • こんな感じになっているとおもいます
  • SORACOM Air for セルラーで外と通信するように設定変更します
    • 詳しいことはあんまりわかっておりません :sweat_smile:
    • 大山ダムでデータが打ち上がっていたので大丈夫でしょう
pi@raspberrypi:~ $ sudo route del default
pi@raspberrypi:~ $ sudo route add default dev ppp0
pi@raspberrypi:~ $ sudo route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         0.0.0.0         0.0.0.0         U     0      0        0 ppp0
10.10.10.10     0.0.0.0         255.255.255.255 UH    0      0        0 ppp0
192.168.1.0     0.0.0.0         255.255.255.0   U     202    0        0 eth0
pi@raspberrypi:~ $ curl https://soracom.jp/

soracom-2.png

  • :tada::tada::tada:
  • あとはElixirでアプリケーションを書きます
  • L-02Cを抜いて、一旦Raspberry Pi 4の電源OFF -> ONしておきます

⑥ PhoenixアプリをAzureにデプロイ

⑦ Raspberry Pi 4 で動かすElixirプログラムを書く

1. プロジェクトの雛形をつくる

$ mix new honeko_pack
  • honeko_packはアプリケーションの名前です
  • honekoは私が使っているネコのアイコンの名前です
  • 適当な名前をつけてください

2. mix.exsに依存ライブラリを追加する

mix.exs
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
      {:circuits_i2c, "~> 0.1"},
      {:httpoison, "~> 1.8"},
      {:jason, "~> 1.2"},
      {:quantum, "~> 3.0"}
    ]
  end
$ mix deps.get

3. Grove AHT20 I2C温度および湿度センサー 工業用グレードから温度・湿度を読み取るモジュールをつくる

lib/aht20/reader.ex
defmodule Aht20.Reader do
  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 = I2C.read(ref, @i2c_addr, 7)

    I2C.close(ref)

    value(ret)
  end

  defp value({:ok, val}), do: {:ok, convert(val)}

  defp value(_), do: :error

  defp convert(<<_, raw_humi::20, raw_temp::20, _>>) do
    humi = Float.round(raw_humi * 100 / @two_pow_20, 1)
    temp = Float.round(raw_temp * 200 / @two_pow_20 - 50.0, 1)

    {temp, humi}
  end
end

実行結果

  • ここまでを動かしてみましょう
pi@raspberrypi:~/projects/honeko_pack $ iex -S mix

iex> Aht20.Reader.read
{:ok, {26.3, 50.4}}

4. 天気予報を取得 (おまけ)

lib/weather/forecast.ex
defmodule Weather.Forecast do
  @options [timeout: 50_000, recv_timeout: 50_000]
  def get(area \\ 400_000) do
    "https://www.jma.go.jp/bosai/forecast/data/overview_forecast/#{area}.json"
    |> HTTPoison.get!([], @options)
    |> Map.get(:body)
    |> Jason.decode!()
  end
end

実行結果

  • ここまでを動かしてみましょう
iex> Weather.Forecast.get 
%{
  "headlineText" => "福岡、北九州地方では、9日明け方まで強風に注意してください。",
  "publishingOffice" => "福岡管区気象台",
  "reportDatetime" => "2021-05-08T16:34:00+09:00",
  "targetArea" => "福岡県",
  "text" => " 福岡県は、気圧の谷の影響により概ね曇りとなっています。また、福岡県では、黄砂を観測しています。\n\n 8日は、高気圧に覆われて晴れる所もありますが、気圧の谷や湿った空気の影響により概ね曇りとなるでしょう。\n\n 9日は、はじめ気圧の谷や湿った空気の影響により曇りで、雨が降る所がありますが、高気圧に覆われて次第に晴れとなるでしょう。\n\n また、上空の風の予想から9日にかけて黄砂の飛来が予想されます。"
}

6. Bingでとれたて新鮮ニュースを取得 (おまけ)

lib/azure/bing/news_search.ex
defmodule Azure.Bing.NewsSearch do
  @subscription_key "ひみつ"
  @options [timeout: 50_000, recv_timeout: 50_000]

  def top_news do
    search()
    |> Map.get("value")
    |> Enum.at(0)
  end

  def search do
    "https://api.bing.microsoft.com/v7.0/news/search?q=&setLang=ja-JP&mkt=ja-JP"
    |> HTTPoison.get!(["Ocp-Apim-Subscription-Key": @subscription_key], @options)
    |> Map.get(:body)
    |> Jason.decode!()
  end
end
  • ソースコードたったこれだけ!

実行結果

  • ここまでを動かしてみましょう
iex> Azure.Bing.NewsSearch.search
%{
  "_type" => "News",
  "queryContext" => %{"originalQuery" => ""},
  "readLink" => "https://api.bing.microsoft.com/api/v7/news/search?q=&setLang=ja-JP",
  "value" => [
    %{
      "ampUrl" => "https://mainichi.jp/articles/20210508/k00/00m/030/247000c.amp",
      "category" => "World",
      "datePublished" => "2021-05-08T13:46:00.0000000Z",
      "description" => "李 漢東氏(イ・ハンドン=元韓国首相)韓国メディアによると8日、持病のため自宅で死去、86歳。  34年、京畿道抱川生まれ。ソウル大卒業後、判事や検事などを経て政界入りし、81年から国会議員に6回当選した。金大中政権時代の00~02年に首相を務めた。(共同)",
      "headline" => true,
      "image" => %{
        "thumbnail" => %{
          "contentUrl" => "https://www.bing.com/th?id=OVFT.IEpej2yPYyaDwnpj9O_KzC&pid=News",
          "height" => 367,
          "width" => 700
        }
      },
      "name" => "李漢東氏さん死去 86歳 金大中政権時代の元韓国首相",
      "provider" => [%{"_type" => "Organization", "name" => "mainichi.jp"}],
      "url" => "https://mainichi.jp/articles/20210508/k00/00m/030/247000c"
    },
    %{
      "about" => [
        %{
          "name" => "Yahoo!",
          "readLink" => "https://api.bing.microsoft.com/api/v7/entities/420cd466-c4de-0a7c-0139-d304079cd3e3?setLang=ja-JP"
        }
      ],
      "category" => "Japan",
      "datePublished" => "2021-05-08T13:22:00.0000000Z",
      "description" => "新型コロナウイルスについて県と宇都宮市は8日、新たに51人の感染を発表しました。 1日当たりの感染者の発表が50人を超えるのは今年1月22日以来で、およそ3カ月半ぶりです。 新たに感染が確認されたのは宇都宮市、小山市、栃木市、佐野市、さくら市、日光市、那須塩原市、大田原市、真岡市、下野市、上三川町、茨城県の10代から80代までの男女51人です。 このうち、23人が今のところ、感染経路が分かっていま",
      "headline" => true,
      "name" => "栃木県内 新たに51人感染 3ヵ月半ぶり50人超 新型コロナ 8日発表",
      "provider" => [
        %{"_type" => "Organization", "name" => "Yahoo!ニュース"}
      ],
      "url" => "https://news.yahoo.co.jp/articles/b6b021f884fc9b10ebc6979e0d5806cda45004e3"
    },
    %{
      "about" => [
        %{
          "name" => "Tokyo",
          "readLink" => "https://api.bing.microsoft.com/api/v7/entities/cb44a92f-6c6f-99c4-2ae3-51601fdc919a?setLang=ja-JP"
        }
      ],
      "ampUrl" => "https://www.tokyo-np.co.jp/amp/article/102969",
      "category" => "Entertainment",
      "datePublished" => "2021-05-08T09:20:00.0000000Z",
      "description" => "人気アイドルグループ「関ジャニ∞」の元メンバーで、ソロアーティストの渋谷すばるさん(39)が8日、ファンクラブ向けのブログで結婚したことを発表した。 渋谷さんは2004年から「関ジャニ∞」のメインボーカルとして活躍し、18年にジャニーズ事務所を退所すると発表。19年からソロアーティストとして活動している。 電子版ログイン",
      "headline" => true,
      "image" => %{
        "thumbnail" => %{
          "contentUrl" => "https://www.bing.com/th?id=OVFT.FBHZ-rI1S27zQB0KzCKR5i&pid=News",
          "height" => 367,
          "width" => 700
        }
      },
      "name" => "渋谷すばるさんが結婚を発表 「関ジャニ∞」元メンバー",
      "provider" => [%{"_type" => "Organization", "name" => "東京新聞"}],
      "url" => "https://www.tokyo-np.co.jp/article/102969"
    },
    %{
      "datePublished" => "2021-05-08T08:36:00.0000000Z",
      "description" => "通算1500奪三振を達成したオリックス・能見=ZOZOマリンスタジアム(撮影・田村亮介) 11. 禁煙できぬ夫 妻の「最終手段」 12. 無印の紙ナプキン コロナ対策に? 13. みりんで作る生キャラメルが旨い 14. 痛快報復 サレ妻「執念」の復讐 15. アットホーム? ブラック企業経験 16. しまむらにカリスマモデルの新作 17. 毛穴の黒ずみ やりがちNG行為 18. 買ってよかった無印",
      "headline" => true,
      "image" => %{
        "thumbnail" => %{
          "contentUrl" => "https://www.bing.com/th?id=OVFT.z7pyv9yBM4DPN6Sx5FvRUi&pid=News",
          "height" => 450,
          "width" => 294
        }
      },
      "name" => "オリックス・能見がプロ通算1500奪三振",
      "provider" => [%{"_type" => "Organization", "name" => "livedoor NEWS"}],
      "url" => "https://news.livedoor.com/article/image_detail/20157727/?img_id=29146641"
    },
    %{
      "about" => [
        %{
          "name" => "Yahoo!",
          "readLink" => "https://api.bing.microsoft.com/api/v7/entities/420cd466-c4de-0a7c-0139-d304079cd3e3?setLang=ja-JP"
        }
      ],
      "category" => "Japan",
      "datePublished" => "2021-05-08T13:59:00.0000000Z",
      "description" => "5月8日夜、北海道札幌市中央区で、灯油タンク周辺が燃える火事があり、現場は一時騒然となりました。 火事があったのは、札幌市中央区宮の森2条14丁目の住宅です。 5月8日午後9時30分ごろ、「住宅の灯油タンクが燃えている」と目撃者から消防に通報がありました。 消防車8台が駆け付け、火は約20分後に消し止められ、住宅の外壁が一部焼けました。ケガをした人はいませんが、住宅街は一時騒然となりました。 警察",
      "headline" => true,
      "name" => "「灯油タンクが燃えている」目撃者から通報…\"たばこ\"不始末か ...",
      "provider" => [
        %{"_type" => "Organization", "name" => "Yahoo!ニュース"}
      ],
      "url" => "https://news.yahoo.co.jp/articles/c5043a26572f14d8984c5562b67abc2e4cbec8ff"
    },
    %{
      "category" => "Japan",
      "datePublished" => "2021-05-08T10:38:00.0000000Z",
      "description" => "8日午後、長野県軽井沢町で、走行中の北陸新幹線がクマと衝突し緊急停止しました。JR東日本によりますとけが人はなく、30分ほどで運転を再開しました。",
      "headline" => true,
      "name" => "北陸新幹線がクマと衝突 けが人なし 冬眠終え活動時期入りか",
      "provider" => [%{"_type" => "Organization", "name" => "NHK"}],
      "url" => "http://www.nhk.or.jp/knews/20210508/k10013019591000.html"
    },
    %{
      "ampUrl" => "https://www.nikkansports.com/m/entertainment/news/amp/202105080000626.html",
      "datePublished" => "2021-05-08T08:30:00.0000000Z",
      "description" => "5人組アイドルユニットsherbetが8日、都内で、全国11都市ワンマンツアー「CrystalMemories」初日公演を開催し、全16曲を披露した。同ユニッ… - 日刊スポーツ新聞社のニュースサイト、ニッカンスポーツ・コム(nikkansports.com)",
      "headline" => true,
      "image" => %{
        "thumbnail" => %{
          "contentUrl" => "https://www.bing.com/th?id=OVFT.7_vLqepXPQbHIVT3o25NOi&pid=News",
          "height" => 427,
          "width" => 500
        }
      },
      "mentions" => [%{"name" => "Puerto Rico"}],
      "name" => "アイドルユニットsherbetが全国ツアー初日公演、全16曲を披露",
      "provider" => [%{"_type" => "Organization", "name" => "nikkansports.com"}],
      "url" => "https://www.nikkansports.com/entertainment/news/202105080000626.html"
    },
    %{ 
      "datePublished" => "2021-05-08T02:00:00.0000000Z",
      "description" => "米女子ゴルフのホンダLPGAは7日、タイ南部パタヤのサイアムCC(パー72)で第2ラウンドが行われ、20位で出た畑岡奈紗が7バーディー、2ボギーの67で回り、通算8アンダ... アプリで開く この記事は会員限定です。登録すると続きをお読みいただけます。",
      "headline" => true,
      "image" => %{
        "thumbnail" => %{
          "contentUrl" => "https://www.bing.com/th?id=OVFT.tNhV2rUr6U9W-nTSGFFtaC&pid=News",
          "height" => 430,
          "width" => 700
        }
      },
      "name" => "(短信)畑岡10位、渋野は28位 米女子ゴルフ",
      "provider" => [%{"_type" => "Organization", "name" => "NIKKEI"}],
      "url" => "https://www.nikkei.com/article/DGKKZO71678110X00C21A5UU8000/"
    },
    %{
      "ampUrl" => "https://www.nikkansports.com/m/battle/news/amp/202105080000409.html",
      "datePublished" => "2021-05-08T05:25:00.0000000Z",
      "description" => "「カネロ」の愛称を持つボクシングの人気スター選手のWBAスーパー、WBC世界スーパーミドル級王者サウル・アルバレス(30=メキシコ)が約5000人のファンに向… - 日刊スポーツ新聞社のニュースサイト、ニッカンスポーツ・コム(nikkansports.com)",
      "headline" => true,
      "image" => %{
        "thumbnail" => %{
          "contentUrl" => "https://www.bing.com/th?id=OVFT.OS2IZgB2W7djo3U5pa3CYC&pid=News",
          "height" => 316,
          "width" => 500
        }
      },
      "name" => "アルバレス、3団体統一戦の前日計量パス ファンの赤ちゃんにキス ...",
      "provider" => [%{"_type" => "Organization", "name" => "nikkansports.com"}],
      "url" => "https://www.nikkansports.com/m/battle/news/202105080000409_m.html"
    },
    %{
      "ampUrl" => "https://www.47news.jp/amp/6220661.html",
      "datePublished" => "2021-05-08T04:09:00.0000000Z",
      "description" => "東京五輪の聖火リレーは7日、全国20府県目となる長崎県の初日を迎えた。",
      "headline" => true,
      "image" => %{
        "thumbnail" => %{
          "contentUrl" => "https://www.bing.com/th?id=OVFT.D6hg051vveFGuaNL1pJnyS&pid=News",
          "height" => 316,
          "width" => 474
        }
      },
      "name" => "石原さとみさん登場 聖火、長崎県1日目",
      "provider" => [%{"_type" => "Organization", "name" => "47NEWS"}],
      "url" => "https://www.47news.jp/6220661.html",
      "video" => %{
        "name" => "石原さとみさん登場 聖火、長崎県1日目",
        "thumbnail" => %{"height" => 200, "width" => 300},
        "thumbnailUrl" => "https://www.bing.com/th?id=OVF.3MCsMK4rpaB9tS%2Btev%2FHzA&pid=News"
      }
    }
  ]
}

7. Text to Speechで文字列を音声データにする (おまけ)

cognitive_services/text_to_speech.ex
defmodule Azure.CognitiveServices.TextToSpeech do
  @moduledoc """
  Documentation for `Azure.TextToSpeech`.
  """

  @region "japaneast"
  @subscription_key "ひみつ"
  @audio_list ~w(
    raw-16khz-16bit-mono-pcm            raw-8khz-8bit-mono-mulaw
    riff-8khz-8bit-mono-alaw            riff-8khz-8bit-mono-mulaw
    riff-16khz-16bit-mono-pcm           audio-16khz-128kbitrate-mono-mp3
    audio-16khz-64kbitrate-mono-mp3     audio-16khz-32kbitrate-mono-mp3
    raw-24khz-16bit-mono-pcm            riff-24khz-16bit-mono-pcm
    audio-24khz-160kbitrate-mono-mp3    audio-24khz-96kbitrate-mono-mp3
    audio-24khz-48kbitrate-mono-mp3     ogg-24khz-16bit-mono-opus
    raw-48khz-16bit-mono-pcm            riff-48khz-16bit-mono-pcm
    audio-48khz-96kbitrate-mono-mp3     audio-48khz-192kbitrate-mono-mp3)
  @user_agent "awesome"
  @options [timeout: 50_000, recv_timeout: 50_000]

  def voices_list do
    voices_list_endpoint()
    |> HTTPoison.get!(authorization_header(access_token()), @options)
    |> Map.get(:body)
    |> Jason.decode!()
  end

  def audio_list do
    @audio_list
  end

  def ssml(text, %{"Name" => name, "Locale" => locale, "Gender" => gender}) do
    """
    <speak version='1.0' xml:lang='#{locale}'>
      <voice xml:lang='#{locale}' xml:gender='#{gender}' name='#{name}'>
        #{text}
      </voice>
    </speak>
    """
  end

  def to_speech_of_standard_voice(ssml, audio \\ "riff-24khz-16bit-mono-pcm") do
    to_speech_of_neural_voice(ssml, audio)
  end

  def to_speech_of_neural_voice(ssml, audio \\ "riff-24khz-16bit-mono-pcm") do
    standard_and_neural_voice_endpoint()
    |> HTTPoison.post!(ssml, headers(audio), @options)
    |> Map.get(:body)
  end

  def to_speech_of_custom_voice do
    # TODO
  end

  def to_speech_of_long_audio do
    # TODO
  end

  defp access_token do
    %{token: token, time: time} = Azure.CognitiveServices.TextToSpeech.AccessTokenAgent.get()

    if DateTime.diff(DateTime.now!("Etc/UTC"), time) > 60 * 9 do
      get_access_token()
      |> Azure.CognitiveServices.TextToSpeech.AccessTokenAgent.update()

      access_token()
    else
      token
    end
  end

  defp get_access_token do
    headers = [
      "Ocp-Apim-Subscription-Key": @subscription_key,
      "Content-type": "application/x-www-form-urlencoded"
    ]

    issue_token_endpoint()
    |> HTTPoison.post!("", headers, @options)
    |> Map.get(:body)
  end

  defp issue_token_endpoint do
    "https://#{@region}.api.cognitive.microsoft.com/sts/v1.0/issueToken"
  end

  defp voices_list_endpoint do
    "https://#{@region}.tts.speech.microsoft.com/cognitiveservices/voices/list"
  end

  defp standard_and_neural_voice_endpoint do
    "https://#{@region}.tts.speech.microsoft.com/cognitiveservices/v1"
  end

  defp authorization_header(token) do
    [Authorization: "Bearer #{token}"]
  end

  defp headers(audio) do
    access_token()
    |> authorization_header()
    |> Keyword.merge(
      "Content-Type": "application/ssml+xml",
      "X-Microsoft-OutputFormat": audio,
      "User-Agent": @user_agent
    )
  end
end
lib/azure/cognitive_services/text_to_speech/access_token_agent.ex
defmodule Azure.CognitiveServices.TextToSpeech.AccessTokenAgent do
  use Agent

  def start_link(_initial_value) do
    Agent.start_link(fn -> %{token: nil, time: DateTime.from_unix!(0)} end, name: __MODULE__)
  end

  def get, do: Agent.get(__MODULE__, & &1)

  def update(token) do
    Agent.update(__MODULE__, fn _ ->
      %{token: token, time: DateTime.now!("Etc/UTC")}
    end)
  end
end
lib/honeko_pack/application.ex
  def start(_type, _args) do
    children = [
      # Starts a worker by calling: HonekoPack.Worker.start_link(arg)
      # {HonekoPack.Worker, arg}
      Azure.CognitiveServices.TextToSpeech.AccessTokenAgent,
    ]
  • Ctl + c を2回おして一度IExを終了させてください

実行結果

  • ここまでを動かしてみましょう
pi@raspberrypi:~/projects/honeko_pack $ iex -S mix

iex> locale = "ja-JP"

iex> voice_type = "Neural"

iex> gender = "Female"

iex> voice = (
Azure.CognitiveServices.TextToSpeech.voices_list()
|> Enum.filter(fn %{"Locale" => l} -> l == locale end)
|> Enum.filter(fn %{"VoiceType" => vt} -> vt == voice_type end)
|> Enum.filter(fn %{"Gender" => g} -> g == gender end)
|> Enum.random()
)

iex> path = "output.wav"

iex> (
Azure.CognitiveServices.TextToSpeech.ssml("団長! いつでもどこでも温度・湿度が測れるのであります! (Elixir, SORACOM Air for セルラー)", voice)
|> Azure.CognitiveServices.TextToSpeech.to_speech_of_neural_voice()
|> (&File.write(path, &1)).()
)

iex> :os.cmd('aplay #{path}')
  • 音声が読み上げられましたでしょうか! :speaker:

8. これまで作ったものを組み合わせたり、自動的に実行したりするようにします

lib/honeko_pack.ex
defmodule HonekoPack do
  @default_voice_path "output"

  def run do
    me = self()

    [&make_top_news_voice_file/0, &make_weather_forecast_voice_file/0]
    |> Enum.map(fn f ->
      spawn_link(fn -> send(me, {self(), f.()}) end)
    end)
    |> Enum.map(fn pid ->
      receive do
        {^pid, result} ->
          result
      end
    end)
    |> Enum.map(&play/1)
  end

  def make_weather_forecast_voice do
    make_weather_forecast()
    |> make_voice()
  end

  def make_weather_forecast do
    Weather.Forecast.get()
    |> Map.get("text")
  end

  def make_weather_forecast_voice_file do
    do_something_and_create_file(:make_weather_forecast_voice)
  end

  def make_top_news_voice do
    make_top_news()
    |> make_voice()
  end

  def make_top_news do
    Azure.Bing.NewsSearch.top_news()
    |> Map.get("description")
  end

  def make_top_news_voice_file do
    do_something_and_create_file(:make_top_news_voice)
  end

  def play(path \\ @default_voice_path) do
    do_play(path, :os.type())
  end

  def make_voice(text) do
    Azure.CognitiveServices.TextToSpeech.ssml(text, select_voice())
    |> Azure.CognitiveServices.TextToSpeech.to_speech_of_neural_voice()
  end

  def select_voice(opts \\ []) do
    locale = Keyword.get(opts, :locale) || "ja-JP"
    voice_type = Keyword.get(opts, :voice_type) || "Neural"
    gender = Keyword.get(opts, :gender) || "Female"

    Azure.CognitiveServices.TextToSpeech.voices_list()
    |> Enum.filter(fn %{"Locale" => l} -> l == locale end)
    |> Enum.filter(fn %{"VoiceType" => vt} -> vt == voice_type end)
    |> Enum.filter(fn %{"Gender" => g} -> g == gender end)
    |> Enum.random()
  end

  defp do_play(path, {:unix, :darwin}) do
    :os.cmd('afplay #{path}')
  end

  defp do_play(path, _) do
    :os.cmd('aplay #{path}')
  end

  defp do_something_and_create_file(function_name) do
    path = Atom.to_string(function_name) <> ".wav"

    apply(__MODULE__, function_name, [])
    |> (&File.write(path, &1)).()

    path
  end
end
lib/honeko_pack/worker/aht20_agent.ex
defmodule HonekoPack.Worker.Aht20Agent do
  use Agent

  def start_link(_initial_value) do
    Agent.start_link(fn -> %{temperature: 0, humidity: 0, time: 0} end, name: __MODULE__)
  end

  def get, do: Agent.get(__MODULE__, & &1)

  def update(temp, hum, time) do
    Agent.update(__MODULE__, fn _ ->
      %{temperature: temp, humidity: hum, time: time}
    end)
  end
end
lib/honeko_pack/worker.ex
defmodule HonekoPack.Worker do
  @url "http://beam.soracom.io:8888"
  @headers [{"Content-Type", "application/json"}]
  @options [timeout: 50_000, recv_timeout: 50_000]

  def run do
    aht20()
  end

  defp aht20 do
    {:ok, {temperature, humidity}} = aht20_read(:os.type())
    time = DateTime.utc_now() |> DateTime.to_unix()
    HonekoPack.Worker.Aht20Agent.update(temperature, humidity, time)
    # post()
    if rem(time, 10) == 0, do: post()
  end

  defp aht20_read({:unix, :darwin}) do
    # debug on macOS
    temperature = 10..20 |> Enum.random()
    humidity = 20..50 |> Enum.random()
    {:ok, {temperature, humidity}}
  end

  defp aht20_read(_) do
    Aht20.Reader.read()
  end

  defp post do
    %{temperature: temperature, humidity: humidity, time: time} =
      HonekoPack.Worker.Aht20Agent.get()

    json = Jason.encode!(%{value: %{temperature: temperature, humidity: humidity, time: time}})
    HTTPoison.post(@url, json, @headers, @options)
  end
end

04-soracom-beam.png

lib/honeko_pack/ticker.ex
defmodule HonekoPack.Ticker do
  use GenServer

  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
    spawn(HonekoPack.Worker, :run, [])

    {:noreply, state}
  end
end
  • HonekoPack.Worker.run/0を1秒に1回動かすモジュール
lib/honeko_pack/scheduler.ex
defmodule HonekoPack.Scheduler do
  use Quantum, otp_app: :honeko_pack
end
lib/honeko_pack/application.ex
  def start(_type, _args) do
    children = [
      # Starts a worker by calling: HonekoPack.Worker.start_link(arg)
      # {HonekoPack.Worker, arg}
      Azure.CognitiveServices.TextToSpeech.AccessTokenAgent,
      HonekoPack.Ticker,
      HonekoPack.Worker.Aht20Agent,
      HonekoPack.Scheduler
    ]
config/config.exs
import Config

config :honeko_pack, HonekoPack.Scheduler,
  jobs: [
    {"30 21 * * *", {HonekoPack, :run, []}}
  ]
  • 日本時間の06:30にHonekoPack.run/0が実行されます
  • つまり、とれたて新鮮ニュースと今日の天気を読み上げてくれる目覚ましアラームです

⑧ Run (イゴかす)

  • 電源をモバイルバッテリーにしましょう
  • L-02CはRaspberry Pi OSが立ち上がってから挿したほうがよいという記事をみましたので、私もそうしています
    • L-02Cを挿したまま立ち上げるとうまく立ち上がらないといった事象が起こる場合があるそうです
pi@raspberrypi:~ $ sudo wvdial &

pi@raspberrypi:~ $ sudo route del default
pi@raspberrypi:~ $ sudo route add default dev ppp0

pi@raspberrypi:~ $ cd /home/pi/projects/honeko_pack
pi@raspberrypi:~/projects/honeko_pack $ nohup iex -S mix &

E0Y2GqDVgAInS0U.jpeg

02-Anywhere-can-be-measuredのコピー.png

Wrapping up :lgtm::lgtm::lgtm::lgtm::lgtm:

  • L-02Cで通信をする場合は、AT+COPS=1,2,"44010"しておくといいことあるかもしれません
    • まとめに書くにしてはあんまり自信がある書き方をできないのですが、少なくとも悪くなることはないとおもいます
    • だってNTTドコモさんから販売されたデータ通信端末なのですもの
  • Elixirのプログラムを作るところは駆け足になりました
    • ちゃんと説明できていませんが感じてください!
    • コードの中でさらっと使っている|>はパイプ演算子と呼ばれるもので、前の計算結果を次の関数の第一引数にいれてくれます
    • これが気持ちいいんですよ
    • 私は感覚でしか語れません
    • |>のよさに関するちゃんとした説明は、@zacky1972先生の大学でElixirを教えた話をご参照ください
  • この記事を書くために、取り組んだいろいろなこと(特にATコマンド)は、あーでもない、こーでもないと試行錯誤を繰り返しまして、間違いなく私の普段の生活を豊かにしてくれました
    • コンテストをきっかけにはじめてSORACOMさんのサービスを触りました
    • そうしたきっかけをいただけて本当にありがとうございます!
  • あなたのIoT開発にもElixirを使ってみませんか:bangbang:
  • Elixirを使ってみようとおもってくださった方がいらっしゃったらうれしいです
  • Enjoy Elixir :bangbang::bangbang::bangbang:
  • 心臓を捧げよ!
  • 必ず星をあげる!
  • はい!

  1. 進撃の日田というスマホアプリ 

  2. https://www.hontai.or.jp/find/vote2021.html 

  3. 大山ダムで撮影したときはまだSORACOM Beamは使っておらず、直接Azure 仮想マシンへPOSTしていました 

17
8
0

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
17
8