aptpod Tech Blog

株式会社アプトポッドのテクノロジーブログです

intdash SDK for Swiftを公開しました

f:id:aptpod_tech-writer:20210423144440j:plain

製品開発グループで主にネイティブアプリケーション開発を担当している上野です。

この度、弊社製品のintdashというデータストリーミングプラットフォームをモバイルアプリケーションで利用することができるSDK、「intdash SDK for Swift」をリリースしました。

このSDKはモバイルアプリのintdash MotionVM2M StreamVideoで利用されているものです。intdash MotionやVM2M Stream Videoは、PCやターミナルデバイスを用意せずとも、お手持ちのスマートフォンやタブレットにて手軽にデータ収集や伝送、可視化を可能とするアプトポッドのプロダクトです。

公開したリポジトリにはいくつかのサンプルアプリケーションを同梱しております。 今回はサンプルアプリを用いながらどんな事が出来るのか解説したいと思います。

サンプルアプリケーションのセットアップ

同梱しているサンプルアプリはプロジェクトをXcodeで開いてビルドしたり実行したりすることができますがそのままではサーバーを経由したデータストリーミングは行えません。 まずはデータストリーミングを仲介するサーバー(intdashサーバー)の用意とREADME事前準備と書かれている見出しの内容の対応が必要です。

※intdashサーバーの利用をご希望の方は、弊社営業までお問い合わせください。

aptpod,Inc. Contact

intdashではサーバーとの認証にOAuth2.0と呼ばれるRFC6749で定義されている認証(認可)フレームワークを利用しています。OAuth2.0の細かい説明は省きますが、このサンプルアプリを利用するためには、サーバーへのクライアントID1とコールバックスキーマ2の登録が必要となります。

サーバー管理者へ依頼してそれらの情報を登録及び取得できた場合はいくつかのプロジェクトファイルを更新します。

同梱しているサンプルアプリでは Samples/iOS/Classes/Common.swift に対象のサーバーとクライアントIDを登録するグローバル変数が用意されていますので必要に応じて変更してください。

// Common.swift
let kTargetServer: String = "https://example.com"

/// OAuth2.0認証用のクライアントID。intdashサーバー管理者に問い合わせてください
let kIntdashClientId: String = ""

アプリケーションスキーマの登録は各プロジェクトのInfo.plistURL Typesで行えます。

f:id:aptpod_tech-writer:20210415153210p:plain
URL Type設定

※または画像の様にプロジェクトセッティングのInfoからでも可能です。

intdashサーバーへのデータストリーミング

実際にintdashサーバーとストリーミングを行うアプリを紹介します。

iPhone/iPadから取得したデータををintdashサーバーへ伝送するアップストリーム処理のサンプルアプリは下記の2つです。

  • SensorGPSUpstreamApp
  • VideoUpstreamApp

また、伝送されたデータをリアルタイムに取得し可視化するダウンストリーム処理のサンプルアプリは下記の2つです。

  • SensorGPSDownstreamApp
  • VideoDownstreamApp

センサーとGPSサンプルアプリの実行動画

ビデオサンプルアプリの実行動画

サンプルアプリではセンサー及びGPSとVideoで分けて作られており、それぞれのデータ取得方法と可視化方法を理解する事ができます。もちろん必要に応じて組み合わせたり様々な用途に利用できます。

// MainViewController+GPSManager.swift
func sendLocation(location: CLLocation, rtcTime: TimeInterval) {
    ...
    DispatchQueue.global().async {
        do {
            if !Config.GPS_IS_PRIMITIVE_DATA {
                // 送信する`IntdashData`を生成します。
                let sensor = GeneralSensorGeoLocationCoordinate(lat: Float(location.coordinate.latitude), lng: Float(location.coordinate.longitude))
                // `GeneralSensor***`は`IntdashData`に変換が可能。
                let data = sensor.toData()
                // データ送信前の保存処理。
                if let fileManager = self.gpsDataFileManager {
                    _ = try fileManager.write(units: [data], elapsedTime: elapsedTime)
                }
                // 生成した`IntdashData`を送信します。
                    try self.intdashClient?.upstreamManager.sendUnit(data, elapsedTime: elapsedTime, streamId: streamId)
                }     
                ...
        } catch {
            print("Failed to send location coordinate. \(error)")
        }
    }
}

上記はSensorGPSUpstreamAppで実装されている位置情報データを送信する処理の一部です。

intdashサーバーへデータを送信する際には、SDK内部で定義しているIntdashDataというクラスに送信したいデータを格納して使用します。その際、iPhoneでサポートされているセンサー値を格納するにはGeneralSensor***というクラスを使用します。

