製品開発グループで主にネイティブアプリケーション開発を担当している上野です。
この度、弊社製品のintdashというデータストリーミングプラットフォームをモバイルアプリケーションで利用することができるSDK、「intdash SDK for Swift」をリリースしました。
このSDKはモバイルアプリのintdash MotionやVM2M StreamVideoで利用されているものです。intdash MotionやVM2M Stream Videoは、PCやターミナルデバイスを用意せずとも、お手持ちのスマートフォンやタブレットにて手軽にデータ収集や伝送、可視化を可能とするアプトポッドのプロダクトです。
公開したリポジトリにはいくつかのサンプルアプリケーションを同梱しております。 今回はサンプルアプリを用いながらどんな事が出来るのか解説したいと思います。
サンプルアプリケーションのセットアップ
同梱しているサンプルアプリはプロジェクトをXcodeで開いてビルドしたり実行したりすることができますがそのままではサーバーを経由したデータストリーミングは行えません。
まずはデータストリーミングを仲介するサーバー(intdashサーバー)の用意とREADMEの事前準備
と書かれている見出しの内容の対応が必要です。
※intdashサーバーの利用をご希望の方は、弊社営業までお問い合わせください。
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.plist
のURL Types
で行えます。
※または画像の様にプロジェクトセッティングの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.DataJPEG
やIntdashData.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でサポートしているセンサーやビデオデータとは別のデータを同時に伝送する機能を持っています。
この機能は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、端末内のアプリ間通信はファイルベースが一般的でストリーミングで行っている情報は少ないと思うので人によっては良い参考になるかもしれません。
アプトポッドでは、今回ご紹介した intdash SDK for Swift の他にも、Python でintdashと繋がるアプリケーションを構築できる intdash SDK for Python、intdash と繋がるエッジデバイスを開発するためのエージェントソフトウェアを含む intdsah SDK for Edge Device など、さまざまなSDKをご提供しております。
拡張性の高いIoTプラットフォームをお探しのお客様や、協業先、共同ソリューションをお探しのパートナー企業様、弊社の案件開発にご協力いただける開発会社様など、こちらの記事で intdash というプラットフォームやそのSDKに興味をお持ちいただけましたら、ぜひこちらのお問い合わせ先までご連絡ください。