[Azure x IoT] IoTHubで受け取ったメッセージをリアルタイムにクライアントに通知しよう

※本記事は以下の記事のうち、AzureIoTHubの作成とBroadcast関数の実装部分が異なるだけです。そのため、以下の記事を既にご覧になった方は,IoTHubの作成及びIoTHub連携したAzureFunctionを作成する部分以外は読み飛ばしていただいて大丈夫です!

****[[Azure] AzureでSingalRを使ってリアルタイム通知を行う](https://qiita.com/iron-samurai/items/2fa7fb950c7aac94f64a)****

# 本記事のゴール

本記事では、IoTHubでのメッセージ受信をトリガーにFunctionApp/SignalR経由でメッセージを送り、それをReactアプリケーションで受信できるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/2d8875ef-5cf4-c31a-fea4-bea6b731266f.png)


# AzureSignalRとは？

Azureの提供するリアルタイムプッシュ通信を提供するサービスです。

このサービスを利用すればサーバーサイド側のイベントをトリガーにクライアントアプリケーションに通知を行うことができます。

[SignalR Service - リアルタイム Web | Microsoft Azure](https://azure.microsoft.com/ja-jp/services/signalr-service/?cdn=disable#overview)

# 準備するリソース

先の概略図ではAzureFunctionsを１つしか描きませんでしたが、実際にAzureSignalRとクラインアントの接続を確立するには以下の図の①から③手順を踏むため、Functionsは２つ必要になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/33eeac66-0df4-89e4-9ff2-159e58f045a8.png)


したがって、AzureFunctionsはnegotiation用とbroadcast用で２つ用意します。

上記を踏まえて必要なリソースは以下のようになります。

- AzureIoTHub
- AzureSignalR
- AzureFunctions(HttpTrigger)
- AzureFunctions(SingalRのnegotiation用)
- Azure WebApp(ReactAppのホスティング用)
- AzureContainerRegistry(ReactAppのイメージ格納用)

# 1. Azure IoTHubを作成する

AzurePortalでAzure IoTHubサービスを作成します。

プランはF1(無料）で大丈夫です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/744ef8d3-0e31-6e94-5821-3193f288ab36.png)


# 2. Azure SignalRを作成する

AzurePortalでAzureSignalRサービスを作成します。この際、サービスのモードはDefaultではなくServerlessとなるようにします。

またプランはFreeで大丈夫です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/c0e1f5f8-757e-d1f6-9b92-2184b757d9b6.png)


# 3.Azure Functionsを用意する

次にnegotiation関数とbroadcast関数を作成します。

## negotiation関数の作成

### FunctionAppの作成

関数の入れ物になるFunctionAppを作成します。どのプランでも大丈夫です。

### 関数の中身の作成

テンプレートが用意されているので以下のコマンドで一発で作成できます

```bash
func new --name SingalrNegotiator --template "SignalR negotiate HTTP trigger"
```

negotiation関数は中身をいじる必要がないのでコードはテンプレートのままで大丈夫です。

### 関数のデプロイ

関数を作成したFunctionAppにデプロイします。

```bash
npm i 
npm run build

func azure functionapp publish ${作成したNegotiation用FunctionAppの名前}
```

### FunctionApp上の環境変数の設定

FunctionAppの構成ページで,templateで生成されたfunctions.jsonに記載されている`AzureSignalRConnectionString`を**アプリケーション設定として追加します。**

値は、SignalRの"キー"ページの"接続文字列"を入力します。

- SignalR
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/f394d375-54cf-a713-94d5-9081d5578ebe.png)

    
- FunctionAppのアプリケーション設定
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/8e05b582-d3dc-9163-776a-383d0b64d296.png)

    

これによってFunctionAppとSignalRを連携させます。****

## broadcast関数の作成

### FunctionAppの作成

関数の入れ物になるFunctionAppを作成します。どのプランでも大丈夫です。

### 関数の中身の作成

よくあるエラーやより詳細な手順は筆者の以下の記事を参考にしてみてください！

[[Azure x IoT]Azure IoTHub とAzure Functionsを連携しよう - Qiita](https://qiita.com/iron-samurai/items/d5015f4bb18609f71aaa)

今回はテストしやすいようにHttpTriggerの関数を作成します。こちらもテンプレートで作成できます。

```bash
func new --name IoTHubTriggeredFunction --template "IoT Hub (Event Hub)"
```

次にfunction.jsonにiothub/signalrとの連携用の記述を変更または追加します.

```bash
{
      "type": "signalR",
      "name": "signalRMessages",
      "hubName": "default",
      "connectionStringSetting": "AzureSignalRConnectionString",
      "direction": "out"
},
{
      "type": "eventHubTrigger",
      "name": "IoTHubMessages",
      "direction": "in",
      "eventHubName": "MyEventHub",  ←テンプレートから変更
      "connection": "myEventHubReadConnectionAppSetting",  ←テンプレートから変更
      "cardinality": "one", ←テンプレートから変更
      "consumerGroup": "$Default"
}
```

最後にindex.tsを以下のように書き換えます
`context.bindings.signalRMessages`の部分がsignalRを使って送るメッセージを構築する部分です。targetで指定した文字列をクライアント側でも指定してサブスクライブします。

```tsx
import { AzureFunction, Context } from "@azure/functions"

const IoTHubTrigger: AzureFunction = async function (context: Context, IoTHubMessage: any): Promise<void> {

// ここがsignalRを使って送るメッセージを構築する部分
context.bindings.signalRMessages = [{
        target:'test',
        arguments:[{message:IoTHubMessage.value,timestamp:new Date()}]
    }]

};

export default IoTHubTrigger;
```

### 関数のデプロイ

関数を作成したFunctionAppにデプロイします。

```bash
npm i 
npm run build

func azure functionapp publish ${作成したBroadcast用FunctionAppの名前}
```

### FunctionApp上の環境変数の設定

Negotiation関数の時と同様に、FunctionAppの構成ページで,templateで生成されたfunctions.jsonに記載されている以下の２つをアプリケーション設定として追加します

- AzureSignalRConnectionString(Negotiation関数の設定を参照)
- myEventHubReadConnectionAppSetting

myEventHubReadConnectionAppSettingに入れる値は、Event Hub-compatible endpointで、その値はIoTHubから確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/0dcadf93-3c14-e139-5947-ca5b3da8c8c5.png)