このクラスを使うと位置情報の緯度・経度、加速度等のXYZ軸の3つの数値といったように、複数のデータをまとめて一つのクラスで送信する事ができます。

GeneralSensor***toData()IntdashData.DataGeneralSensorに変換して利用します。

// MainViewController+GPSManager.swift
func sendLocation(location: CLLocation, rtcTime: TimeInterval) {
    ...
    DispatchQueue.global().async {
        do {
            if !Config.GPS_IS_PRIMITIVE_DATA {
                ...
            } else {
                // 送信する`IntdashData`を生成します。
                let lat = try IntdashData.DataFloat(id: Config.GPS_PRIMITIVE_DATA_LATITUDE_ID, data: location.coordinate.latitude)
                let lng = try IntdashData.DataFloat(id: Config.GPS_PRIMITIVE_DATA_LONGITUDE_ID, data: location.coordinate.longitude)
                // データ送信前の保存処理。
                if let fileManager = self.gpsDataFileManager {
                    _ = try fileManager.write(units: [lat, lng], elapsedTime: elapsedTime)
                }
                // 生成した`IntdashData`を送信します。
                try self.intdashClient?.upstreamManager.sendUnit(lat, elapsedTime: elapsedTime, streamId: streamId)
                try self.intdashClient?.upstreamManager.sendUnit(lng, elapsedTime: elapsedTime, streamId: streamId)
            }     
        } catch {
            print("Failed to send location coordinate. \(error)")
        }
    }
}

もちろんGeneralSensorに定義されていない一般的なデータも送信が可能です。

小数値であればIntdashData.DataFloat、文字列であればIntdashData.DataString、整数値であればIntdashData.DataInt、バイナリであればIntdashData.DataBytesというクラスを使用してください。これらのクラスは、データ本体とともにデータの内容を表すIDを格納することができます。

// MainViewController+EncodeFunc.swift
func sendJPEG(jpeg: Data, timestamp: TimeInterval) {
    ...
    DispatchQueue.global().async {
        do {
            // 送信する`IntdashData`を生成します。
            let data = IntdashData.DataJPEG(data: [UInt8](jpeg))                
            // データ送信前の保存処理。
            if let fileManager = self.intdashDataFileManager {
                _ = try fileManager.write(units: [data], elapsedTime: elapsedTime)
            }
            // 生成した`IntdashData`を送信します。
            try self.intdashClient?.upstreamManager.sendUnit(data, elapsedTime: elapsedTime, streamId: streamId)
        } catch {
            print("Failed to send jpeg. \(error)")
       }
    }
}

IntdashDataには他にもIntdashData.DataJPEGIntdashData.DataAACといった画像や音声などの特定のフォーマットのデータを格納するクラスもありますので必要に応じて使い分ける事ができます。

// MainViewController+GPSManager.swift
func sendLocation(location: CLLocation, rtcTime: TimeInterval) {
    guard let streamId = self.gpsUpstreamId else { return }
        
    self.clockLock.lock()
    // 計測開始時間が未送信であれば送信します。
    if self.baseTime == -1 {
        self.sendFirstData(timestamp: rtcTime)
    }
    if self.locationBaseTime == -1 {
        self.locationBaseTime = rtcTime
        self.locationSampleBaseTime = location.timestamp.timeIntervalSince1970
    }
    self.clockLock.unlock()
        
    // 計測開始時間から経過時間を算出します。
    let elapsedTime = ((location.timestamp.timeIntervalSince1970 - self.locationSampleBaseTime) + self.locationBaseTime) - self.baseTime
    guard elapsedTime >= 0 else {
        print("Elapsed time error. \(elapsedTime)")
        return
    }
    ...
    // 生成した`IntdashData`を送信します。
    try self.intdashClient?.upstreamManager.sendUnit(data, elapsedTime: elapsedTime, streamId: streamId)
}

生成したIntdashDataは時系列データとして取り扱う為、基準時刻(ベースタイム)からの時間差分(経過時間)とともに送信します。

※基準時刻や経過時間などのintdashの時間にまつわるコンセプトについてはこちらのドキュメント(詳説iSCP 1.0)を参照してください。

