iOSウィジェットにCO2濃度を表示する 【Raspberry Pi × CO2-mini × co2meter】

WFH、捗ってますでしょうか。

我が家の場合、机やモニターなど家で仕事をするのに十分な設備は元々整っていたのですが、たったひとつだけオフィスにはあって自宅にないものがありました。

CO2モニターです。

## 成果物
パッと目につくところに表示させたかったので、iOSのウィジェットに置くことにしました。
最終的にこんな感じでCO2濃度、ついでに室温が見えるようになりました。

### iPhone
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145728/1a863164-9e85-5266-1f63-2ad4bdc20ab3.jpeg" width=380>

### iPad
<img width="1532" alt="スクリーンショット 2020-04-26 22.34.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145728/ec90302c-56ef-2047-0b5a-cdb770f24735.png">

### 構成と概要解説

構成はこんな感じです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145728/10c88a06-7d5e-ccf2-b20c-4a0951f5be8a.png)

ポイントは以下です。

* CO2-miniでCO2濃度、室温を計測する
* CO2-miniとRaspberry Piを接続して定期的な計測を行い、Webサーバーとしてアクセス可能にする
* iOSアプリからWebサーバーへアクセスし、ウィジェットとして表示する
* 換気したくなる

### CO2-mini
CO2濃度の計測は既製品であるこちらを使います。
[CO <sub>2</sub>モニター CO2-mini | 自然環境測定器 - 製品情報 - 計測器のカスタム](https://www.kk-custom.co.jp/emp/CO2-mini.html)

MH-Z19などのモジュールも検討したのですが、WFHによる需要の高まりのためか高価になってる or 配送に時間がかかるため、比較的手に入りやすいこちらを選択しました。

電源供給がmicro USB Type-Bなのでこれを利用してPaspberry Piと接続します。
公式でAPIが公開されているわけではありませんが、有志の解析によってOSSなどを経由してアクセスできるようになっています。

なお、今回はCO2-miniに予め備わっている表示部を見て換気したくなる行為は反則負けとします。

### サーバーサイド
主となるCO2濃度のロギング、Webサーバー化は `co2meter` を使います。
https://github.com/vfilimonov/co2meter

このOSSを使うことで、Pythonを使ったCO2-miniへのアクセスが可能になります。さらに定期的な計測、JSON/CSVへの書き出し、FlaskによるWebサーバー化、折れ線グラフによる可視化まで担ってくれます。

### クライアントサイド
iOSアプリ部分については自作して、簡単なものですが公開しました。
https://github.com/akeome/RoomCondition

ラズパイで構築したサーバーにアクセスして、レスポンスのJSONの最新値を表示するだけのものです。

# CO2濃度を見える化する
この環境を構築するための手順を記します。
僕は今回初めてRaspberry Piをいじったので、備忘も兼ねてセットアップの記載から始めます。

## 準備
必要なハード類です。

- Mac
- Raspberry Pi 
  - 今回はRaspberry Pi Zero WHを使いました。Wi-Fiに接続できればどのモデルでも大丈夫なはず
- CO2-mini
- microSDカード
- アダプタ、ケーブル等
  - MacからmicroSDに書き込むためのハブなどが必要です
  - Raspberry PiとCO2-miniの接続は、micro USB Type-Bとmicro USB Type-Bです
  - Raspberry Pi Zero WHの場合、電源供給はmicro USB Type-Bです

## Raspberry Pi のセットアップ
今回使用したRaspberry Pi Zero WHには有線LANポートがないので、Wi-Fi経由でMacから操作を行うための手順を書きます。

### MacからmicroSDに書き込む
Paspberry PiにmicroSDを挿す前に、Macで諸々書き込んでいきます。

#### OSを書き込む

公式サイトからOSをダウンロードします。
https://www.raspberrypi.org/downloads/raspbian/

- GUIは使わないので軽量な `Raspbian Buster Lite` にしました
- めっちゃ時間かかります(1時間ぐらいかかりました)

`diskutill list` コマンドでMacに接続されているmicroSDのパスを取得します。

```
$ diskutil list
# （中略）
/dev/disk4 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *32.0 GB    disk4
   1:             Windows_FAT_32 NO NAME                 32.0 GB    disk4s1
```

自分の環境の場合、32.0 GBの表示から `/dev/disk4` が該当のmicroSDを指すとわかりました。

`diskutil unmountDisk` でアンマウントと、 `dd` でOSの書き込みを行います。

```
$ diskutil unmountDisk /dev/disk4
Unmount of all volumes on disk4 was successful

$ sudo dd bs=1m if=/👉パス/2020-02-13-raspbian-buster-lite.img of=/dev/rdisk4 conv=sync
Password:
1764+0 records in
1764+0 records out
1849688064 bytes transferred in 55.800569 secs (33148194 bytes/sec)
```

- `/dev/disk4` としている箇所は環境に合わせて変えてください
- `👉パス` としている箇所はOSをダウンロードしたパスです
- .imgファイルのファイル名も時期によって変わるはずです

Macに `boot` という名前でディスクが接続されていればOKです。

```
$ ls /Volumes/
Macintosh HD	boot
```

#### ssh接続を有効にする

MacからsshでRaspberry Piに（一時的に）接続するために空ファイルが必要です。持続的に接続する方法については後述します。

```
$ touch /Volumes/boot/ssh
```

#### Wi-Fi接続を有効にする

Wi-Fi接続に必要なファイルを作成します。

```
$ nano /Volumes/boot/wpa_supplicant.conf
```

以下の内容を書き込みます。

``` wpa_supplicant.conf
country=JP
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
    key_mgmt=WPA-PSK
    ssid="👉SSID名"
    psk="👉パスワード"
}
```

- SSID、パスワード（場合によってはkey_mgmtも）は環境に合わせて変えてください
- パスワードを直接入力するとセキュリティ上のリスクがあります。ここでは触れませんが `wpa_passphrase` コマンドで暗号化できます

もちろん `nano` コマンドを使わなくても、.confを作成できればおっけーです。

ここまででmicroSDへの書き込みは完了です。

### Paspberry Piを起動する
諸々書き込みが終わったmicroSDカードをRaspberry Piに差し込みます。
電源に接続します。Raspberry Pi Zero WHの場合、電源供給はPWRと書かれた方の差込口を使います。
電源が入ると緑のLEDが点灯します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145728/751e7bdb-c14d-b127-5188-c41fb3738706.jpeg" width="800">


`ssh` コマンドでMacからラズパイに接続します。
接続先は `pi@raspberrypi.local` 、パスワードは `raspberry` です。

```
$ ssh pi@raspberrypi.local

The authenticity of host 'raspberrypi.local (xxxx)' can't be established.
ECDSA key fingerprint is SHA256:xxxx.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'raspberrypi.local,xxxx' (ECDSA) to the list of known hosts.
pi@raspberrypi.local's password: 
```

```
pi@raspberrypi:~ $ 
```

こんな感じでプロンプトがラズパイになっていれば成功です🎉

続いて諸々の設定をやっていきます。

#### タイムゾーンを設定する

ラズパイの設定画面に入るコマンドを使います。

```
pi@raspberrypi:~ $ sudo raspi-config
```

<img width="800" alt="スクリーンショット 2020-04-29 0.38.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145728/017673b2-73c3-63c5-660f-d61f4d16b40e.png">


`4 Localisation Options > I2 Change Timezone > Asia > Tokyo`
でタイムゾーンを日本にしておきます。

#### ssh接続を継続して利用可能にする

上記設定画面から、
`5 Interfacing Options > P2 SSH > Yes`
でssh接続が持続的に利用可能になります。

#### IPアドレスを固定する

`ifconfig` で現在のIPアドレスを、 `route -n` でデフォルトゲートウェイを確認します。

```
pi@raspberrypi:~ $ ifconfig
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 4032  bytes 203013 (198.2 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 4032  bytes 203013 (198.2 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

wlan0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.100.50  netmask 255.255.255.0  broadcast 192.168.100.255

pi@raspberrypi:~ $ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.100.254 0.0.0.0         UG    302    0        0 wlan0
```

無線LANはwlan0なので、上記の場合 `192.168.100.50` が現在のIPです。
また、Gatewayに表示された `192.168.100.254` が現在のデフォルトゲートウェイです。

確認できたら `sudo nano /etc/dhcpcd.conf` で以下の内容を追記します。

``` dhcpcd.conf
interface wlan0
static ip_address=192.168.100.50/24
static routers=192.168.100.254
static domain_name_servers=192.168.100.254
```

- ip_addressに固定するIPアドレスを入力します。今回は `ifconfig` で確認した `192.168.100.50` を使っています。サブネットマスクは基本的にはそのまま `/24` でいいはずです 
- routersとdomain_name_serversには `route -n` で確認したデフォルトゲートウェイを入力します

再起動後、設定したIPアドレスになっていればおっけーです。

``` bash
# 再起動
pi@raspberrypi:~ $ sudo reboot

# 再接続
$ ssh pi@raspberrypi.local

# IP確認
pi@raspberrypi:~ $ ifconfig

wlan0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.100.50  netmask 255.255.255.0  broadcast 192.168.100.255
```

#### 各種ライブラリをアップデートする

ラズパイのセットアップでのお決まりのようです。

```
pi@raspberrypi:~ $ sudo apt update
pi@raspberrypi:~ $ sudo apt upgrade
```

#### デフォルトのPythonのバージョンを3系にする

これは必須ではありませんが、 `python` コマンドのデフォルトで Python2系が起動したので3系にしておきます。

```
pi@raspberrypi:~ $ python --version
Python 2.7.16

pi@raspberrypi:~ $ sudo unlink /user/bin/python

pi@raspberrypi:~ $ sudo ln -s /user/bin/python3 python

pi@raspberrypi:~ $ python --version
Python 3.7.3
```

#### Gitをインストールする

Gitをインストールします。ライブラリの使用に必要です。

```
pi@raspberrypi:~ $ sudo apt install git

pi@raspberrypi:~ $ git --version
git version 2.20.1
```


## Raspberry Pi と CO2-mini の接続

ラズパイとCO2-miniを接続します。
接続端子はお互いmicro USB Type-Bです。
接続できるとCO2-miniはしばらくの待機後、CO2濃度と室温を表示してくれます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145728/e0d53990-1a29-88a7-9486-9df611223eb3.jpeg" width="800">

### `co2meter` の事前準備

Python製のライブラリ `co2meter` を使って、ラズパイからCO2-miniへアクセスします。
[vfilimonov/co2meter: A Python library for USB CO2 meter](https://github.com/vfilimonov/co2meter)

このco2meterを動かすにあたって必要なライブラリをインストールしていきます。

#### 依存ライブラリのインストール

USB接続機器にアクセスしたりするために必要になるみたいです。

```
pi@raspberrypi:~ $ sudo apt install libusb-1.0-0-dev libudev-dev
```

#### rulesファイルの作成

`/etc/udev/rules.d/98-co2mon.rules` を作成します。

```
root@raspberrypi:/home/pi# nano /etc/udev/rules.d/98-co2mon.rules
```

以下を書き込みます。

``` 98-co2mon.rules
KERNEL=="hidraw*", ATTRS{idVendor}=="04d9", ATTRS{idProduct}=="a052", GROUP="plugdev", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="04d9", ATTRS{idProduct}=="a052", GROUP="plugdev", MODE="0666"
```

`udevadm` コマンドで設定を反映させます。
が、この操作はrootユーザーとしての実行が必要らしく、まずはrootユーザーになるためにパスワードを設定します。

```
pi@raspberrypi:~ $ sudo passwd root
New password: 
Retype new password: 
passwd: password updated successfully
```

- パスワードを2度聞かれるので2度入力します

`su` コマンドでrootユーザーに変身します。

```
pi@raspberrypi:~ $ su
Password: 

root@raspberrypi:/home/pi# 
```

プロンプトが `root@raspberrypi:` になっていればおっけーです。
この状態で `udevadm` コマンドを実行します。

```
root@raspberrypi:/home/pi/# sudo udevadm control --reload-rules && udevadm trigger
```

rootユーザーから一般ユーザーに戻るには `exit` です。

```
root@raspberrypi:/home/pi# exit
exit

pi@raspberrypi:~ $ 
```

ここまでで事前準備は完了です。

### ライブラリ `co2meter` のインストール

これによっていよいよCO2-miniへのアクセスが可能になります。

```
pi@raspberrypi:~ $ pip3 install hidapi co2meter
```

README記載の基本的な使い方を試してみます。

```
pi@raspberrypi:~ $ python
```

``` python
>>> import co2meter as co2
>>> mon = co2.CO2monitor()
>>> mon.read_data()
(datetime.datetime(2020, 4, 25, 11, 10, 21), 1005, 24.100000000000023)
```

取得できました🎉🎉
内容は `(タイムスタンプ, CO2濃度, 室温)` のタプルです。

## Raspberry Pi をWebサーバーとして動かす

CO2濃度、室温が取得できることがわかったところで、続いてWebサーバーとして動かす方法を記載します。Webサーバーとして動かすことで、外部からブラウザ経由で計測結果を確認することができます。

`co2meter` には、予めサーバーとして動かす機能が実装されているので利用します。

- 定期的な計測
- JSON/CSVへのロギング
- 折れ線グラフによる可視化
- Webサーバー立ち上げ

といった機能が簡単に使えます。

### 関連パッケージのインストール

#### flask、pandasのインストール

`co2meter` のWebサーバー化はflaskで実装されています。

```
pi@raspberrypi:~ $ pip3 install -U flask pandas
```

### Webサーバー起動

`co2meter` をサーバーとして実行するには `co2meter_server` コマンドだけでOKです。
これと組み合わせて、バックグラウンドでプロセスを継続させるために `nohup` コマンドと `&` オプションをつけて実行します。

```
pi@raspberrypi:~ $ nohup co2meter_server -H 0.0.0.0 -P 1201 &
``` 

- `-H 0.0.0.0` オプションをつけることで外部のブラウザから（例えば同じLAN内のMacから）接続できます
- `-P 1201` オプションはポート番号です。デフォルトで使われていた1201に指定していますが数字に意味はないです

これで、ブラウザから `http://ラズパイのIPアドレス:ポート番号` (この手順通りに進めていれば `http://192.168.100.50:1201/` )へアクセスしてCO2濃度、室温が見れるようになりました🎉🎉🎉

<img width="1280" alt="スクリーンショット 2020-04-26 18.31.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145728/621b9257-663f-24cf-4e72-5b53500bee97.png">

<img width="1280" alt="スクリーンショット 2020-04-26 18.32.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145728/c3ef2147-c6e9-100d-6440-de73a01df058.png">

サーバーとして動かすことでlogsディレクトリが作成され、CSVでログが蓄積されていきます。
計測間隔はデフォルトで約35秒のようです。

```
pi@raspberrypi:~ $ cat logs/co2.csv 

timestamp,co2,temp
2020-04-26 15:01:59,772,23.9
2020-04-26 15:02:34,768,23.9
2020-04-26 15:03:09,766,23.9
2020-04-26 15:03:44,766,23.9
2020-04-26 15:04:20,760,24.0
```


### Webサーバーの停止

停止させるには、 `ps` コマンドでPIDを確認して `kill` コマンドです。 `-9` オプションは強制終了です。

```
pi@raspberrypi:~ $ ps
  PID TTY          TIME CMD
  599 pts/0    00:00:00 bash
  626 pts/0    00:23:44 co2meter_server
 6201 pts/0    00:00:00 ps

pi@raspberrypi:~ $ kill -9 626
```

プロセスが表示されない場合は `x` オプションをつけて `ps x` で試してみてください。

## iOSウィジェットに表示する

ここまでの手順で念願のCO2濃度可視化は実現できました。
あとはどこにどんな感じで表示するかですが、今回はWebサーバーから返されるJSONを使ってiOSウィジェットに表示することにしました。

できたソースはこちら
https://github.com/akeome/RoomCondition

Xcodeからこのプロジェクトをビルドすれば動くはずです。

- Targetはまず本体アプリ `RoomCondition` 、次にウィジェット `RoomConditionTodayExtension` の順番でビルドします
- 接続先のIPアドレス、ポート番号は `http://192.168.100.50:1201/` にしています。環境に合わせて適宜変更してください。この記事通りに進めていればそのままで問題ないです

### ちょっとコード解説

ウィジェット（Today Extension）は今回初めて実装してみたので知見を書いておきます。

#### API通信部分のコード共通化

本体アプリ、ウィジェットで共通して使う処理はFrameworkとして実装することで共通化できます。

Frameworkの作成はメニューの `File > New > Target > Framework` です。
今回は `APIRequest` という名前で作成しました。

``` swift:RoomCondition.swift
public struct RoomCondition: Codable {
    public let co2: String
    public let temp: String
    public let timestamp: String
}
```

``` swift:APIRequest.swift
public struct API {
    
    public static func request(completion: @escaping (RoomCondition?) -> Void) {
        
        let url = "http://192.168.100.50:1201/log.json"
        
        let session = URLSession.shared
        let task = session.dataTask(with: URL(string: url)!) { data, urlResponse, error in
            
            let currentCondition = try! JSONDecoder().decode([RoomCondition].self, from: data!)
            completion(currentCondition.last)
        }
        
        task.resume()
    }
}
```

呼び出し側では `import APIRequest` して使います。

``` swift:呼び出し側.swift
API.request(completion: { roomCondition in
    guard let roomCondition = roomCondition else {
        return
    }
    
    print(roomCondition) // RoomCondition(co2: "1015", temp: "21.4", timestamp: "2020-04-28 00:35:32")
})
```

今回は結局Today Extension側でしか使ってないのでFrameworkにしなくてもよかったかもしれません😇

#### Today Extensionの更新契機について

ウィジェットが表示される度に `func widgetPerformUpdate(completionHandler: @escaping (NCUpdateResult) -> Void)` が呼び出されるようです。
この中に通信処理を書いているのですがちゃんと都度最新の値を取ってきてくれます。

`completionHandler` の引数には `NCUpdateResult` を渡します。

``` swift
public enum NCUpdateResult : UInt {
    case newData
    case noData
    case failed
}
```

上から順に、新規データあり、新規データなし（更新不要）、失敗を表します。
これによっていい感じにウィジェットの再描画が行われるようです。

#### 色分けのロジック

CO2-miniの表示部に着想を得て、ppmによって色分けしました。

``` swift:TodayViewController.swift
    func co2Color(co2Value: String) -> UIColor {
        switch Int(co2Value) {
        case (..<1000)?:
            return .systemGreen
        case (..<1200)?:
            return .systemYellow
        default:
            return .systemRed
        }
    }
```

**快適**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145728/8d33c282-499f-f959-71ab-03c37b2083b2.jpeg" width=320>

**そろそろ換気しよ**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145728/cd562df2-032b-e398-5ec3-ff1cc2673954.jpeg" width=320>

**あかん**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145728/d3d0c3f3-4891-d23e-5922-7a3c0ca43fbc.jpeg" width=320>

1000とか1200の基準は環境にもよると思うので適宜変更してください。
また室温の色分けは季節によって変えるべきかもしれません。

#### おまけ、本体アプリ

本体アプリを起動しても真っ白なのは寂しかったのでとりあえず `SFSafariViewController` でダッシュボートを表示させておきました。

``` swift:ViewController.swift
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let url = URL(string: "http://192.168.100.50:1201/dashboard")
        if let url = url {
            let safari = SFSafariViewController(url: url)
            present(safari, animated: false)
        }
    }
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145728/14f2119e-4e3f-fee8-a7af-217d97433422.jpeg" width=380>

# おわりに　〜二酸化炭素濃度が人体に与える影響について〜
一般的に空気中の二酸化炭素濃度が概ね1000ppmを超えると眠気や倦怠感を誘発すると言われています。

厚生労働省が規定する建築物環境衛生管理基準においても、建築物の管理者は1000ppmになるように空調設備をちゃんとしてね〜との記載があります。
[建築物環境衛生管理基準について｜厚生労働省](https://www.mhlw.go.jp/bunya/kenkou/seikatsu-eisei10/)

この基準値を検証する資料もありました。
1968年のWHO報告書を根拠としていると考えられる、との考察があったりなかなか興味深いです。
[建築物環境衛生管理基準の設定根拠の検証について] (https://www.mhlw.go.jp/seisakunitsuite/bunya/kenkou_iryou/kenkou/seikatsu-eisei/gijutukensyuukai/dl/h23_3.pdf) 
(※pdfです)

上記資料から一部抜粋

- 18ページ
> 二酸化炭素自体は、少量であれば人体に有害ではな
いが、1000ppmを超えると倦怠感、頭痛、耳鳴り、息苦しさ等の症
状を訴えるものが多くなり、フリッカー値（フリッカー値が小さいほ
ど疲労度が高い）の低下も著しい

- 19ページ
> 1000ppmのCO2の吸入実験(Eliseeva 1964)で呼吸、循環器系、大
脳の電気活動に変化がみられたと報告している。

街の噂で聞く1000ppmで眠くなる説はこのあたりがソースになっているのかもしれませんね。

ということで我が家のCO2濃度可視化の記録でした。
適度に換気して、快適なWFHを〜🏡

# 参考記事

参考にさせていただきました。先人の皆様ありがとうございます。

- [Raspberry Pi のセットアップ！モニター、キーボードなしでMacからSSH](https://101010.fun/posts/raspberry-pi-zero-install.html)
- [Raspberry Pi Zero W 環境構築ことはじめ MacOS編 - Qiita](https://qiita.com/hiesiea/items/67ead05a4c3aa5d47ce7)
- [Raspberry Pi Zero(W, WH)のセットアップ - Qiita](https://qiita.com/hishi/items/8bdfd9d72fa8fe2e7573)
- [Raspberry Pi Zero WHの環境構築(Mac PC) - 日々の学びのアウトプットするブログ](https://ushinji.hatenablog.com/entry/2019/08/12/231226)
- [Raspberry Pi に固定IPアドレスを割り当てる方法（Raspbian Jessie） - Qiita](https://qiita.com/marie_khr/items/b088ffb252a92eee8f5d)
- [RaspberryPiでPythonのデフォルトをPython2.7からPython3に変更する | そう備忘録](https://www.souichi.club/raspberrypi/python3-default/)
- [「apt-get」はもう古い？新しい「apt」コマンドを使ったUbuntuのパッケージ管理 | LFI](https://linuxfan.info/package-management-ubuntu)
- [How to Install Git on Raspberry Pi | Linuxize](https://linuxize.com/post/how-to-install-git-on-raspberry-pi/)
- [Python - Raspberry piでnumpyがimportできない。｜teratail](https://teratail.com/questions/225250)
- [nohupを使ってsshログアウト後もシェルスクリプトを動かす - Qiita](https://qiita.com/toshihirock/items/4a6b17a38f9b6e5e7116)
- [networking - Opening port not working - Unix & Linux Stack Exchange](https://unix.stackexchange.com/questions/438798/opening-port-not-working)
- [Today Extension Tutorial: Getting Started | raywenderlich.com](https://www.raywenderlich.com/697-today-extension-tutorial-getting-started)