これによってFunctionAppとIoTHub/SignalRを連携させます。

# 4. クライアントアプリケーションを作成する

ホスティング方法はなんでも良いのですが本記事では次のチュートリアルに倣ってContainerRegistryにpushしたimageをWebAppがpullして利用する形を採用します。

ContainerRegistryとWebAppの作成方法は以下を参考にしてください。

[Azure App Service を使ってコンテナー化された Web アプリをデプロイして実行する - Learn](https://docs.microsoft.com/ja-jp/learn/modules/deploy-run-container-app-service/)

## クライアントアプリの中身の作成

雛形の作成

```bash
npx create-react-app webapp --template typescript
```

次にsignalrを使う上で必要な次のパッケージをインストールしてください。

```bash
npm i @microsoft/signalr
```

最後にApp.tsxを以下のように編集してください。メッセージを受け取ったらその内容と時刻を表示するだけのアプリケーションです。

.envにREACT_APP_NEGOTIATOR_NAMEを追加するのを忘れないようにしてください。

```bash
import React, {useEffect, useState} from 'react';
import './App.css';
import * as signalR from '@microsoft/signalr'
const connection = new signalR.HubConnectionBuilder()
  .withUrl(`https://${process.env.REACT_APP_NEGOTIATOR_NAME}.azurewebsites.net/api`)
  .configureLogging(signalR.LogLevel.Trace)
  .withAutomaticReconnect()
  .build()

function App() {
  const [data,setData] = useState({message:'',timestamp:''})

  useEffect(()=>{
    connection.on('test', data => {
      console.log("Received data from signalR")
      console.log(data)
      setData(data)
    })

    connection.onclose(function() {
      console.log('signalr disconnected')
    })
    connection.onreconnecting(err =>
      console.log('err reconnecting  ', err)
    )
    connection
      .start()
      .then((res:any) => {})
      .catch(console.error)
  },[])

  return (
    <>
      <div>Message from SignalR</div>
      <div>{data.message}</div>
      <div>arrived at {data.timestamp}</div>
    </>
  );
}

export default App;
```

## デプロイ

以下のコマンドでデプロイします

```bash
acr build --registry ${コンテナレジストリの名前} --image ${イメージ名} ${DockerFileのあるディレクトリへのパス}"
```

参考までにDockerfileも記載しておきます.以下を配置いただいて

```bash
FROM node:14
RUN npm install -g serve # A simple webserver
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["serve", "-s", "build", "-l", "3000"]
```

# 5.SignalRとnegotiation関数でCORSの設定を行う

デフォルトではそれぞれ、同じオリジンからのアクセスしか受け付けてないので、ホスティングしたWebAppからもアクセスできるようにします。WebAppのURLはリソースのトップページで確認できます。

SignalRもnegotiation関数(FunctionApp)もサイドメニューにCORSメニューがあるのでそこから設定します。

ローカルで立ち上げたアプリでも接続したい場合は、http://localhost:3000なども登録すれば大丈夫です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/6d377df6-96d7-ed59-3b19-f16b9e8beaf6.png)


# 6. 動作確認

WebAppにアクセスしてアプリケーションを開いた状態で、FunctionAppをテスト実行します

1. WebAppにアクセスしてアプリケーションを開きます
こんな感じの画面になるはずです。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/8295a729-d4e3-9beb-0c61-b8e7440c9a5e.png)

    
2. IotHubに対してメッセージを送信します
    
    必要な環境変数を設定して以下を実行します。
    
    ```bash
    curl -i -X POST \
    -H "Content-Type:application/json" \
    -H "Authorization:$SAS" \
    -d '{"value":"hello,world with https"}' \
    "https://$IOTHUB_NAME.azure-devices.net/devices/$DEVICE_NAME/messages/events?api-version=2018-06-30"
    ```
    
    SASは次のコマンドで取得できます
    
    ```bash
    
    az iot hub generate-sas-token -n $IOTHUB_NAME --du $EXPIRATION_SECONDS
    ```
    
    詳しくは筆者の[こちらの投稿](https://qiita.com/iron-samurai/items/3015971f15a20ccf954c)をご覧ください
    
3. アプリケーション上にIoTHubに送ったメッセージと実行時間が表示されたら成功です！
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/6ac55cff-8c77-4b69-58e2-193baf4ee452.png)

    

# おわりに

いかがでしたでしょうか?これでIoTデバイスからのメッセージをトリガーにリアルタイムにクライアントアプリケーションに通知を行えるようになりました!  
あとは自宅のいろんなところにセンサーを置いてマイホームをスマートホーム化してもいいかもしれないですね!