Swiftでドローンを飛ばしてみる
この記事はSwift愛好会vol49@JapanTaxiでの発表の詳細の記事です。 発表のスライドはこちらです。 上記のスライドにもあるドローンを使ったエンタメに興味をもち、また愛好会の発表のネタにもなりそうだったので、Telloのドローンを買って、遊んでみました。
仕組み
下の図のように、iPhoneなどの端末とドローンをUDP通信で接続します。その後指定のコマンドを送ることで、ドローンがそのコマンド通りに動いてくれます。
実装
@eito_2さんのこちらの記事を参考にし、Network FrameworkのNWNetworkを使用して、実装しました。WWDC2019でそれに関する動画がありましたので、興味のある方はどうぞ。 まずはドローンのアドレス/ポート情報をまとめた構造体とコマンド一覧を定義したenumを準備します。コマンドは一部しか定義していないので、利用可能なコマンド一覧をみたい方は、SDK2.0 User Guideをご確認ください。
struct TelloConstants {
static let ipAddress = "192.168.10.1"
static let port: UInt16 = 8889
}
enum TelloCommands {
case start
case takeoff
case land
case emergency
// Min: 20, Max: 500(cm)
case left(x: Int)
case right(x: Int)
case forward(x: Int)
case back(x: Int)
case up(x: Int)
case down(x: Int)
// Min: 1, Max: 360(degree)
case rotateRight(x: Int)
case rotateLeft(x: Int)
case flip(direction: TelloDirection)
}
extension TelloCommands {
var asString: String {
switch self {
case .start: return "command"
case .land: return "land"
case .takeoff: return "takeoff"
case .emergency: return "emergency"
case .left(let x): return "left \(x)"
case .right(let x): return "right \(x)"
case .forward(let x): return "forward \(x)"
case .back(let x): return "back \(x)"
case .up(let x): return "up \(x)"
case .down(let x): return "down \(x)"
case .rotateRight(let x): return "cw \(x)"
case .rotateLeft(let x): return "ccw \(x)"
case .flip(let direction): return "flip \(direction.asString)"
}
}
}
enum TelloDirection {
case left
case right
case forward
case back
var asString: String {
switch self {
case .left: return "l"
case .right: return "r"
case .forward: return "f"
case .back: return "b"
}
}
}
次にドローンとの接続と実際にコマンドを送信する実装です。 init時に、stateUpdateHandler
にクロージャーをセットすることで、ドローンとの接続状況を確認できるようにしています。 またメソッドとしては、以下の3つを用意しました。
- コマンドのenumを受け取り、コマンド実行後のレスポンスを受信後、指定のクロージャにResult型で結果を通知する。
- コマンドを受け取り、その実行結果をSingleで返す(1のラッパーメソッド)。
- コマンドの配列とコマンド実行間隔の秒数を受け取り、そのコマンドを順に実行し、その実行結果をObservableで返す。
class TelloManager {
let connection = NWConnection(host: .init(TelloConstants.ipAddress), port: .init(integerLiteral: TelloConstants.port), using: .udp)
init() {
connection.stateUpdateHandler = { [unowned self] state in
switch state {
case .setup:
print("Setup")
case .waiting(let error):
print(error)
case .preparing:
print("preparing")
case .ready:
self.connection.start(queue: .global())
print("ready")
case .failed(let error):
print(error)
case .cancelled:
print("cancelled")
@unknown default:
fatalError("Unknown error")
}
}
}
//1.コマンドのenumを受け取り、コマンド実行後のレスポンスを受信後、指定のクロージャにResult型で結果を通知する。
func send(command: TelloCommands, completion: @escaping ((Result<String, Error>) -> Void)) {
print("Commands: ", command.asString)
let message = command.asString.data(using: .utf8)
connection.send(content: message, contentContext: .defaultMessage, isComplete: true, completion: .contentProcessed({ error in
if let error = error {
completion(.failure(error))
}
}))
connection.receive(minimumIncompleteLength: 0, maximumLength: Int(Int32.max)) { (data, context, isComplete, error) in
if !isComplete, let error = error {
completion(.failure(error))
} else if let data = data, let message = String(data: data, encoding: .utf8) {
completion(.success(message))
} else {
print("Unknown error")
}
}
}
//2.コマンドを受け取り、その実行結果をSingleで返す(1のラッパーメソッド)。
func send(command: TelloCommands) -> Single<String> {
return Single<String>.create { [unowned self] single -> Disposable in
self.send(command: command) { result in
switch result {
case .success(let message):
single(.success(message))
case .failure(let error):
single(.error(error))
}
}
return Disposables.create()
}
}
//3.コマンドの配列とコマンド実行間隔の秒数を受け取り、そのコマンドを順に実行し、その実行結果をObservableで返す。
func sends(parameters: (commands: [TelloCommands], interval: Int)) -> Observable<String> {
let commandsObservables = parameters.commands.map(send)
.map({ $0.asObservable().delay(.seconds(parameters.interval), scheduler: MainScheduler.instance) })
return Observable.concat(commandsObservables)
}
}
ViewControllerの実装は以下の通りです。 このドローンの制約上、最初にcommand
というコマンドを最初に送信しないといけないので、setup
というボタンをタップしてから、その後の操作に入るようになっています。 その後takeoff
ボタンをタップしたら離陸、land
ボタンを押したら着陸、 pattern1
を押したら、2秒間隔で「離陸→60cm上昇→30cm下降→左に100cm移動→右に100cm移動→前にフリップ→後ろにフリップ→着陸」が実行されます。 ちなみにフリップはこんな動きをします。
class ViewController: UIViewController {
@IBOutlet weak var setupButton: UIButton!
@IBOutlet weak var takeOffButton: UIButton!
@IBOutlet weak var landButton: UIButton!
@IBOutlet weak var rightButton: UIButton!
@IBOutlet weak var leftButton: UIButton!
@IBOutlet weak var pattern1Button: UIButton!
private let resultRelay = PublishRelay<String>()
private let disposeBag = DisposeBag()
private let tello = TelloManager()
override func viewDidLoad() {
super.viewDidLoad()
setupButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance)
.map({ _ in .start })
.flatMap(tello.send)
.bind(to: resultRelay)
.disposed(by: disposeBag)
takeOffButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance)
.map({ _ in .takeoff })
.flatMap(tello.send)
.bind(to: resultRelay)
.disposed(by: disposeBag)
landButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance)
.map({ _ in .land })
.flatMap(tello.send)
.bind(to: resultRelay)
.disposed(by: disposeBag)
rightButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance)
.map({ _ in .right(x: 100) })
.flatMap(tello.send)
.bind(to: resultRelay)
.disposed(by: disposeBag)
leftButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance)
.map({ _ in .left(x: 100) })
.flatMap(tello.send)
.bind(to: resultRelay)
.disposed(by: disposeBag)
pattern1Button.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance)
.map({ _ -> [TelloCommands] in [.takeoff, .up(x: 60), .down(x: 30), .left(x: 100), .right(x: 100), .flip(direction: .forward), .flip(direction: .back), .land] })
.map({ ($0, 2) })
.flatMap(tello.sends)
.bind(to: resultRelay)
.disposed(by: disposeBag)
resultRelay.asObservable()
.subscribe(onNext: { message in
print("Message: ", message)
}, onError: { (error) in
print("Error: ", error)
})
.disposed(by: disposeBag)
}
}
まとめ
普段はモバイル内のみを動かすことが多いSwiftですが、ドローンを動かせたのは新鮮でした。 Try!Swift2019ではSwiftでラズパイを動かす発表もあったので、いずれそちらも触ってみたいです。 またこのドローンはカメラの情報も受け取れる仕組みになっているので、その情報を受け取って、写真や動画の撮影できるようにするのも、いずれ実装したいです(今回は時間がなくて、断念しました)。 ソースコードはこちらです。
参考
•https://www.ryzerobotics.com/jp/tello •https://qiita.com/eito_2/items/0d9e2c92b0be0ea16e77 •https://dl-cdn.ryzerobotics.com/downloads/Tello/Tello%20SDK%202.0%20User%20Guide.pdf