LoginSignup
6
2

More than 1 year has passed since last update.

Azure × FlashAir でリングフィットアドベンチャーの履歴を記録する(1)Blobにアップロード編

Last updated at Posted at 2022-03-23

はじめに

コロナ禍の引きこもり生活ですっかり運動不足になってしまったので、運動習慣を付けようと今更ながらリングフィットアドベンチャーに入門しました。

プレイの記録をApple Watchで取っていたのですが、始める際にいちいちワークアウトを開始するのが面倒でした。
何とかこれを省力化できないかと思いネットを徘徊していると、Switchの機能でリザルト画面のスクショを撮りTwitterに投稿、それを画像解析して記録する例が見つかりました。

しかし、自分の記録をTwitterに投稿するのはなんか気恥ずかしい…と悩んでいたところに妙案が浮かびました。

「FlashAirを使えばいいじゃないか」と。

というわけで、とうの昔に旬が過ぎ去ったネタのような気もしますが、リングフィットアドベンチャーとFlashAirを組み合わせて運動結果の自動記録システムを作っていきます。
お仕事でAzureをよく使うので、バックエンドはAzureで構築します。

全体像

こんな感じのものを作ります。
ringfit.jpg

  1. Switchでスクリーンショットを記録
    サンプル

  2. ファイルの保存をトリガーにしてFlashAirがBlob Storageへ画像をアップロード

  3. Blob追加をトリガーにEventGrid経由でFunctionsを起動

  4. Azure Cognitive Serviceを使ってOCRを行い活動時間・消費カロリーを取得

  5. Google Fitに記録

全体を説明すると長くなるので、本記事ではFlashAirからBlobアップロードの部分のみを解説します。

FlashAirとは

東芝が販売していIoT機能を持ったSDカードです。
(会社がキオクシアに変わっていつの間にかディスコンになってるっぽい?悲しい…。)
旧ブランド製品 | KIOXIA

Wifiでネットワークに接続でき、Luaで書いたスクリプトを実行することで様々な処理を実行することができます。

用意したもの

image.png
ここまでで1万円以上使ってるので後には引けません

Azure側の準備

画像アップロードの受け口となるストレージアカウントを作成します。

リソースグループを作成し、

$ az group create --location japaneast --resource-group RingFitRecorder

ストレージアカウントを作成。

$ az storage account create -g RingFitRecorder -n ringfitimage --sku Standard_LRS

ストレージアカウント作成時の出力結果の「id」の値をscopesに設定しサービスプリンシパルを作成します。

az ad sp create-for-rbac -n RingFitRecorder --scopes /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/RingFitRecorder/providers/Microsoft.Storage/storageAccounts/ringfitimage --role "Storage Blob Data Contributor" --year 100

サービスプリンシパル作成時の出力結果は次の工程で必要になるので忘れずに控えておきましょう。

作成したストレージアカウントに「upload」というコンテナーを作成しておきます。
image.png

これでAzure側の準備は完了です。

FlashAirの準備

SDカード内の /SD_WLAN/CONFIG ファイルを編集します。
以下の行のみ変更・追記すればよいでしょう。

APPMODE=5                            # 自動起動モードで無線LAN機能を子機として起動
APPSSID=<無線LANのSSID>
APPNETWORKKEY=<無線LANのパスワード>
UPLOAD=1                             # 必要ないかもしれないが、念のためアップロード機能をオンに
LUA_SD_EVENT=/RingFitRecorder.lua    # ファイル書き込みが発生した際に実行するLuaスクリプトファイルを指定する

次に、画像をアップロードするLuaスクリプトを「RingFitRecorder.lua」というファイル名でSDカード直下に保存します。
FlashAirはファイル書き込み時にスクリプトを実行してくれますが、どのファイルが更新されたかは通知してくれません。そのため、スクリプト内で最新更新日時を持つファイルを探索しています。Switchのスクリーンショットは「/Nintendo/Album」配下に記録されるので、このディレクトリ内を探索対象としています。
また、後のFunctionsの処理でファイルの記録された時間を利用したいため、アップロード時のファイル名をUNIX時間+拡張子としています。

RingFitRecorder.lua
-- Azure config
tennant_id = "<AzureのテナントID>"
client_id = "<サービスプリンシパルのクライアントID>"
client_secret = "<サービスプリンシパルのシークレット>"
azure_storage_base_path = "https://ringfitimage.blob.core.windows.net/"
blob_container_name = "upload/"

-- ディレクトリ内のファイルを再帰探索する
function FileGet(fpath)
    for dirname in lfs.dir(fpath) do
        local dirpath = fpath .. "/" .. dirname
        local mod_dir = lfs.attributes(dirpath, "mode" )
        if mod_dir == "directory" then 
            FileGet(dirpath)
        else
            print(dirpath)
            table.insert(files, dirpath)
        end
    end
end

-- FAT時間を時刻テーブルへ変換する
function GetFileModificationTime(Fat_binary_time)
    local date ={
        year = bit32.band (bit32.rshift(Fat_binary_time, 9+16),0x7F) + 1980,
        month = bit32.band (bit32.rshift(Fat_binary_time, 5+16),0x0F),
        day = bit32.band (bit32.rshift(Fat_binary_time,0+16),0x1F),
        
        hour = bit32.band (bit32.rshift(Fat_binary_time, 11),0x1F),
        min = bit32.band (bit32.rshift(Fat_binary_time, 5),0x3F),
        sec = bit32.band (Fat_binary_time,0x1F)*2; --FAT時間は秒数が2秒刻み
    }
    return date
