JavaScriptを使用してiPhoneのセンサ情報を取得してみる

現在、遠隔地の車両データをモニタリングしたり解析できるクラウド計測解析システム「[Smart Logging Service](https://www.toyota-td.jp/product_site/smart_logging_service/index.html)」、「[Opti Meister Online](https://www.toyota-td.jp/product_site/optimeister_online/index.html)」を開発しております。
SmartLoggingServiceでは専用デバイスで車両のCAN情報やGPS、加速度、カメラ映像など計測していますが、iPhoneがあれば専用のデバイスがなくてもGPS、カメラ、加速度など簡易的な計測できるはずです。iPhoneのセンサデータをSmartLoggingServiceにつなげるとどんな風に見えるのかまずは簡単なWebアプリを作成してみることにしました。

![hw.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763657/46547ff9-6ce5-2942-6810-d7cb6f664d9d.png)

# 作りたいもの
- iPhoneのWebブラウザだけで動作する（chrome）
- iPhoneの加速度、位置情報、カメラ画像をクラウドに送信して遠隔地でモニタリングする
- モニタ用の画面は既存システムであるSmartLoggingServiceのモニタ画面につなげる

# できたもの


### iPhoneでの画面表示
iPhone画面上にカメラ画像、加速度、GPS座標を表示しています。これらのデータをクラウドに送信しています。

![iphone.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763657/befa7f9e-ece0-2e2a-4c9f-456c8b481850.gif)



### SmartLoggingServiceのモニタ画面
iPhoneのカメラ画像、加速度、GPS座標をSmartLoggingServiceのモニタ画面からモニタリングすることができました。ほぼリアルタイムで体感0.2~0.3秒ほどの時間差で表示できていそうです。

![smalog.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763657/2bbc3b1d-2714-d5d1-e74a-dc0f07f7840f.gif)


# 構成

iPhoneのセンサーデータはJavaScriptで取得します。
アプリはAWS ChaliceとJinja2を使ったサーバレスアプリとしました。Lambdaのソースコード内にhtmlやjavascriptを配置して配信しています。
データの共有はJavaScriptのpahoを使用してMQTTデータをAWS IotCoreに送信してします。
JavaScriptからAWSIoTCoreにアクセスするための認証情報はCognitoを使用しています。

![design.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763657/7be59514-22a0-25eb-757f-aba7e93fa7b3.png)


# センサーデータの取得
JavaScript＋HTMLで加速度、GPS、カメラ画像のプレビューを表示します
## GPS
Geolocation APIのNavigator:geolocationを使用します
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/geolocation

```html:GPS情報（html）
<div class="pl-2">
    <p>Device Position</p>
    <span id="longitude">0</span>
    <span id="latitude">0</span>
</div>
```
```javascript:GPS情報（javascript）
function onSuccessGetPosition(position){
    document.getElementById('latitude').innerHTML = position.coords.latitude;
    document.getElementById('longitude').innerHTML = position.coords.longitude;
}
navigator.geolocation.getCurrentPosition(onSuccessGetPosition)
```

## 加速度
Device orientation eventsのDeviceMotionEventを使用します
https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent
```html:加速度（html）
<div class="pl-2">
    <p>Device Motion</p>
    <span id="x">0</span>
    <span id="y">0</span>
    <span id="z">0</span>
</div>
```
```javascript:加速度（javascript）
function deviceMotionRequest () {
    if (DeviceMotionEvent.requestPermission) {
        DeviceMotionEvent.requestPermission()
        .then(permissionState => {
            if (permissionState === 'granted') {
                window.addEventListener("devicemotion", function (event) {
                    if (!event.accelerationIncludingGravity) {
                        alert('event.accelerationIncludingGravity is null');
                        return;
                    }
                    document.getElementById('x').innerHTML = event.accelerationIncludingGravity.x;
                    document.getElementById('y').innerHTML = event.accelerationIncludingGravity.y;
                    document.getElementById('z').innerHTML = event.accelerationIncludingGravity.z;
                })
            }
        })
        .catch(console.error);
    } else {
        alert('DeviceMotionEvent.requestPermission is not found')
    }
}
```

## カメラ
Media Capture and Streams APIのMediaDevices: getUserMedia()を使用します
https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
```html:camera画像（html）
<div>      
    <p>Device Camera</p>  
    <button onClick="publishCamera()" class="btn btn-primary mr-1">Publish Camera</button>
    <video id="video" autoplay playsinline></video>
    <canvas id="canvas" width="320" height="240"></canvas>
</div>
```

```javascript:camera画像（javascript）
async function startCamera() {
    try {
        const constraints = {
            video: {
                facingMode: "environment", // 背面カメラを使用
                width: { max: 320 },
                height: { max: 240 }
            }
        };
        
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        const video = document.getElementById('video');
        video.srcObject = stream;
    } catch (err) {
        console.error('Error accessing the camera: ', err);
    }
}
document.addEventListener('DOMContentLoaded', startCamera);
```

# MQTT送信
pahoを使用してMQTTのpub/subを行います。
MQTTはIoT通信に多く活用されている軽量なデータ通信プロトコルです
認証情報にはあらかじめCongitoから取得した認証キーを使用します(認証情報取得するソースコードは省略)
```javascript:mqtt.js
/**
 * mqttクラス
 */
class mqttClient {
    /**
     * コンストラクタ
     */
    constructor() {
        this.client = null;
    }

    /**
     * 接続
     * @param  {string} regionName リージョン　ex)ap-northeast-1
     * @param  {string} awsIotEndpoint IoTCoreエンドポイント　ex)xxxxxxxxx.iot.ap-northeast-1.amazonaws.com
     * @param  {string} accessKey アクセスキー
     * @param  {string} secretKey シークレットキー
     * @param  {object} onMessage メッセージ取得時の処理
     */
    connect(regionName, awsIotEndpoint, accessKey, secretKey, sessionToken, onMessage, onSuccess, onFailure) {        
        const endpoint = this.#createEndpoint(regionName, awsIotEndpoint, accessKey, secretKey, sessionToken);
        const clientId = Math.random().toString(36).substring(7);
        this.client = new Paho.MQTT.Client(endpoint, clientId);
        const connectOptions = {
            useSSL: true,
            timeout: 3,
            mqttVersion: 4,
            onSuccess : onSuccess,
            onFailure: onFailure
        };
        this.client.connect(connectOptions);
        this.client.onMessageArrived = onMessage;
        this.client.onConnectionLost = (e) => { console.log(e) };
    }

    publish(topicName, message){
        let mqtt_message = new Paho.MQTT.Message(message);
        mqtt_message.destinationName = topicName;
        this.client.send(mqtt_message);
    }

    /**
     * 受信トピック指定
     * @param  {string} topicName トピック名
     */
    subscribe(topicName, onSuccess) {
        const options = {
            onSuccess: onSuccess,
            onFailure: () => {
                console.log("Failed subscribe.");
            }
        }
        this.client.subscribe(topicName, options);
    }

    unsubscribe(topicName, onSuccess){
        const options = {
            onSuccess: onSuccess,
            onFailure: () => {
                console.log("Failed unsubscribe.");
            }
        }
        this.client.unsubscribe(topicName, options);
    }

    #createEndpoint(regionName, awsIotEndpoint, accessKey, secretKey, sessionToken) {
        var time = moment.utc();
        var dateStamp = time.format('YYYYMMDD');
        var amzdate = dateStamp + 'T' + time.format('HHmmss') + 'Z';
        var service = 'iotdevicegateway';
        var region = regionName;
        var secretKey = secretKey;
        var accessKey = accessKey;
        var algorithm = 'AWS4-HMAC-SHA256';
        var method = 'GET';
        var canonicalUri = '/mqtt';
        var host = awsIotEndpoint;
        var sessionToken = sessionToken;
   
        var credentialScope = dateStamp + '/' + region + '/' + service + '/' + 'aws4_request';
        var canonicalQuerystring = 'X-Amz-Algorithm=AWS4-HMAC-SHA256';
        canonicalQuerystring += '&X-Amz-Credential=' + encodeURIComponent(accessKey + '/' + credentialScope);
        canonicalQuerystring += '&X-Amz-Date=' + amzdate;
        canonicalQuerystring += '&X-Amz-SignedHeaders=host';
        var canonicalHeaders = 'host:' + host + '\n';
        const payloadHash = this.#sha256("");
        const canonicalRequest = method + '\n' + canonicalUri + '\n' + canonicalQuerystring + '\n' + canonicalHeaders + '\nhost\n' + payloadHash;   
        const string2Sign = algorithm + '\n' +  amzdate + '\n' +  credentialScope + '\n' +  this.#sha256(canonicalRequest);
        //getSignatureKey
        const hmacDate = CryptoJS.HmacSHA256(dateStamp, 'AWS4' + secretKey);
        const hmacRegion = CryptoJS.HmacSHA256(region, hmacDate);
        const hmacService = CryptoJS.HmacSHA256(service, hmacRegion);
        const signingKey = CryptoJS.HmacSHA256('aws4_request', hmacService);
        //sign
        const signature = CryptoJS.HmacSHA256(string2Sign, signingKey).toString(CryptoJS.enc.Hex);
   
        canonicalQuerystring += '&X-Amz-Signature=' + signature;
        return 'wss://' + host + canonicalUri + '?' + canonicalQuerystring + '&X-Amz-Security-Token=' + encodeURIComponent(sessionToken);
    }

    #sha256(msg) {
        const hash = CryptoJS.SHA256(msg);
        return hash.toString(CryptoJS.enc.Hex);
    };

}

```


# まとめ
JavaScriptで簡単にiPhoneのセンサ情報を取得することができました。また、思っていたよりもリアルタイム性高くモニタ表示できたことに驚きました。今後は複数のiPhoneを使用した多地点分散計測やBluetoothやLightningケーブルでiPhoneに接続した外部のセンサ情報なども取得できないかトライしてみたいと思います。

# 参考
- [iPhone搭載センサの種類と使われ方集 + デモアプリ](https://qiita.com/tsuboyan/items/ff6988ea3fed5bfadcb3)
- [MQTT とは何ですか?](https://aws.amazon.com/jp/what-is/mqtt/)
- [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API)