【enebularの本気】enebularをフル活用して外出先から操作できるスマートエアコンを実現する

# enebularの本気
過去記事で enebular ＋ RaspberryPi + Slack を連携させて簡単な遠隔Lチカを実現しました。

| [【超入門】初めてのenebular!](https://qiita.com/TakedaHiromasa/items/5f89451b041e5674e378) <BR><BR><font color="DarkGray">eneblarの始め方から、"hello,world"に"Lチカ"と簡単なチュートリアル | <img width="100" alt="createproject.png" src="https://camo.qiitausercontent.com/d4b9f63d0a74e4fc34f66666ff2918931a8b77e5/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3235393631372f34613834373331352d663461662d653661612d306231372d6364343732303638326166392e706e67">|
|:--|:-:|

しかし、**「enebularの本気はまだまだこんなもんじゃない・・・」**と思い、なるべくenebularの特徴を十分に活かした開発をしてみました。

# 作るもの
今回は、スマホで外出先からも操作できるスマートエアコンを実現しました。
外出先から操作できるスマートエアコンがあれば家に帰る直前にエアコンをONにして、帰宅した時には部屋がポッカポカというわけです。
また、いつも持ち歩いているスマホで操作できるので、部屋のどこに置いたか忘れたリモコンを探し回る必要もありません。
私のように寒い地域 (岩手県：この記事を書いている時点で-9℃) に住む人には嬉しいデバイスだと思います。
<img width="600" src="https://qiita-image-store.s3.amazonaws.com/0/259617/e90e8047-0100-849f-7ec2-8bf5578bf02f.png">

# 完成したもの
で、できたものがこんな感じです。
動画で見ると地味ですが実際に動いた時は結構感動します。
![AIRCON.gif](https://qiita-image-store.s3.amazonaws.com/0/259617/368652d9-b758-ad13-4cce-b89565400801.gif)

# 全体の構成
### 構成を考える
エアコンを操作したり、部屋の温度を取得したりするのは`RaspberryPi Zero WH(ラズパイ)`に任せます。
そして、リモコンは`Webアプリ`として`Heroku`に作成します。

### enebularを活用する
しかし、こうなってくると**「enebularで作成したフローをラズパイのNode-REDにコピーして・・・」「ラズパイと連携するようにHerokuでアプリ作って・・・」**と、色んなサービスやハードウェアを行ったり来たりしなければいけません。
そこで、今回`ラズパイ`と`Heroku`のコーディングとデプロイを`enebular`のみで行います。
この、作成したフローの**Herokuへのデプロイ**、**AWS IoTを経由したRaspberryPiやmbedなどのハードへのデプロイ**が簡単にできることがenebularの特徴の一つでもあります。
<img width="700" src="https://qiita-image-store.s3.amazonaws.com/0/259617/77ac3b5f-31eb-e3b0-8d23-9417a963d179.png">

# 準備するもの
1. Raspberry Pi
1. 赤外線受光モジュール( [GP1UXC41QS](http://akizukidenshi.com/catalog/g/gI-06487/) )
1. 赤外線LED( [OSI5FU5111C-40](http://akizukidenshi.com/catalog/g/gI-03261) )
1. 電解コンデンサ `47μF`
1. 抵抗器 `130Ω` `47Ω`
1. ブレッドボード + ジャンパーワイヤ

用意する部品の型番を参考までに書きましたが、他の製品でも大体動くと思います。
私は、赤外線受光モジュールは [VS838](https://www.amazon.co.jp/dp/B00ZE9UKL6/ref=cm_sw_em_r_mt_dp_U_4n6sCbMGT2VG6) 、赤外線LEDはジャンクのリモコンから抜いたものを使って今回作りましたが、一応動きました。

# ラズパイで赤外線を送受信できるようにする
> Raspberry Pi Zero で赤外線リモコンを作る
> https://qiita.com/ww24/items/32d2a78c20e6dcc2e68c
> by [@ww24](https://qiita.com/ww24)

**大部分で、こちらの記事を参考にさせていただきました。ありがとうございます。**
ラズパイで赤外線を送受信できるようにする手順は、基本的には上記の記事の通りなのですが、所々簡略化したので内容が多少ダブりますが解説します。

## 回路図
### 赤外線受信
受信部は[参考記事の回路](https://qiita.com/ww24/items/32d2a78c20e6dcc2e68c#%E5%8F%97%E4%BF%A1%E9%83%A8%E5%88%86)と同じです。
<img width="500" src="https://qiita-image-store.s3.amazonaws.com/0/259617/31e5eb25-5c75-b521-1d8e-9194ef8b8e53.png">



### 赤外線送信
<img width="500" src="https://qiita-image-store.s3.amazonaws.com/0/259617/56df0ac8-3ff5-de59-14f0-39cf63553276.png">

## ラズパイのセットアップ
回路が組めたら、リモコンの赤外線を学習・送信できるようにラズパイをセットアップしましょう。

### lirc のインストールと設定
Linux上から赤外線の制御を簡単にできるようにする、[**LIRC**](https://wiki.archlinux.jp/index.php/LIRC)というデーモンを入れます。

```bash:install
sudo apt update
sudo apt upgrade
sudo apt install lirc
```
無事インストールできたら、
**lirc-rpiを有効化**、**回路につながっているGPIOピンを設定**します。
以下のコマンドで設定ファイルを開いて、

```
sudo vi /boot/config.txt
```
50行目あたりを以下のように書き換えます。

```:50行目あたり
# Uncomment this to enable the lirc-rpi module
# dtoverlay=lirc-rpi
```
↓

```:50行目あたり(変更後)
# Uncomment this to enable the lirc-rpi module
dtoverlay=lirc-rpi,gpio_in_pin=24,gpio_out_pin=25
```
設定が完了したら、`sudo reboot now`でラズパイを再起動します。
[参考記事](https://qiita.com/ww24/items/32d2a78c20e6dcc2e68c)に書かれていますが、以下のことに注意が必要です。
> ⚠上記の設定によって lircd は自動起動するので、 systemctl enable 等は不要です。逆に他の方法で自動起動を設定してしまうことで GPIO の割り当てられるピンが変わってしまうなど不具合が起こる事もあり、注意が必要です。

### リモコンの学習
以下のコマンドを実行した状態で、赤外線受光モジュールにリモコンを向けてボタンを押してみてください。

```bash:
mode2 -d /dev/lirc0 | cat
```
出力(謎の数字列)が得られれば、赤外線を無事に受信できています！

```
pulse 3774
space 1858
pulse 515
space 426
pulse 569
space 1330
pulse 538
space 405
・・・
```

受信できていることを確認したら、`lircd.conf`というファイルに、受信した信号を整形したものを記述して、ラズパイに記憶させる必要があるのですが・・・
今回はそこらへんの処理を全て`shellスクリプト`にまとめました。

#### 赤外線信号を記憶する
**【赤外線受信情報を保存するスクリプト】**

```bash:read_pulse.sh 
#!/bin/bash

if [ $# -ne 1 ]; then
  echo "ERROR: missing file operand"
  echo "Usage: $0 output"
  exit 1
fi

# 読み取り
echo "[Send a signal within 5 seconds!!]"
timeout 5 mode2 -d /dev/lirc0 | sed -ue '1d' | tee $1

# 整形
echo "================================="
cat $1 | awk '{if(NR % 30) ORS=" "; else ORS="\n"; print $1}' | { cat -; echo; } | tee $1
```
コピペして保存したら、`chmod +x read_pulse.sh `で実行権限を付与してください。

**使い方**
以下のように実行して、**5秒以内に**赤外線受光モジュールに向けてリモコンのボタンを押せば、読み取った数値が`on`というファイル名で保存されます。

```bash:
./parse_pulse.sh on
```
同じ要領で、`off`ボタンの信号も読み取っておきましょう。

**【赤外線情報を LIRC に登録するスクリプト】**

```bash:mklircd.sh 
#!/bin/bash

output="lircd.conf"

# ==========================

head="begin remote

  name            $1
  flags           RAW_CODES
  eps             30
  aeps            100
  gap             200000
  toggle_bit_mask 0x0

  begin raw_codes
"

# ==========================

end="  end raw_codes

end remote
"

# ==========================

if [ $# -lt 2 ]; then
  echo "ERROR: Argument is missing"
  echo "Usage: $0 name Signal_file [Signal file ...]"
  exit 1
fi

echo "$head" | tee $output

shift

for x in "$@"
do
  echo "  name $x"
  cat  "$x"
  echo
done | tee -a $output

echo "$end" | tee -a $output
```
上と同じく、コピペして保存したら`chmod +x mklircd.sh `で実行権限を付与してください。

**使い方**
以下のように、`./mklircd.sh [リモコンの名前] [登録したい信号ファイル]　[登録したい信号ファイル]　...`という形でコマンドを実行すると`lircd.conf`というLIRCの設定ファイルを自動で生成してくれます。

```bash:
./mklircd.sh AIR on off
```

#### 設定を反映
設定ファイルがうまく生成されたら、以下のコマンドで設定を適用します。

```bash:
sudo cp lircd.conf /etc/lirc/lircd.conf
```
設定ファイルを適用したら、`/etc/lirc/hardware.conf`を以下のように変更。

```
# Run "lircd --driver=help" for a list of supported drivers.
DRIVER="UNCONFIGURED"
# usually /dev/lirc0 is the correct setting for systems using udev
DEVICE=""
MODULES=""
```
↓

```
# Run "lircd --driver=help" for a list of supported drivers.
DRIVER="default"
# usually /dev/lirc0 is the correct setting for systems using udev
DEVICE="/dev/lirc0"
MODULES="lirc_rpi"
```
以上が完了したら、LIRC を再起動して設定を反映します。

```
sudo systemctl restart lirc
sudo systemctl status lirc
```

### 信号の送信
以下のように、`irsend LIST '' ''`と`irsend LIST AIR ''`でリモコンのボタンが登録されたか確認できます。

```bash:
$ irsend LIST '' ''
irsend: AIR

$ irsend LIST AIR ''
irsend: 0000000000000001 on
irsend: 0000000000000002 off
```

**長かったですが、やっと信号を送出します。**
実行するときは赤外線LEDをエアコンの赤外線受信部に十分近づけて実行してください。

```
irsend SEND_ONCE AIR on
```
これでエアコンが`ON`になれば成功です。
とりあえずお疲れ様です・・・

## ラズパイをAWS IoTに繋いでenebularと連携する
ここの部分についてはこちらの記事にまとめています。
> RaspberryPiをAWS IoTに繋いでenebularと連携する
> https://qiita.com/TakedaHiromasa/items/b6828e4ac434bf99325d

# いよいよenebularで開発！
ここまで下準備が長かったですがやっとフローの作成です・・・！
今回は`SmartAircon`というプロジェクトに `Device_raspberrypi` `Controller_heroku` という2つのフローを作成しました。
<img width="700" src="https://qiita-image-store.s3.amazonaws.com/0/259617/26fb2474-1fec-88ff-f2b7-44a2d1ff2de4.png">

## 「Device_raspberrypi」 フロー
まずは`Device_raspberrypi`のフローから。
<img width="800" alt="スクリーンショット 2019-01-26 22.21.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/259617/764f9fe0-a9cd-9301-3382-b1bb22a61f27.png">

動きとしては以下のような感じ。

1. **MQTT**で`"ON"` or `"OFF"`のテキストを受信して、それぞれのケースに分岐。
1. **exec**で`irsend SEND_ONCE AIR on`とかを実行。
1. 実行エラーなら`0`、成功なら`"ON"` or `"OFF"`のテキストを**MQTT**で返す。

フローのソースは以下の通り。

```json:Device_raspberrypi
[
    {
        "id": "c2e2e67c.0eb118",
        "type": "switch",
        "z": "e5de4f7.bd044b",
        "name": "",
        "property": "payload",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "ON",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "OFF",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 210,
        "y": 140,
        "wires": [
            [
                "41c9a9ae.849118"
            ],
            [
                "853d6803.816448"
            ]
        ]
    },
    {
        "id": "41c9a9ae.849118",
        "type": "exec",
        "z": "e5de4f7.bd044b",
        "command": "irsend SEND_ONCE AIR on",
        "addpay": false,
        "append": "",
        "useSpawn": "",
        "timer": "5",
        "oldrc": false,
        "name": "AIR_on ",
        "x": 360,
        "y": 100,
        "wires": [
            [],
            [],
            [
                "2cd16419.b2c07c"
            ]
        ]
    },
    {
        "id": "853d6803.816448",
        "type": "exec",
        "z": "e5de4f7.bd044b",
        "command": "irsend SEND_ONCE AIR off",
        "addpay": false,
        "append": "",
        "useSpawn": "",
        "timer": "5",
        "oldrc": false,
        "name": "AIR_off",
        "x": 360,
        "y": 180,
        "wires": [
            [],
            [],
            [
                "b70a29c9.1df7e8"
            ]
        ]
    },
    {
        "id": "1c821c08.b3f094",
        "type": "mqtt in",
        "z": "e5de4f7.bd044b",
        "name": "",
        "topic": "",
        "qos": "0",
        "broker": "f6b7453d.c2a968",
        "x": 90,
        "y": 140,
        "wires": [
            [
                "c2e2e67c.0eb118"
            ]
        ]
    },
    {
        "id": "2cd16419.b2c07c",
        "type": "switch",
        "z": "e5de4f7.bd044b",
        "name": "error",
        "property": "payload.code",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "0",
                "vt": "num"
            },
            {
                "t": "neq",
                "v": "0",
                "vt": "num"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 490,
        "y": 100,
        "wires": [
            [
                "1c1b151b.96394b"
            ],
            [
                "6dec0c3d.e2aa34"
            ]
        ]
    },
    {
        "id": "b70a29c9.1df7e8",
        "type": "switch",
        "z": "e5de4f7.bd044b",
        "name": "error",
        "property": "payload.code",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "0",
                "vt": "num"
            },
            {
                "t": "neq",
                "v": "0",
                "vt": "num"
            }
        ],
        "checkall": "true",
        "outputs": 2,
        "x": 490,
        "y": 180,
        "wires": [
            [
                "3c7f5948.87ea06"
            ],
            [
                "66026380.b53dcc"
            ]
        ]
    },
    {
        "id": "1c1b151b.96394b",
        "type": "template",
        "z": "e5de4f7.bd044b",
        "name": "",
        "field": "payload",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "mustache",
        "template": "ON",
        "x": 630,
        "y": 60,
        "wires": [
            [
                "66471905.764468"
            ]
        ]
    },
    {
        "id": "3c7f5948.87ea06",
        "type": "template",
        "z": "e5de4f7.bd044b",
        "name": "",
        "field": "payload",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "mustache",
        "template": "OFF",
        "x": 630,
        "y": 160,
        "wires": [
            [
                "143a4a17.b41a96"
            ]
        ]
    },
    {
        "id": "e74a2173.54aec",
        "type": "mqtt out",
        "z": "e5de4f7.bd044b",
        "name": "",
        "topic": "",
        "qos": "2",
        "retain": "true",
        "broker": "f6b7453d.c2a968",
        "x": 890,
        "y": 120,
        "wires": []
    },
    {
        "id": "1a0f4483.07de2b",
        "type": "link in",
        "z": "e5de4f7.bd044b",
        "name": "",
        "links": [
            "143a4a17.b41a96",
            "66026380.b53dcc",
            "66471905.764468",
            "6dec0c3d.e2aa34"
        ],
        "x": 795,
        "y": 120,
        "wires": [
            [
                "e74a2173.54aec"
            ]
        ]
    },
    {
        "id": "66471905.764468",
        "type": "link out",
        "z": "e5de4f7.bd044b",
        "name": "",
        "links": [
            "1a0f4483.07de2b"
        ],
        "x": 715,
        "y": 60,
        "wires": []
    },
    {
        "id": "6dec0c3d.e2aa34",
        "type": "link out",
        "z": "e5de4f7.bd044b",
        "name": "",
        "links": [
            "1a0f4483.07de2b"
        ],
        "x": 715,
        "y": 100,
        "wires": []
    },
    {
        "id": "143a4a17.b41a96",
        "type": "link out",
        "z": "e5de4f7.bd044b",
        "name": "",
        "links": [
            "1a0f4483.07de2b"
        ],
        "x": 715,
        "y": 160,
        "wires": []
    },
    {
        "id": "66026380.b53dcc",
        "type": "link out",
        "z": "e5de4f7.bd044b",
        "name": "",
        "links": [
            "1a0f4483.07de2b"
        ],
        "x": 715,
        "y": 200,
        "wires": []
    },
    {
        "id": "f6b7453d.c2a968",
        "type": "mqtt-broker",
        "z": "",
        "broker": "iot.eclipse.org",
        "port": "1883",
        "clientid": "",
        "usetls": false,
        "compatmode": true,
        "keepalive": "60",
        "cleansession": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "willTopic": "",
        "willQos": "0",
        "willPayload": ""
    }
]
```
**MQTT入力・出力のトピックは自分で他の人と被らなそうなトピック名をつけてください。(`名前+日時`とか)**

#### デプロイ
デプロイボタンの右にある矢印を押して出てくるメニューから`Export to Other Services`を選択して`AWS IoT`経由でラズパイにフローをデプロイしましょう！
<img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/259617/93f4e862-1925-cc54-1c32-152b84353b75.png">
<img width="600" src="https://qiita-image-store.s3.amazonaws.com/0/259617/6077a691-a507-3d65-efa7-4c45639c6172.png">

デプロイが完了したら、以下のようなフローを別に作って**エアコンがON/OFFするか**、**"ON "OFF"テキストが返ってくるか**テストしてみるといいでしょう。
(先ほど設定したMQTTトピックとトピック名を合わせるようにすること)
<img width="500" alt="スクリーンショット 2019-01-26 22.45.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/259617/3d7a3792-d0a0-08ea-3517-81e974ea6c57.png">

## 「Controller_heroku」 フロー
今回は、WebアプリのUI作成に[node-red-dashboard](https://flows.nodered.org/node/node-red-dashboard)を使いたいので、enebularのメニューから`パレットの管理`を開いて、`ノードを追加`で`node-red-dashboard`を検索・追加します。
<img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/259617/f6aba8ab-c7bd-d08d-d270-6f64d40c3406.png">
<img width="600" src="https://qiita-image-store.s3.amazonaws.com/0/259617/cb3181b6-5958-e50b-a767-23aaf42c89ce.png">

ノードが追加できたら以下のようにフローを組みます。
<img width="400" src="https://qiita-image-store.s3.amazonaws.com/0/259617/083ab47b-8584-48f3-61de-07aeabc9c3ed.png">

やってることはフローの形からなんとなくわかると思います。

フローのソースは以下の通り。

```json:Controller_heroku
[
    {
        "id": "b5570c93.3f03f",
        "type": "ui_button",
        "z": "b2efaedb.75299",
        "name": "ON",
        "group": "720e73f5.43930c",
        "order": 2,
        "width": 0,
        "height": 0,
        "passthru": false,
        "label": "ON",
        "color": "",
        "bgcolor": "red",
        "icon": "",
        "payload": "ON",
        "payloadType": "str",
        "topic": "",
        "x": 90,
        "y": 140,
        "wires": [
            [
                "3ca1dc71.6ffbd4"
            ]
        ]
    },
    {
        "id": "6ab44bc.2e6a5b4",
        "type": "ui_text",
        "z": "b2efaedb.75299",
        "group": "720e73f5.43930c",
        "order": 1,
        "width": 0,
        "height": 0,
        "name": "",
        "label": "現在の状態：",
        "format": "{{msg.payload}}",
        "layout": "row-center",
        "x": 360,
        "y": 80,
        "wires": []
    },
    {
        "id": "8cc4529a.c0c0d",
        "type": "ui_button",
        "z": "b2efaedb.75299",
        "name": "OFF",
        "group": "720e73f5.43930c",
        "order": 0,
        "width": 0,
        "height": 0,
        "passthru": false,
        "label": "OFF",
        "color": "",
        "bgcolor": "",
        "icon": "",
        "payload": "OFF",
        "payloadType": "str",
        "topic": "",
        "x": 90,
        "y": 220,
        "wires": [
            [
                "3ca1dc71.6ffbd4"
            ]
        ]
    },
    {
        "id": "3ca1dc71.6ffbd4",
        "type": "mqtt out",
        "z": "b2efaedb.75299",
        "name": "",
        "topic": "",
        "qos": "0",
        "retain": "true",
        "broker": "76ac36e8.8ca7c8",
        "x": 330,
        "y": 180,
        "wires": []
    },
    {
        "id": "939da3fa.9c869",
        "type": "mqtt in",
        "z": "b2efaedb.75299",
        "name": "",
        "topic": "",
        "qos": "2",
        "broker": "76ac36e8.8ca7c8",
        "x": 90,
        "y": 80,
        "wires": [
            [
                "6ab44bc.2e6a5b4"
            ]
        ]
    },
    {
        "id": "720e73f5.43930c",
        "type": "ui_group",
        "z": "",
        "name": "Default",
        "tab": "4a68af23.3436b",
        "disp": true,
        "width": "6",
        "collapse": false
    },
    {
        "id": "76ac36e8.8ca7c8",
        "type": "mqtt-broker",
        "z": "",
        "name": "",
        "broker": "iot.eclipse.org",
        "port": "1883",
        "clientid": "",
        "usetls": false,
        "compatmode": true,
        "keepalive": "60",
        "cleansession": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "willTopic": "",
        "willQos": "0",
        "willPayload": ""
    },
    {
        "id": "4a68af23.3436b",
        "type": "ui_tab",
        "z": "",
        "name": "Home",
        "icon": "dashboard"
    }
]
```

#### Herokuにデプロイ
下記の公式ドキュメント通りの手順を踏むと、作成したフローをHerokuにDeployできます！
>Heroku へのデプロイ
>https://docs.enebular.com/ja/Deploy/DeployFlow/Heroku/

`https://[Herokuアプリ名].herokuapp.com/ui`にアクセスするとエアコン操作画面が出てきます。
<img width="600" src="https://qiita-image-store.s3.amazonaws.com/0/259617/cad58919-d956-35e0-d8da-a1f717f3cfcf.png">

# ついに完成・・・！
最初に載せたのと同じ動画。
動くとやっぱり感動します。
![AIRCON.gif](https://qiita-image-store.s3.amazonaws.com/0/259617/368652d9-b758-ad13-4cce-b89565400801.gif)

# enebularの凄さ
今回は赤外線を扱うために回路作りやモジュールの設定など事前準備があったため、前半やることがみっちりでしたが、enebularでの開発に入ってからはいたってスムーズでした。
エッジデバイス（回路含め）さえ作成・設置すれば、あとは実物が手元になくてもブラウザ１つで開発・反映・確認ができるので、自分の好きな時に好きな場所から修正・検証できます。
また、今回のように`RaspberryPi` , `Heroku` と、複数の環境をまたいで開発する場合でも、各環境用のコードがバラバラにならず、`プロジェクト`としてenebular上で一括管理できるのも嬉しいポイントです。これによって共有や共同開発も簡単にできます。
実際にenebular中心の開発をしてみることで、**「あらゆるデバイスとクラウドサービスを簡単に『つなぐ』、IoTオーケストレーション・サービス」**と銘打っている理由がなんとなくわかった気がします。

# おわりに
私は集合住宅に住んでいて、ポート開放やグローバルIPの取得ができないので、今まで家にラズパイを置いたまま外部から遠隔開発するのは絶望的だったのですが、AWSIoTとラズパイを連携したらグローバルIPがなくても遠隔デプロイできたのが地味に嬉しかったです。
ちょっと長い記事になってしまいましたが最後まで読んでいただきありがとうございました！
みなさん、良きenebularライフをー！