end

print("start!")

cjson = require "cjson"

-- 最新更新ファイルを取得
fpath = "/Nintendo/Album" 
files = {}
FileGet(fpath)
max_mod = 0
last_file = ""
for n,file in pairs(files) do
    mod = lfs.attributes(file, "modification")
    if mod >= max_mod then
        last_file = file
        max_mod = mod
    end
end

-- fpath配下にファイルがなければ処理終了
if last_file == "" then
    print("file not exist.")
    return;
end

-- デバッグ用: 処理対象のファイルと、そのファイルの更新日時(FAT時間形式)を表示
print(last_file)
print(max_mod)


-- Bearerトークン取得
auth_url = "https://login.microsoftonline.com/" .. tennant_id .. "/oauth2/token"
auth_payload = "grant_type=client_credentials"
            .. "&resource=https://storage.azure.com/"
            .. "&client_id=" .. client_id
            .. "&client_secret=" .. client_secret
length = string.len(auth_payload)

auth_body = fa.request{
    url=auth_url,
    method="POST",
    headers = {
        ["Content-Length"] = length
    },
    body=auth_payload
}

token = cjson.decode(auth_body)["access_token"]

-- x-ms-dateヘッダに設定する日付をファイルの更新日時から生成
mod_unixtime = os.time(GetFileModificationTime(max_mod)) - 9 * 60 * 60
mod_date = os.date("%a, %d %b %Y %H:%M:%S",mod_unixtime) .. " GMT"

-- Content-Lengthヘッダ用にファイルサイズを取得する
file_size =  lfs.attributes(last_file, "size")

-- Blobアップロード先のURLを生成(UNIX時間をファイル名に使用する)
blob_url = azure_storage_base_path .. blob_container_name .. tostring(mod_unixtime) .. string.sub(last_file, -4)
print(blob_url)

-- "<!--WLANSDFILE-->" がファイルの中身と置き換わる
blob_payload = "<!--WLANSDFILE-->"

-- Blobアップロード
upload_body,upload_code,upload_header = fa.request{
    url=blob_url,
    method="PUT",
    headers={
        ["Authorization"] = "Bearer " .. token ,
        ["x-ms-date"] = mod_date ,
        ["Content-Length"] = file_size ,
        ["x-ms-version"] = "2021-04-10" ,
        ["x-ms-blob-type"] = "BlockBlob"
    },
    file=last_file,
    body=blob_payload
}

print(upload_body)
print(upload_code)
print(upload_header)

アップロードのテスト

ここまで準備できたら、FlashAirをSwitchに刺してスクリーンショットを撮ってみましょう。
無事にファイルがアップロードされていればオッケーです!
image.png

ハマったポイント

ここまで作り上げるのにハマったポイントを紹介しておきます。

FlashAirが通信できるURLの文字数に上限がある?

当初アップロードにSASトークンを利用しようとしていたのですが、謎のエラーが表示され困っていました。
いろいろと試してみた限り、通信する先のURLが長すぎるとエラーになってしまうようです。SASトークンはURLの末尾に付与する形になるので、文字数制限に引っかかってしまったようです。
上述のコードのように、サービスプリンシパルのBearer Tokenを取得しヘッダに付与することでクリアできました。

TLS1.1以上が利用できない

ストレージアカウントの設定で、最小のTLSバージョンを1.0に設定しないとエラーになります。
ざっと探した限りでは明言された仕様は見つかりませんでしたが、TLS1.0までしか対応していないようです。

「/Nintendo/Album」以外のディレクトリに対し書き込みがあってもスクリプトが実行されてしまう

SDカード内のすべての書き込みイベントに対しスクリプトを実行してしまいます。
そのたびに「/Nintendo/Album内の最新ファイルを探索してアップロード」という挙動になってしまうため、重複してアップロードしてしまう可能性があります。
とはいえ、ファイル名が同じなのでアップロード時にエラーになるはずなので、きっと大丈夫だと楽観視してます。
また、AzureのREST APIの仕様上、x-ms-dateヘッダに付与した日付が現在時刻から15分ズレるとエラーになるため、古いデータが誤ってアップロードされる可能性は低いはずです。

スクリーンショットを連続で撮るとアップロードに漏れが生じる

連続で撮らないでください。運用でカバーです。

動画を撮影してもアップロードされてしまう

動画を撮らないでください。運用でカバーです。

おわりに

というわけでSwitchからAzureへアップロードするところまでができました。
次回はFunctionsを起動してCognitive ServiceでOCR処理を行うところまでを解説したいと思います。
(なおFunctionsの処理は未完成のため、次回投稿は未定です)

補足

そういえば去年あたりにAzure ADのTLS1.0・1.1を廃止するってアナウンスあったような…。
上記の通りFlashAirがTLS1.0までしか対応していないようなので、廃止されたら別の手を考えなければいけません。

謝辞

6
2
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
6
2