// MainViewController+IntdashManager.swift
extension MainViewController: ..., IntdashClientDownstreamManagerDelegate {
    //MARK:- IntdashClientDownstreamManagerDelegate
    func downstreamManagerDidParseDataPoints(_ manager: IntdashClient.DownstreamManager, streamId: Int, dataPoints: [RealtimeDataPoint]) {
        dataPoints.forEach { (dataPoint) in
           // 取得したデータポイントに含まれるデータ種別応じて処理を行います。
            switch dataPoint.dataModel.dataType {
            case .generalSensor:
                guard let dataGeneralSensor = dataPoint.dataModel as? IntdashData.DataGeneralSensor else { return }
                self.sensorDataLock.lock()
                if let sensor = sensor as? GeneralSensorGeoLocationCoordinate {
                    self.setUserLocation(latitude: Double(sensor.lat), longitude: Double(sensor.lng))
                }
                ...
            case .nmea:
                guard let dataNMEA = dataPoint.dataModel as? IntdashData.DataNMEA else { return }
                ...
        }
}

上記はSensorGPSDownstreamAppに含まれる、指定したエッジのデータをリアルタイムにダウンストリームし、そのデータを参照している処理です。

リアルタイムデータの参照はIntdashClientDownstreamManagerDelegateに含まれるコールバックから可能です。 コールバックから返却されたRealtimeDataPointが先ほどデータのアップストリームにも出てきたIntdashDataクラスを持っており、IntdashData.IntdashDataTypeからこのデータの種別を識別できるので種別にあったIntdashDataに変換して利用してください。

伝送したデータをクラウドから取得

データ伝送時にクラウドへの保存設定を有効にした場合は、伝送終了後もデータを参照することができます。

計測済みデータを参照するサンプルアプリの実行動画

AccessingMeasurementDataSampleではクラウドに保存されているデータの取得方法を知ることができます。

もしリアルタイムデータの伝送や可視化だけでなくあとから振り返ってデータを再生したい場合は、このサンプルアプリに含まれる処理を参考に追加でキャッシュ等を実装することで実現できます。

また、工夫をすれば、ファイルにエクスポートして共有するといった機能も実現可能です。

// RequestDataPointsSampleViewController.swift
func updateDataPoints(elapsedTime: TimeInterval) {
    ...
    // 要求するデータのフィルターを作成する
    let filter = makeRequestDataPointsFilters()
    self.loadingDialog = LoadingAlertDialogView.init(addView: self.app.window!, showMessageLabel: false)
    self.loadingDialog?.startAnimating()
    DispatchQueue.global().async { [weak self] in
        ...
        IntdashAPIManager.shared.requestDataPoints(name: targetMeasurement.uuid, filters: filter, start: start, end: end, limit: Config.INTDASH_REQUEST_DATA_POINTS_LIMIT) { (response, error) in
            var timestamp: TimeInterval = 0
            var date: Date?
            if let response = response {
                print("Data points size: \(response.dataPoints.count)")
                self?.dataPointList = response.dataPoints.sorted {
                    let t0 = $0.time?.timeIntervalSince1970
                    let t1 = $1.time?.timeIntervalSince1970
                    if t0 == nil, t1 == nil { return false }
                    else if t0 == nil || t1 == nil { return true }
                    return t0! < t1!
                }
                ...
            }
        }
    }
}

上記がクラウドに保存されたデータをリクエストする例です。
intdashでは時系列データは計測という単位で保存されます。この例では計測に付与されたID(計測ID)をもとにデータ取得しています。

※intdashの計測のコンセプトについてはこちらのドキュメント(詳説iSCP 1.0)を参照してください。

サンプルアプリでは利用可能な各種REST APIを IntdashAPIManager にまとめていますので、必要に応じてご利用ください。

intdash Motionのプラグインアプリについて

弊社製品のintdash Motion(※以下、Motion)は同じiPhone内にインストールしたプラグインアプリと連携してMotionでサポートしているセンサーやビデオデータとは別のデータを同時に伝送する機能を持っています。

f:id:aptpod_tech-writer:20210415103128p:plain
Motion Plugins

この機能はBluetooth等で接続したIoTデバイスのデータをUDPを用いてプラグインアプリがMotionへ伝送することによって実現しています。
また、Motionとの接続にはUDP3を使用しているため複数のプラグインアプリが同時にMotionへ接続することができます 。

MotionPluginAppSampleはMotionのプラグインアプリのサンプルでMotionとの接続とBluetoothデバイスとの接続方法が実装されています。

プラグインアプリとBluetoothデバイスサンプルアプリの実行動画

動画にてプラグインアプリと接続しているアプリはBluetoothDeviceSampleです。 プラグインアプリと接続するBluetoothデバイスを仮想的に試す事ができるサンプルです。MotionPluginAppSampleをお手持ちのiPhoneにインストールし、別のiPhoneにBluetoothDeviceSampleをインストールして接続してみてください。

なお、このサンプルはMacでも動作するのでiPhoneを2台用意できない場合は、iPhone1台とMacでも試してみることができます。

// MainViewController+BLEManager.swift
func manager(_ manager: BLECentralManager, peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    // ToDo:ここでタイムスタンプ保持?もしくはデータから取得する可能性あり
    // 基本的にはMotion側でDate()で取れる端末時間を元にNTPサーバーと同期を行っているのでDate()を扱うことを推奨
    // 端末時間と送信元デバイスとの伝送遅延を考慮したタイムスタンプであると良い。
    let time = Date().timeIntervalSince1970
    // 送られてきたデータの取得
    guard let data = characteristic.value else { return }
    // JSONのパース
    guard let dic = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
        print("Failed to decode data.")
        return
    }
    DispatchQueue.global().async {
        if let vYaw = dic["yaw"] as? NSNumber, let vPitch = dic["pitch"] as? NSNumber, let vRoll = dic["roll"] as? NSNumber,
          let vX = dic["x"] as? NSNumber, let vY = dic["y"] as? NSNumber, let vZ = dic["z"] as? NSNumber {
            let yaw = vYaw.floatValue
            let pitch = vPitch.floatValue
            let roll = vRoll.floatValue
            let x = vX.floatValue
            let y = vY.floatValue
            let z = vZ.floatValue
            let angle = GeneralSensorOrientationAngle(oaa: yaw, oab: pitch, oag: roll).toData()
            let gyro = GeneralSensorRotationRate(rra: x, rrb: y, rrg: z).toData()
            let string = try! IntdashData.DataString(id: "Test-Message-ID", data: "TestMessage")
            // 送信対象データの生成方法
            guard let data = try? IntdashPacketHelper.generatePackets(units: [angle, gyro, string]) else {
                print("Failed to convert unit.")
                return
            }
            let strs = NSMutableString()
            strs.append("{")
            strs.append("\n  \"t\": \"\(time)\",")
            strs.append("\n  \"d\": \"\(data.base64EncodedString())\"")
            strs.append("\n}")
            let message = String(strs)
            guard let messageData = message.data(using: .utf8) else { return }
            self.sendMessage(data: messageData)
        }
    }
}

上記はMotionPluginAppSampleに送られてきた回転角情報をMotionで認識可能なフォーマットに変換してMotionへ送信している処理です。

サンプルではBluetoothデバイスはJSON形式でデータを送信しているのでintdashで認識可能なデータに変換します。
もし使用したいIoTデバイスにデータの取得用SDKやAPIが用意されている場合は、サンプルに含まれる BLECentralManager からのデータ取得処理を、SDKやAPIからの取得処理に変更することで応用が可能です。

まとめ

サンプルアプリを用いたintdash SDK for Swiftの解説は以上となります。

サンプルアプリで提供しているintdashサーバーとのやりとりは基本的に片側通信ですがアップストリーム、ダウンストリームを同時に繋げることで双方向通信を実現することも可能です。 双方向通信を用いることでビデオ通話や配信アプリのような実装も可能になります。
また、プラグインアプリを実装すればお手持ちのスマートデバイスをintdashに接続することができ、データの保存やリモート制御などに手軽に活用することができるようになるため、活用の幅が広がります。

プラグインアプリで行っているiOSにおける同じiPhone、端末内のアプリ間通信はファイルベースが一般的でストリーミングで行っている情報は少ないと思うので人によっては良い参考になるかもしれません。


www.aptpod.co.jp

アプトポッドでは、今回ご紹介した intdash SDK for Swift の他にも、Python でintdashと繋がるアプリケーションを構築できる intdash SDK for Python、intdash と繋がるエッジデバイスを開発するためのエージェントソフトウェアを含む intdsah SDK for Edge Device など、さまざまなSDKをご提供しております。

拡張性の高いIoTプラットフォームをお探しのお客様や、協業先、共同ソリューションをお探しのパートナー企業様、弊社の案件開発にご協力いただける開発会社様など、こちらの記事で intdash というプラットフォームやそのSDKに興味をお持ちいただけましたら、ぜひこちらのお問い合わせ先までご連絡ください。

aptpod,Inc. Contact


  1. クライアントID: リソースサーバーがこのサンプルアプリを識別するためのID

  2. コールバックスキーマ: 認証後にアプリへコールバックするために使用するアプリケーションスキーマ

  3. UDPはTCPとは違いハンドシェイクを必要としないコネクションレスプロトコル。