#SORACOM LTE-M Button の簡易位置情報を SORACOM #Funk を通して Azure Functions (C#) で取得する

LTE-M button の簡易位置情報を Azure Functions (C#) で取得する」シリーズの第3弾、完結編です。(そんな大袈裟なものなのか)

今回は Azure Functions にデプロイした C# なアプリケーションで、ボタンの位置情報SORACOM Funk 経由で取得します。

位置情報取得シリーズはこんな構成。


SORACOM Funk は、Azure Functions (GCP Functions でも同様とのこと) に対して、位置情報を x-soracom-token ヘッダーに JWT 形式で入れてリクエストしてきます。

デコードするという一手間(本当に一手間ですが)が必要です。

プロジェクトに JWT パッケージを追加

JWT を操作するために System.IdentityModel.Tokens.Jwt パッケージを使います。
NuGet Gallery を見ると最新版の状況などが分かりますよね。
今回は .NET Core の Function App なので、Jwt パッケージが使えます。

Visual Studio Code では、

  1. Ctrl + @ でターミナルを開きます
  2. “dotnet add package System.IdentityModel.Tokens.Jwt –version 5.5.0” を入力

これで、プロジェクトにパッケージがインストールされます。
これで JWT を操作できるようになります。

関数のクラス(ButtonLoc.cs)の先頭付近に、

using System.IdentityModel.Tokens.Jwt;

を追加します。

ローカルPC で位置情報を取得する手順 のページで作ったプロジェクトだと、ファイル名は “ButtonLoc.cs” であるはず。
それ以外の名前で関数を作った場合は、適宜読み替えてください。

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.IdentityModel.Tokens.Jwt;

namespace ButtonSample.ButtonApp
{

(以下、省略)

JWT のデコード

SORACOM Funk からのリクエストからデータを取り出してデコードするには以下のようにします。

string token = req.Headers["x-soracom-token"];
var tokenHandler = new JwtSecurityTokenHandler();
var jwt = tokenHandler.ReadToken(token) as JwtSecurityToken;

位置情報は x-soracom-token ヘッダーで渡されるということで、まずそれを取り出します。
この値を JwtSecirutyTokenHandler クラスの ReadToken に渡すとデコードされます。

なお、x-soramon-token の値(上のコードの “token” 変数)は https://jwt.io/ の Debugger を使うと、デコードされた結果を見ることができます。


Payload をパースして位置情報を取り出す

位置情報は Payload の中の “ctx” の中に入っています。

“ctx” の中には位置情報 (location) の他に、位置情報の取得に成功したかどうか “locationQueryResult” も入っています。

“locationQueryResult” が “success” の場合は位置情報が取れていますので取り出します。

以下のようにします。

dynamic ctx = jwt.Payload["ctx"];
var locResult = ctx["locationQueryResult"].ToString();
if (locResult == "success")
{
    dynamic location = ctx["location"];
    var lat = location.lat;
    var lon = location.lon;
}

これで無事、位置情報が取得できました!


メソッドの完成

ここまでを受けて、”ButtonLoc.cs” は以下のようにします。

せっかくなので、Beam 経由でも Funk 経由でも位置情報を取れるようにしてみます。

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.IdentityModel.Tokens.Jwt;

namespace ButtonSample.ButtonApp
{
    public static class ButtonLoc
    {
        [FunctionName("ButtonLoc")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            // log.LogInformation("C# HTTP trigger function processed a request.");

            bool result;
            string errorMessage;
            float latitude = 0;
            float longitude = 0;

            string userAgent = req.Headers["user-agent"];
            if (userAgent == "SORACOM Beam")
            {
                (result, latitude, longitude, errorMessage) = GetLocationFromBeam(req);
                if (!result)
                {
                    log.LogWarning(errorMessage);
                    return (ActionResult)new OkObjectResult(errorMessage);
                }
            }
            else if (userAgent == "SORAOCM Funk")
            {
                (result, latitude, longitude, errorMessage) = GetLocationFromFunk(req);
                if (!result)
                {
                    log.LogWarning(errorMessage);
                    return (ActionResult)new OkObjectResult(errorMessage);
                }
            }
            else
            {
                log.LogWarning($"Not Supported: {userAgent}");
                return (ActionResult)new OkObjectResult($"Not Supported: {userAgent}");
            }

            var rawBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic body = JsonConvert.DeserializeObject(rawBody);
            string clickTypeName = body?.clickTypeName ?? "(Unknown)";

            var message = $"{clickTypeName} on {DateTime.UtcNow:yyyy/MM/dd HH:mm:ss} at Latitude: {latitude}, Longitude: {longitude} By {userAgent}";
            log.LogInformation(message);
            return (ActionResult)new OkObjectResult(message);
        }

        private static (bool result, float latitude, float longitude, string errrorMessage) GetLocationFromBeam(HttpRequest req)
        {
            string locResult = req.Headers["X-Soracom-Geo-Position-Query-Result"];
            if (locResult == "success")
            {
                var loc = req.Headers["X-Soracom-Geo-Position"].ToString().Split(';');
                var lat = float.Parse(loc[0]);
                var lon = float.Parse(loc[1]);

                return (true, lat, lon, null);
            }
            else
            {
                return (false, 0, 0, "Failed to Get Location");
            }
        }

        private static (bool result, float latitude, float longitude, string errorMessage) GetLocationFromFunk(HttpRequest req)
        {
            string token = string.Empty;
            try
            {
                token = req.Headers["x-soracom-token"];
            }
            catch (Exception ex)
            {
                return (false, 0, 0, $"Failed to Get Token: {ex.Message}");
            }

            var tokenHandler = new JwtSecurityTokenHandler();
            var jwt = tokenHandler.ReadToken(token) as JwtSecurityToken;

            try
            {
                dynamic ctx = jwt.Payload["ctx"];
                var locResult = ctx["locationQueryResult"].ToString();
                if (locResult == "success")
                {
                    dynamic location = ctx["location"];
                    var lat = location.lat;
                    var lon = location.lon;

                    return (true, lat, lon, jwt.ToString());
                }
                else
                {
                    return (false, 0, 0, $"Failed to Get Location Temporarily: {jwt.ToString()}");
                }
            }
            catch (Exception ex)
            {
                return (false, 0, 0, $"Cannot Get Location: {ex.Message}");
            }
        }
    }
}

ここまで出来たら、改めて Azure に デプロイ します。


関数の URL の確認と SORACOM Funk の設定

SORACOM Funk の設定をします。

まずは改めて Azure Functions にデプロイした 関数の URL を確認します。
(前の投稿で一度やっている内容です)

やり方ですが、 “Function App” ブレードで “<関数名>” を選択してから [関数の URL の取得] をクリックします。
URL が表示されるのでクリップボードにコピーしておきます。

関数の URL が分かったので、SORACOM Funk の設定です。

項目
有効化のスイッチON
サービスAzure Functions
送信データ形式JSON
認証情報(後述)
エンドポイント関数の URL の “https://~/api/ButtonLoc&#8221;
(”?code=” より前の部分)

認証情報は以下の通り。

項目
認証情報 ID任意
概要任意
種別API トークン認証情報
API トークン関数の URL の “?code=” より後ろの部分
(”?code=” を含まず)

これで登録すれば OK。


動作確認

Azure ポータルの関数ブレードで [ログ] を確認することもできますが、ここでは Visual Studio Code の Azure Functions 拡張機能を使ってログの確認をしてみます。

Visual Studio Code の Functions 拡張機能を開いて、[<関数名>] – [Start Streaming Logs] を選択します。
これで Azure Functions のログが Visual Studio Code の [出力] タブに表示されるようになります。

いよいよ LTE-M Button を操作 してみます。

SORACOM ユーザーコンソールで Beam を無効化しておくと、こんな風に表示されます。

Funk, Beam とも有効にしてボタンを操作するとこうなります。

当然ですが、 ボタンの 1回の操作ごとに Azure にデプロイした関数が 2回実行されます。
Funk で 1回、Beam で 1回ですね。

実際には Funk と Beam とで同じ関数を叩くことはないと思いますが、やってみると当然ながら、このようなログが出力されます。


以上で、LTE-M Button の位置情報を SORACOM Funk から Azure Functions に連携できました。

ボタン側では何もすることはありませんし、SORACOM ユーザーコンソールでの設定も簡単です。(私はハマりましたが orz)
あとはアイデア次第!楽しんでください。


(余談)
Funk 経由で位置情報を取る方法が分からなくて Funk 経由の位置情報のドキュメントをちゃんと読んでなくて、呟いたら速攻で 中の人(Max)に正解を教えてもらいました
いや恥ずかしい、「ダメじゃん、俺」的な。でも、これこそが SNS の正しい使い方(言い訳)

ここまで分かれば出来ます、自分にも出来ます!
ということで、やってみました。夏休みの自由研究です。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中