【THETAプラグイン開発】THETAで長時間連続写真撮影を行う

#THETAプラグインで長時間連続写真撮影を行う
##はじめに
長時間連続稼働の定点観測システムにTHETAを使えないか試してみました。
今回使用した機種はTHETA Z1です。
THETAプラグイン及びAndroidでの開発は今回が初めてとなるので、ルールに即していない実装があるかもしれませんが、ご了承ください。
また、本記事内容の開発時期は2020年5月頃であり、最新の環境とは異なる可能性があります。
##実現したいこと
- 24/7で1分毎に静止画を撮影する
- 目標は1か月の連続稼働（無人で稼働する）
- 魚眼で空を撮影した画像を保存する
- 撮った写真は順次クラウドにアップロードする
- 設定ファイルをクラウドからダウンロードして、撮影パラメータをリモートで変更可能とする

##開発機能
###インターバル撮影
インターバル撮影に関しては`MainActivity.onCreate`に`java.util.Timer`を実装して実現しています。
当初は*theta-plugin-sdk*ベースで**WebAPI**を使って実装していたのですが、1日～3日程度で終了して電源が落ちている等安定しませんでした。
そこで*theta-plugin-camera-api-sample*ベースの**CameraAPI**での実装に変えてみたところ、自分で中断した場合や明確なエラー発生（後述）以外には止まってしまうことはないくらいに安定しました。
###画像の加工
THETA Z1では静止画の解像度を選択できないようで（*Stitching*と*NonStitching*の違いのみ）、撮影したJpegの画像サイズは8MB前後有ります。
このままではクラウドにアップロードできないので、`android.graphics.Matrix`を使用して縦横それぞれ半分の解像度（元の1/4）に縮小しています。
また、今回は三脚を使ってTHETAを横向きに使用し魚眼の写真を保存するため*NonStitching*で撮影を行い、さらに空方向の片眼のみの画像となるように半分に切り取っています。

```
Matrix matrix = new Matrix();
matrix.preScale(0.5f, 0.5f);
Bitmap resizedPicture = Bitmap.createBitmap(originalPicture, 0, 0,
    originalPicture.getWidth(), originalPicture.getHeight(), matrix, true);
```
###クラウド連携
今回は「AWS Mobile SDK」を使用してS3への接続を行います。
注意しなければいけないのは「THETA SDK」のターゲットとしている「Android SDK」ビルドバージョンが**25**なのに対して、「AWS Mobile SDK」を使用するには**28**にする必要があるということです。
####「build.gradle」ファイルの編集
- `dependencies`に`implementation 'com.amazonaws:aws-android-sdk-s3:2.7.7'`を追加し、同期を行うことで自動的にパッケージが導入されます。
- ビルドバージョンについてのエラーが出ると思うので、`compileSdkVersion`,`targetSdkVersion`を**28**にします。
「THETA SDK」をビルドするために、`minSdkVersion`は**25**にしておきます。

```
android {
    compileSdkVersion 28
    buildToolsVersion "28.0.3"
    defaultConfig {
        applicationId "com.theta360.pluginsample"
        minSdkVersion 25
        targetSdkVersion 28
        versionCode 2
        versionName "2.1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
```
以上の設定で「AWS Mobile SDK」が使用可能になりますが、一部にバージョン差異による非推奨関数の警告や例外が発生します。
今回、発生した例外は連続撮影プラグイン動作に影響のない画面表示に関するものだったため、あえてそのまま進めています。（例外が発生しているコードを取り除くとさらにキャッチできない「THETA SDK」内部での例外が発生して、プラグインが動作しなくなるため）
####S3へのアクセス
※現状で非推奨となっているものもありますが、以下の方法を使っています。
本来S3へのアクセスにはリージョン指定はいらないはずですが、この場合はデフォルトリージョン（US）となりアクセスできないため、明示的に指定します。

```
String accessKey = ※アクセスキー
String secretKey = ※シークレットアクセスキー
String buchet = ※バケット
String endPoint = "https://s3-ap-northeast-1.amazonaws.com";
```

- アップロード

S3へのアクセスは非同期で行わるため、ファイルアップ後に、そのファイルに対して処理を行う場合には`onStateChanged()`でチェックし、処理が終わったタイミングで行うように実装しています。

