数年前、観光・アウトドア向けのドローン撮影体験を想定し、SwiftとDJI SDKを使ったiOSアプリケーションを設計・実装しました。
このシステムでは、オペレーターが撮影パターンと撮影時間を選択すると、
- ドローンの離陸
- 自動飛行
- ジンバル制御
- 動画撮影
- 機体からiPhoneへの動画転送
- iPhone上での動画編集
- AWSへのアップロード
- QRコードによるユーザーへの配信
までを一連のワークフローとして実行します。
最終的な目標は、撮影から約10分程度でユーザーが完成動画へアクセスできる体験を実現することでした。
本記事では、具体的なソースコードや固有名称などは省きつつ、システム全体の設計と、実機を使ったフィールドテストから得た知見を紹介します。
システム境界を「ドローンの着陸」で終わらせない
このプロジェクトで最初に重要だったのは、システムの境界をどこに置くかでした。
ドローンアプリというと、飛行制御そのものに注目しがちです。
しかし実際のユーザー体験として考えると、
ドローンが正しく飛んだ
だけでは成功とは言えません。
ユーザーが最終的な動画を受け取るところまで含めて、初めてプロダクトとして成立します。
システム全体は大きく5つのレイヤーに分けて考えました。
Operator UI
↓
Flight Execution
↓
Safety Supervision
↓
Media Processing
↓
Cloud Delivery
Operator UI
オペレーターが撮影パターンと撮影時間を選択します。
Flight Execution
離陸、機体移動、ジンバル制御、録画、帰還などを担当します。
Safety Supervision
自動飛行中でも、人間がいつでも介入できる状態を維持します。
Media Processing
撮影した動画を機体から取得し、トリミングや音声合成などを行います。
Cloud Delivery
完成動画をクラウドへアップロードし、QRコード経由でユーザーへ提供します。
この設計で重要だったのは、各レイヤーが独立して成功しても、システム全体として成功するとは限らない点です。
たとえば、
- 飛行には成功したが動画を取得できない
- 動画編集には成功したがアップロードできない
- アップロードできたがユーザーが簡単にアクセスできない
といった状態は、すべてプロダクトとしては失敗です。
3種類の撮影パターンを抽象化する
アプリでは、主に3種類のシネマティックな撮影パターンを扱いました。
Vertical Rise
被写体をフレーム内に残しながら、機体を垂直方向に上昇させます。
Zoom-out
機体を上昇させながら後方へ移動し、徐々に周囲の景色を見せます。
Orbit
被写体を中心として円を描くように飛行し、カメラを常に内側へ向けます。
重要なのは、オペレーターにDJI SDKの実装詳細を意識させないことでした。
UI上では単純に、
Vertical Rise
Zoom-out
Orbit
から選択するだけです。
一方、内部では接続されている機体やSDKの機能に応じて、実際の制御方法を切り替えます。
Mission APIとVirtual Stick
DJI SDKを利用した飛行制御では、高レベルのMission APIとVirtual Stickによる直接制御の両方を検討しました。
Mission APIでは、WaypointやHotpointのような抽象度の高い操作を利用できます。
メリットは、飛行軌道を比較的簡潔に定義できることです。
一方で、
- 機種ごとの対応差
- SDKバージョンによる違い
- 細かな飛行挙動の調整
を考える必要があります。
Virtual Stickでは、より直接的に
- Pitch
- Roll
- Yaw
- Vertical movement
などを制御できます。
自由度が高い代わりに、アプリ側で
- コマンド送信周期
- タイミング
- 速度
- 終了条件
- Control Mode
- エラー時の停止処理
まで管理する必要があります。
つまり、抽象度と制御自由度のトレードオフです。
そこでアプリ側では、
User Intention
↓
Shot Definition
↓
Aircraft Adapter
↓
DJI SDK
という考え方を採用しました。
ユーザーが指定するのは、
どんな映像を撮りたいか
だけです。
その要求を、
この機体ではどう実行するか
へ変換する責任はデバイス側へ持たせます。
これはドローンに限らず、複数種類のハードウェアを扱うロボティクスシステムで有効な設計だと考えています。
飛行ジオメトリからジンバル角度を計算する
Vertical RiseやZoom-outでは、機体が移動し続けます。
このときジンバル角度を固定すると、被写体がフレームから外れてしまいます。
そこで、飛行中の高度と被写体までの距離を利用して、ジンバルのPitch角を動的に計算しました。
単純化すると、以下の関係を利用できます。
$$
\theta = \arctan\left(\frac{h}{d}\right)
$$
ここで、
- $h$:機体の相対高度
- $d$:被写体までの水平距離
- $\theta$:被写体方向へカメラを向けるための角度
です。
イメージとしては次のようになります。
Drone
*
|\
h | \
| \
| \ Camera
| \
| \
+------* Subject
d
計算した角度をDJI SDK側で必要な角度表現へ変換し、飛行中にジンバルへ反映します。
これはComputer Visionを利用した本格的なSubject Trackingではありません。
しかし、
- 撮影パターンが事前に定義されている
- 被写体のおおよその位置が分かる
- 移動パターンが限定されている
という条件では、単純な幾何学モデルでも十分に実用的でした。
すべてのロボティクス機能に高度な認識モデルが必要なわけではありません。
問題の制約を利用すれば、単純な数式でも有効な制御が実現できます。
ドローンミッションは非同期State Machineとして考える
実装上、飛行ミッションは1回のSDK呼び出しではありません。
複数の非同期イベントを組み合わせたワークフローになります。
概略としては、次のように進みます。
Preparing
↓
Taking Off
↓
Recording
↓
Executing Shot
↓
Stopping
↓
Returning
↓
Downloading
↓
Processing
↓
Uploading
↓
Completed
Swiftで概念だけ表現すると、たとえば以下のようなStateを持たせられます。
enum MissionState {
case idle
case preparing
case takingOff
case recording
case executing
case stopping
case returning
case downloading
case processing
case uploading
case completed
case failed(Error)
}
実際の処理では、
- Aircraft connection
- Flight Controller state
- GPS
- Camera state
- Gimbal state
- Mission progress
- Remote Controller input
- Network state
などが同時に変化します。
例えば録画を開始するだけでも、
Aircraft connected?
Camera available?
Camera mode correct?
SD card available?
Takeoff completed?
Flight state stable?
といった条件を確認する必要があります。
モバイルアプリではCallbackとして書ける処理でも、ロボティクスシステムでは状態遷移として整理した方が安全です。
特に重要なのは、
正常系だけでなく異常系の遷移もState Machineに含める
ことです。
「自律飛行」ではなくSupervised Autonomy
このシステムでは定型的な撮影動作を自動化しています。
ただし、人間の責任を排除する完全自律型システムとしては設計していません。
考え方としてはSupervised Autonomyに近いものです。
物理Remote Controllerは常にオペレーターの介入手段として残します。
自動飛行中もスティック入力を監視し、オペレーターの入力を検知した場合には、自動制御を停止できるようにしました。
目的は、
Automated Control
+
Manual Control
が同時に機体へ命令を送る状態を避けることです。
Cancellation PathをSuccess Pathと同時に設計する
一般的なアプリでは、処理に失敗した場合にエラーダイアログを表示すれば済むこともあります。
しかし飛行中のドローンでは、
Something went wrong
で終わらせることはできません。
異常を検知した場合、
- 自動飛行を停止
- Virtual Stick Controlを解除
- Recordingを停止
- Missionをキャンセル
- オペレーターへ制御を返す
- 必要ならReturn-to-Homeを開始
など、複数の処理を安全な順序で実行する必要があります。
このプロジェクトから得た大きな教訓の一つが、
Cancellation PathはSuccess Pathと同じタイミングで設計する
ことでした。
考慮した主な条件には、
- Manual Override
- GPS validation
- Mission cancellation
- Recording shutdown
- Signal problem
- Mission timeout
- Return-to-Home
などがあります。
自動化が信頼されるためには、自動的に動くこと以上に、
人間が理解できること、止められること、回復できること
が重要です。
撮影終了後に動画を機体から取得する
飛行終了後、撮影した動画はまだドローンのSDカードにあります。
そのため、次にメディア取得処理を行います。
大まかなフローは以下です。
Recording Mode
↓
Media Download Mode
↓
Fetch Media List
↓
Fetch Metadata / Thumbnail
↓
Select Video
↓
Download
↓
Save Locally
↓
Restore Camera Mode
アプリでは、
- Media File Listの取得
- Thumbnail取得
- Metadata取得
- ファイル選択
- 動画Download
- Download Progress
- Cancel
- ローカル保存
などを扱いました。
この部分は見た目としては地味ですが、実運用では非常に重要です。
高解像度動画の場合、メディア転送時間が飛行時間より長くなる場合があります。
そのためUIでは、
- 現在の処理
- ダウンロード進捗
- キャンセル可否
- 次の操作が可能かどうか
を明確に表示する必要があります。
ロボティクス製品では、待ち時間もUXの一部です。
動画編集をiPhone側で行う
取得した動画はAppleのMedia Frameworkを利用し、iOSデバイス上で処理しました。
用途に応じて、
- 動画のTrim
- BGMとの合成
- ロゴOverlay
- ユーザー固有のVisual Overlay
- MP4へのExport
- 一時ファイル管理
などを行います。
ここでのポイントは、オリジナル動画をそのままクラウドへアップロードしないことでした。
Drone
↓
Raw Video
↓
iPhone
↓
Trim / Audio / Overlay / Export
↓
Final MP4
↓
Cloud
完成したファイルだけをクラウドへ送ることで、不要なアップロードを減らせます。
責任分界は次のようにしました。
Edge / iPhone
- 機体から動画取得
- 動画編集
- Audio Mixing
- Overlay
- Final Export
Cloud
- 完成動画の永続化
- 配信
Web / Instant Experience
- ユーザーによる閲覧
- Download
- Share
いわゆるEdge-to-Cloud型のMedia Pipelineです。
AWS S3へUploadし、QRコードで配信する
動画処理後、完成したMP4をiOSアプリからAWS S3へアップロードします。
アップロード中はProgressを監視し、
Processing
↓
Uploading 32%
↓
Uploading 78%
↓
Completed
のようにオペレーターへ状態を表示します。
Upload完了後、ユーザー向けDelivery URLに紐づくQRコードを生成します。
周辺システムとして、
- AWS S3
- AWS Amplify
- React
- iOS App Clip
- Android Instant App
などを組み合わせました。
狙いは、動画を見るためだけにユーザーへアプリのインストールを要求しないことです。
理想的なユーザーフローは、
Scan
↓
Open
↓
Watch
↓
Download
↓
Share
です。
ここで重要だったのは、ユーザーがSDKやクラウド構成を評価するわけではないという点です。
ユーザーにとって重要なのは、
撮影した動画が早く届くか
QRコードを読み取って簡単に開けるか
その場で共有できるか
です。
つまり、Cloud DeliveryもFlight Controlと同じくらい重要なプロダクト機能でした。
フィールドテストで見えた「境界」の問題
システムは実際のキャンプ場環境でもフィールドテストしました。
屋内でコンポーネント単位にテストしていると見えなかった問題が、現場では多数発生します。
例えば、
- 無線通信
- GPS状態
- 周囲の飛行環境
- オペレーターの注意力
- 動画転送時間
- ネットワーク速度
- ユーザーの待ち時間
などです。
特に興味深かったのは、問題の多くが1つのコンポーネント内部ではなく、コンポーネント間の境界で発生したことです。
例えば、
Flight → Media
飛行終了直後、期待したMedia Fileがすぐ一覧に現れない。
Media → UI
大容量動画を転送しているが、Progress表示が分かりづらい。
Automation → Operator
自動飛行中、オペレーターがすぐにManual Controlへ戻したい。
Processing → Customer
技術的には正常だが、完成までの操作ステップが多すぎる。
これらは、
Flightだけテスト
Mediaだけテスト
Cloudだけテスト
では発見しにくい問題です。
必要だったのは、
Shot Selection
↓
Flight
↓
Recording
↓
Download
↓
Processing
↓
Upload
↓
QR Delivery
というEnd-to-EndのOperational Loop全体をテストすることでした。
今作り直すならどう設計するか
現在このシステムを再設計するとしたら、基本コンセプトは維持しつつ、アーキテクチャをいくつか改善します。
1. Mission State Machineを明示的にする
当時以上にStateを明確にモデル化します。
例えば、
enum MissionState {
case idle
case checkingAircraft
case ready
case takingOff
case recording
case executingShot
case returning
case downloadingMedia
case processingMedia
case uploading
case delivering
case completed
case failed(Error)
}
各Stateについて、
- Entry Condition
- Exit Condition
- Timeout
- Failure Condition
- Recovery Action
を定義します。
これによりCallback同士の依存関係を減らし、テストもしやすくなります。
2. Shot DefinitionとAircraft Adapterを分離する
例えば、
Zoom-out
Duration: 15 sec
Target distance: x
Target altitude: y
という撮影定義自体は、DJI SDKに依存させません。
Shot Definition
↓
Aircraft Adapter
↓
DJI Implementation
という構造にします。
Aircraft Adapter側で、
- Mission APIを使うか
- Virtual Stickを使うか
- その機体がどの機能に対応しているか
を判断します。
この設計なら、別機種への対応範囲を限定できます。
3. TelemetryとObservabilityを強化する
実機システムでは「クラッシュした」という情報だけでは原因を特定できないことがあります。
1回の撮影セッションに対して、
Session ID
├─ Connection Events
├─ Mission State Transitions
├─ Flight Telemetry
├─ Recording Events
├─ Media Transfer Time
├─ Processing Time
├─ Upload Result
└─ Delivery Result
のようなTraceを保存したいところです。
これによって、
どの高度で何が起きたのか
その瞬間のConnection Stateはどうだったのか
CameraはRecording中だったのか
といった情報を後から追跡できます。
物理デバイスを扱うシステムでは、Observabilityは特に重要です。
4. Media PipelineをResumableにする
動画処理は時間がかかるため、
Download
Processing
Upload
の途中で通信やアプリ状態が変わる可能性があります。
そこで各処理を再開可能なJobとして扱います。
例えば、
Delivery ID: ABC123
Download ✅
Processing ✅
Upload ❌
という状態でUploadだけ失敗した場合、Downloadからやり直す必要はありません。
ユーザー向けのDelivery IDも保持したまま、内部処理だけ再実行できる設計にします。
5. Safetyをテスト可能なProduct Requirementにする
Manual OverrideやReturn-to-Homeを「安全機能として実装した」で終わらせず、明示的なテスト項目にします。
例えば、
Given:
Automated flight is active
When:
Pilot stick input is detected
Then:
Automated control stops
Virtual Stick is disabled
Manual control is restored
という形でSafety Requirementをテスト可能にします。
対象としては、
- Manual Override
- GPS / Telemetry Loss
- Connection Loss
- Mission Timeout
- Camera Failure
- Recording Failure
- Return-to-Home
などがあります。
Safetyは実装項目ではなく、検証可能なProduct Requirementとして扱うべきだと考えています。
まとめ
このプロジェクトを通して最も大きかった学びは、
ドローンソフトウェアは、ドローンを飛ばすソフトウェアだけではない
ということです。
実際には、
Physical Machine
×
Human Operator
×
Mobile Application
×
Media Pipeline
×
Cloud Infrastructure
×
Customer Experience
を、一つのシステムとして成立させる必要があります。
見た目として最も目立つのは自動飛行です。
しかしプロダクトとして価値を生んでいたのは、
撮影パターンを選ぶ
↓
ドローンが飛ぶ
↓
動画を取得する
↓
iPhone上で編集する
↓
CloudへUploadする
↓
QRコードを読み取る
↓
完成動画を受け取る
というEnd-to-Endの体験でした。
ロボティクスやAutonomous Systemを設計するとき、
「この機能は動くか」だけでなく、「ユーザーの目的が最後まで完了するか」
という視点を持つことが重要です。
飛行制御、モバイルアプリ、クラウド、UXは別々の技術領域に見えます。
しかし実際のプロダクトでは、それらの境界部分こそが最も多くの設計判断を必要とします。
このDrone-to-Cloudシステムは、そのことを強く実感したプロジェクトの一つでした。