```
try {
    File uploadFile = new File(fileUrl);
    String uploadName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
    BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey);
    AmazonS3Client s3Client = new AmazonS3Client(basicAWSCredentials);
    Region region = Region.getRegion(Regions.AP_NORTHEAST_1);
    s3Client.setRegion(region);
    s3Client.setEndpoint(endPoint);
    TransferUtility mTransferUtility = new TransferUtility(s3Client, getApplicationContext());
    AmazonS3Client s3Client = new AmazonS3Client(basicAWSCredentials);
    Region region = Region.getRegion(Regions.AP_NORTHEAST_1);
    s3Client.setRegion(region);
    s3Client.setEndpoint(endPoint);
    mTransferUtility = new TransferUtility(s3Client, getApplicationContext());
    TransferObserver observer = mTransferUtility.upload(buchet + "/" + Util.getDate(), uploadName, uploadFile);
    observer.setTransferListener(new TransferListener() {
        @Override
        public void onStateChanged(int id, TransferState state) {
            Log.d("AwsSample", "status: " + state);
            if ((state == TransferState.COMPLETED) || (state == TransferState.FAILED) || (state == TransferState.CANCELED)) {
                if (state == TransferState.COMPLETED) {
                    uploadFile.delete();
                }
                fileDbUpdate(fileUrls);
            }
        }

        @Override
        public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) {
            Log.d("AwsSample", "progress: " + id + " bytesCurrent:" + bytesCurrent + " bytesTotal:" + bytesTotal);
        }

        @Override
        public void onError(int id, Exception ex) {
            ex.printStackTrace();
            outputLog("onError:", ex);
        }
    });

} catch (Throwable th) {
…
```

- ダウンロード

今回はダウンロードした設定ファイルを読み込んで撮影パラメータを変更するため、非同期にならないようにダウンロード処理の終了待ちをしています。


```
try {
    BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey);
    AmazonS3Client s3Client = new AmazonS3Client(basicAWSCredentials);
    Region region = Region.getRegion(Regions.AP_NORTHEAST_1);
    s3Client.setRegion(region);
    s3Client.setEndpoint(endPoint);
    TransferUtility mTransferUtility = new TransferUtility(s3Client, getApplicationContext());
    AmazonS3Client s3Client = new AmazonS3Client(basicAWSCredentials);
    File temp = new File(Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_DCIM).getPath() + "/" + settingFileL);
    Log.d("download", temp.getPath());
    String[] fileUrls_d = {temp.getPath()};
    TransferObserver observer = transferUtility.download(buchet, settingFileC, temp);
    observer.setTransferListener(new TransferListener() {
        @Override
        public void onStateChanged(int id, TransferState state) {
            Log.d("AwsSample", "status: "+state);
            if(state == TransferState.COMPLETED) {
                Log.d("downloadFile", String.valueOf(temp.length()));
                fileDbUpdate(fileUrls_d);
                param[0] = new CameraParameters(temp);
            }
        }

        @Override
        public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) {
            Log.d("AwsSample", "progress: "+id+" bytesCurrent:"+bytesCurrent+" bytesTotal:"+bytesTotal);
        }

        @Override
        public void onError(int id, Exception ex) {
            ex.printStackTrace();
        }
    });
    while ( false == (observer.getState() == TransferState.COMPLETED || observer.getState() == TransferState.FAILED || observer.getState() == TransferState.CANCELED) ) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
} catch (Throwable th) {
...
```

**※ダウンロード時の注意点**
今回はダウンロードする設定ファイルを”*～.json*”としていたのですが、どうしてもダウンロードファイルの保存ができませんでした。
保存するファイル名を”*～.jsn*”にすると保存ができたので、拡張子は3文字までという制限がかかっているようです。

###ネットワーク障害対応
ネットワーク障害（Wi-Fiルータへの接続断）が起こった場合にTHETAはクライアントモードから
自身がルータとなるモードに変更されてしまいます。
この対策として、接続失敗時にはクライアントモードに変更するように設定しておきます。

```
try {
    //S3への接続処理
} catch (Throwable th) {
    // reset wlan client mode
    notificationWlanCl();
}
```
##検証結果
このような感じで実装しながら、随時長時間連続稼働テストも行ってきました。
かなり安定はしているのですが（原因不明で止まってしまうということはない）、まだ連続1か月稼働には至っていません。（最長12日程度）
現在までに発生した
###確認されたエラー
####・ハードウェア故障（おそらく）
#####エラー状況
5月上旬の夕方ごろTHETAの電源が落ちてい居ることを発見し調べてみたのですが、何度を入れ直しても強制電源OFFを行ってもエラー表示が出て起動しない状態となっていました。
その後、1日置いたらとりあえず電源は入ったのですが、PCに接続してもデバイスとして認識したりしなかったり、そしてまたエラー頻発し始めたりで、ADBコマンドでのリセット等を行ってみましたが、まともに動作しない状態でした。
テスト時の状況としては以下のようになっていました。

- THETAを設置していた場所は4時～5時ごろは西日が直接入り込むような状態となっており、室温は通常の20度から25度くらいまで上昇していた。
- 最後に撮影されてクラウドに挙がった写真に写っていたTHETAに近い位置に置いた温度計は45度を指していた。
- 翌日、同位置同時間帯を検証すると、40度を超え温度計の表示も点滅しておかしくなるような状態が少なくとも30分は計測していたものと思われた。

#####対応
以上の事から、温度上昇におけるハードウェア故障ではないかと推測されたことから、もう修理対応しかないということで、修理センターに送付して修理を依頼しましたが、修理不可の有償交換となってしまいました。
なお、原因究明を行いたいため、いったん交換対応は行わずに修理センターからリコーへ送付して故障原因究明を行ってもらうこととしました。
故障原因究明には数か月単位の時間がかかるそうですが、作業自体は無償で行ってくれるそうです。（2020/8/6時点で連絡はまだありません）

####・デバイスのエラー
#####エラー状況
現状のテストが停止する原因としては、まずこのエラーとなっています。
2代目のTHETAを使用できることとなったため、それ以降は上記１の状態にならないように直射日光にさらさず、サーキュレータ等で温度管理しながらテストを続けていましたが、連続7日になろうとしていたくらいの時にプラグインは動作しているのにシャッター音がしないことに気が付き、PCに接続してログを取得しました。

プラグインが停止していなかったのは、撮影処理の呼び出し時にカメラデバイスが使用中ということで抜けていたためです。
また、この状態になると電源を入れ直してもエラーで終了し、強制電源OFFからの電源ONでしか復帰しなくなっていました。
この件について調べてみたのですが、得られる情報は少なく、**CAM_MctServ**で検索するとソニー製のスマートフォン及びカメラでのエラー報告が上がっている（THETAのカメラもソニー製） 点とログ及び、エラー後の挙動から、カメラデバイス自体またはデバイスドライバが使用できなくなっているということを推測しています。
また、再度連続テストを行った場合には１２日程度継続後に同エラーが発生しており、ある一定期間デバイスを使用し続けると発生するものでもない（規則性はない）ということもわかっています。

以下がそのログの抜粋となっています。
#####エラー時のデバイスログ

```
2020-05-30 02:17:16.625 528-15909/? A/libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0x1000f in tid 15909 (CAM_MctServ)
2020-05-30 02:17:16.711 13154-13154/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
2020-05-30 02:17:16.711 13154-13154/? A/DEBUG: Build fingerprint: 'Android/msm8953_64/msm8953_64:7.1.2/N2G47H/542:user/test-keys'
2020-05-30 02:17:16.711 13154-13154/? A/DEBUG: Revision: '0'
2020-05-30 02:17:16.711 13154-13154/? A/DEBUG: ABI: 'arm'
2020-05-30 02:17:16.711 13154-13154/? A/DEBUG: pid: 528, tid: 15909, name: CAM_MctServ  >>> /system/bin/mm-qcamera-daemon <<<
2020-05-30 02:17:16.711 13154-13154/? A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x1000f
2020-05-30 02:17:16.711 13154-13154/? A/DEBUG:     r0 c74e84e0  r1 00020003  r2 00010002  r3 de884de0
2020-05-30 02:17:16.711 13154-13154/? A/DEBUG:     r4 00010003  r5 e64e0205  r6 de884de4  r7 00000000
2020-05-30 02:17:16.711 13154-13154/? A/DEBUG:     r8 d75b4540  r9 df97c800  sl e4e2aae0  fp de8860b8
2020-05-30 02:17:16.711 13154-13154/? A/DEBUG:     ip e654698c  sp de8847f0  lr e5281b7b  pc e5281b6e  cpsr a0010030
2020-05-30 02:17:16.715 13154-13154/? A/DEBUG: backtrace:
2020-05-30 02:17:16.715 13154-13154/? A/DEBUG:     #00 pc 00016b6e  /system/vendor/lib/libmmcamera2_mct.so (mct_list_find_custom+15)
2020-05-30 02:17:16.715 13154-13154/? A/DEBUG:     #01 pc 0000eabf  /system/vendor/lib/libmmcamera2_sensor_modules.so
2020-05-30 02:17:16.715 13154-13154/? A/DEBUG:     #02 pc 00016c73  /system/vendor/lib/libmmcamera2_mct.so (mct_list_traverse+24)
2020-05-30 02:17:16.715 13154-13154/? A/DEBUG:     #03 pc 000210a5  /system/vendor/lib/libmmcamera2_sensor_modules.so
2020-05-30 02:17:16.715 13154-13154/? A/DEBUG:     #04 pc 000098cf  /system/vendor/lib/libmmcamera2_mct.so
2020-05-30 02:17:16.715 13154-13154/? A/DEBUG:     #05 pc 00011271  /system/vendor/lib/libmmcamera2_mct.so
2020-05-30 02:17:16.715 13154-13154/? A/DEBUG:     #06 pc 0000f6f9  /system/vendor/lib/libmmcamera2_mct.so
2020-05-30 02:17:16.715 13154-13154/? A/DEBUG:     #07 pc 00014371  /system/vendor/lib/libmmcamera2_mct.so
2020-05-30 02:17:16.715 13154-13154/? A/DEBUG:     #08 pc 00012b1f  /system/vendor/lib/libmmcamera2_mct.so
2020-05-30 02:17:16.715 13154-13154/? A/DEBUG:     #09 pc 00046fe3  /system/lib/libc.so (_ZL15__pthread_startPv+22)
2020-05-30 02:17:16.715 13154-13154/? A/DEBUG:     #10 pc 00019ced  /system/lib/libc.so (__start_thread+6)
```
#####対応
現在、一定期間でのプラグインの再起動を行って発生を抑制できないかを検証しながら、できればデバイスの再起動などができないか方法を探っています。

**※何か情報をお持ちの方いらっしゃれば、ぜひご意見をいただきたいです！**