├── .gitignore ├── .gitmodules ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .travis.yml ├── LICENSE ├── Package.resolved ├── Package.swift ├── QuadKit.png ├── README.md ├── Sources ├── QuadKit Examples │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ └── ExampleView.swift └── QuadKit │ ├── BindingSupport.swift │ ├── EachineE10W.swift │ ├── InputController.swift │ ├── InputState.swift │ └── QuadClient.swift └── Tests ├── QuadKit.xctestplan └── QuadKitTests └── InputStateTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | .build/ 26 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieloc/QuadKit/aa514a734b7bcc26c74a5f4765f737f2e149a7ce/.gitmodules -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode8.2 3 | xcode_project: QuadKit.xcodeproj 4 | xcode_scheme: QuadKit_iOS #4 5 | xcode_sdk: iphonesimulator10.2 #5 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017 GABRIEL O'FLAHERTY-CHAN 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swiftsocket", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/trisbee/SwiftSocket", 7 | "state" : { 8 | "revision" : "a7e958beb73c5a87cc6315b8a29b67e98138967f", 9 | "version" : "2.2.0" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "QuadKit", 7 | products: [ 8 | .library( 9 | name: "QuadKit", 10 | targets: ["QuadKit"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/trisbee/SwiftSocket", from: "2.2.0"), 14 | ], 15 | targets: [ 16 | .target( 17 | name: "QuadKit", 18 | dependencies: ["SwiftSocket"] 19 | ), 20 | .testTarget( 21 | name: "QuadKitTests", 22 | dependencies: ["QuadKit"] 23 | ), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /QuadKit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieloc/QuadKit/aa514a734b7bcc26c74a5f4765f737f2e149a7ce/QuadKit.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![QuadKit](QuadKit.png) 2 | 3 | **A simple set of interfaces for communicating with your Wi-Fi enabled quadcopter**. 4 | 5 | QuadKit powers [SCARAB](https://itunes.apple.com/us/app/scarab-rc-controller-for-quadcopters/id1205279859), an RC controller for iOS. Read about the making of QuadKit: [Building a Quadcopter Controller for iOS and Open-Sourcing the Internals](https://medium.com/@_gabrieloc/building-a-quadcopter-controller-for-ios-and-open-sourcing-the-internals-3bbc7f526ed2#.8c7aaatsv) 6 | 7 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 8 | 9 | ## Installation 10 | 11 | Add this to your Cartfile: 12 | `github "gabrieloc/QuadKit"` 13 | 14 | To test the examples, make sure [Carthage](https://github.com/Carthage/Carthage#installing-carthage) is installed, then run: 15 | 16 | `carthage update` 17 | 18 | 19 | ## Integration 20 | 21 | Ensure any project using this framework includes `QuadKit.framework` and `SwiftSocket.framework` as embedded binaries. 22 | 23 | To connect to your quadcopter, create an instance of `QuadClient` and call `-connect:`. Ensure that your device has been connected to the Wi-Fi network created by your quadcopter, before attempting to connect. Once a connection has been established, the client expects to be passed an `InputState` object via `-updateInput:`, in response to user interaction. 24 | 25 | ### Example 26 | 27 | ``` Swift 28 | 29 | let client = QuadClient() 30 | let inputState = InputState() 31 | let thrustControl = UIControl() 32 | 33 | func connectClient() { 34 | if let error = client.connect() { 35 | // Error connecting 36 | } 37 | thrustControl.enabled = true 38 | } 39 | 40 | func thrustControlTouched(_ thrustControl: UIControl) { 41 | inputState.thrust = thrustControl.selected ? 1.0 : 0.0 42 | client.updateInput(with inputState) 43 | } 44 | ``` 45 | 46 | iOS and macOS samples can be found inside the `Examples/` directory. 47 | 48 | 49 | ## Contributing 50 | 51 | While the format for input data appears to be generic across models (needs to be verified), it's possible that handshake data isn't. For this reason, `BindingSupport` defines a protocol `QuadcopterModel`, which individual models can use for providing this data. 52 | 53 | Handshake data can be found by running a tool like `tcpdump` or `Wireshark` to get a packet trace, and collecting the first few packets your device sends your quadcopter in the software provided by it's manufacturer. 54 | 55 | ### Example 56 | 57 | 1. Connect your device via USB and create a RVI: `$ rvictl -s (DEVICE UDID)` 58 | 2. Ensure the remote interface was created by running: `$ ifconfig -l` and verifying `rvi0` exists 59 | 3. Turn on your quadcopter and join it's Wi-Fi network 60 | 4. In a tool like [Wireshark](https://www.wireshark.org), begin capturing `rvi0` 61 | 5. Open the software provided by your quadcopter's manufacturer and access the input controls 62 | 6. Stop capturing and find what looks like identification data. It should consistently look the same every time you try to connect the app, and will appear before you begin receiving any form of input or video data 63 | 7. Create a new class in `QuadKit` representing your quadcopter model, and have it adopt `QuadcopterModel` 64 | 8. In your class' implementation of `-identification`, return the data your recorded in the form of byte arrays 65 | 66 | 67 | ## Reference 68 | 69 | Apple provides step by step instructions for creating using various network debugging tools: [Technical Q&A QA1176: Getting a Packet Trace](https://developer.apple.com/library/content/qa/qa1176/_index.html). 70 | 71 | For more information on the protocol, refer to Jim Hung's [Hubsan X4 H107L Quadcopter Control Protocol Specification](http://www.jimhung.co.uk/wp-content/uploads/2014/11/HubsanX4_ProtocolSpec_v1.txt), or his excellent reverse engineering series, [Reverse Engineering a Hubsan X4 Quadcopter](http://www.jimhung.co.uk/?p=1424). 72 | 73 | -------------------------------------------------------------------------------- /Sources/QuadKit Examples/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // QuadKit iOS Example 4 | // 5 | // Copyright (c) <2017>, Gabriel O'Flaherty-Chan 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 1. Redistributions of source code must retain the above copyright 11 | // notice, this list of conditions and the following disclaimer. 12 | // 2. Redistributions in binary form must reproduce the above copyright 13 | // notice, this list of conditions and the following disclaimer in the 14 | // documentation and/or other materials provided with the distribution. 15 | // 3. All advertising materials mentioning features or use of this software 16 | // must display the following acknowledgement: 17 | // This product includes software developed by skysent. 18 | // 4. Neither the name of the skysent nor the 19 | // names of its contributors may be used to endorse or promote products 20 | // derived from this software without specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY skysent ''AS IS'' AND ANY 23 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL skysent BE LIABLE FOR ANY 26 | // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 29 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import UIKit 35 | 36 | @UIApplicationMain 37 | class AppDelegate: UIResponder, UIApplicationDelegate { 38 | 39 | var window: UIWindow? 40 | 41 | 42 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 43 | // Override point for customization after application launch. 44 | return true 45 | } 46 | 47 | func applicationWillResignActive(_ application: UIApplication) { 48 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 49 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 50 | } 51 | 52 | func applicationDidEnterBackground(_ application: UIApplication) { 53 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 54 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 55 | } 56 | 57 | func applicationWillEnterForeground(_ application: UIApplication) { 58 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 59 | } 60 | 61 | func applicationDidBecomeActive(_ application: UIApplication) { 62 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 63 | } 64 | 65 | func applicationWillTerminate(_ application: UIApplication) { 66 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 67 | } 68 | 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /Sources/QuadKit Examples/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Sources/QuadKit Examples/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Sources/QuadKit Examples/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 47 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Sources/QuadKit Examples/ExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleView.swift 3 | // QuadKit 4 | // 5 | // Copyright (c) <2017>, Gabriel O'Flaherty-Chan 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 1. Redistributions of source code must retain the above copyright 11 | // notice, this list of conditions and the following disclaimer. 12 | // 2. Redistributions in binary form must reproduce the above copyright 13 | // notice, this list of conditions and the following disclaimer in the 14 | // documentation and/or other materials provided with the distribution. 15 | // 3. All advertising materials mentioning features or use of this software 16 | // must display the following acknowledgement: 17 | // This product includes software developed by skysent. 18 | // 4. Neither the name of the skysent nor the 19 | // names of its contributors may be used to endorse or promote products 20 | // derived from this software without specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY skysent ''AS IS'' AND ANY 23 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL skysent BE LIABLE FOR ANY 26 | // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 29 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import UIKit 35 | 36 | class ExampleView: UIView, ControlView { 37 | 38 | @IBOutlet weak var thrust: UIButton! 39 | @IBOutlet weak var connectButton: UIButton! 40 | 41 | public var inputHandler: InputHandler? 42 | 43 | override func awakeFromNib() { 44 | super.awakeFromNib() 45 | 46 | thrust.isEnabled = false 47 | } 48 | 49 | func connectionUpdated(_ connected: Bool) { 50 | connectButton.isEnabled = !connected 51 | let connectTitle = connected ? "Connected" : "Reconnect" 52 | connectButton.setTitle(connectTitle, for: .normal) 53 | thrust.isEnabled = connected 54 | } 55 | 56 | @IBAction func beginThrust(_ sender: Any) { 57 | inputHandler?.inputUpdated(.thrust, selected: true) 58 | } 59 | 60 | @IBAction func endThrust(_ sender: Any) { 61 | inputHandler?.inputUpdated(.thrust, selected: false) 62 | } 63 | 64 | @IBAction func connectSelected(_ sender: Any) { 65 | inputHandler?.connectClient() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/QuadKit/BindingSupport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BindingSupport.swift 3 | // QuadKit 4 | // 5 | // Copyright (c) <2017>, Gabriel O'Flaherty-Chan 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 1. Redistributions of source code must retain the above copyright 11 | // notice, this list of conditions and the following disclaimer. 12 | // 2. Redistributions in binary form must reproduce the above copyright 13 | // notice, this list of conditions and the following disclaimer in the 14 | // documentation and/or other materials provided with the distribution. 15 | // 3. All advertising materials mentioning features or use of this software 16 | // must display the following acknowledgement: 17 | // This product includes software developed by skysent. 18 | // 4. Neither the name of the skysent nor the 19 | // names of its contributors may be used to endorse or promote products 20 | // derived from this software without specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY skysent ''AS IS'' AND ANY 23 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL skysent BE LIABLE FOR ANY 26 | // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 29 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import Foundation 35 | 36 | typealias Byte = UInt8 37 | typealias Packet = [Byte] 38 | 39 | struct Conversation { 40 | 41 | let topic: String 42 | let packets: [Packet] 43 | 44 | init(topic: String, packets: [Packet]) { 45 | self.topic = topic 46 | self.packets = packets 47 | } 48 | } 49 | 50 | protocol QuadcopterModel { 51 | 52 | var identification: Conversation { get } 53 | } 54 | 55 | extension QuadClient { 56 | 57 | var allIdentificationConversations: [Conversation] { 58 | 59 | // This list should grow over time 60 | let supportedModels = [ 61 | EachineE10W() 62 | ] 63 | 64 | return supportedModels.map { $0.identification } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/QuadKit/EachineE10W.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EachineE10W.swift 3 | // QuadKit 4 | // 5 | // Copyright (c) <2017>, Gabriel O'Flaherty-Chan 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 1. Redistributions of source code must retain the above copyright 11 | // notice, this list of conditions and the following disclaimer. 12 | // 2. Redistributions in binary form must reproduce the above copyright 13 | // notice, this list of conditions and the following disclaimer in the 14 | // documentation and/or other materials provided with the distribution. 15 | // 3. All advertising materials mentioning features or use of this software 16 | // must display the following acknowledgement: 17 | // This product includes software developed by skysent. 18 | // 4. Neither the name of the skysent nor the 19 | // names of its contributors may be used to endorse or promote products 20 | // derived from this software without specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY skysent ''AS IS'' AND ANY 23 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL skysent BE LIABLE FOR ANY 26 | // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 29 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import Foundation 35 | 36 | struct EachineE10W: QuadcopterModel { 37 | 38 | var identification: Conversation { 39 | let packets = identification1 + [identification2] + [inputIdentification] 40 | return Conversation(topic: "Eachine E10W Identification", packets: packets) 41 | } 42 | 43 | let identification1: [Packet] = [ 44 | [ 45 | 0x49, 0x54, 0x64, 0x00, 0x00, 0x00, 0x52, 0x00, 46 | 0x00, 0x00, 0x05, 0xa7, 0xa9, 0x0f, 0xb3, 0x6e, 47 | 0xcd, 0x3f, 0xa2, 0xca, 0x7e, 0xc4, 0x8c, 0xa3, 48 | 0x60, 0x04, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 49 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 50 | 0xe1, 0x85, 0x47, 0xb8, 0xc2, 0x2e, 0x21, 0xd0, 51 | 0x1b, 0xfb, 0x6b, 0x3d, 0xe3, 0x25, 0xa2, 0x7b, 52 | 0x8f, 0xb3, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 53 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 54 | 0xe1, 0x85, 0xb7, 0x33, 0x0f, 0xb7, 0xc9, 0x57, 55 | 0x82, 0xfc, 0x3d, 0x67, 0xe7, 0xc3, 0xa6, 0x67, 56 | 0x28, 0xda, 0xd8, 0xb5, 0x98, 0x48, 0xc7, 0x67, 57 | 0x0c, 0x94, 0xb2, 0x9b, 0x54, 0xd2, 0x37, 0x9e, 58 | 0x2e, 0x7a 59 | ], 60 | [ 61 | 0x49, 0x54, 0x64, 0x00, 0x00, 0x00, 0x52, 0x00, 62 | 0x00, 0x00, 0x21, 0xe0, 0xc4, 0x77, 0xc7, 0x73, 63 | 0x94, 0xe8, 0x5d, 0x66, 0xa9, 0x8c, 0x2c, 0x92, 64 | 0x2c, 0xc5, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 65 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 66 | 0xe1, 0x85, 0x47, 0xb8, 0xc2, 0x2e, 0x21, 0xd0, 67 | 0x1b, 0xfb, 0x6b, 0x3d, 0xe3, 0x25, 0xa2, 0x7b, 68 | 0x8f, 0xb3, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 69 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 70 | 0xe1, 0x85, 0xb7, 0x33, 0x0f, 0xb7, 0xc9, 0x57, 71 | 0x82, 0xfc, 0x3d, 0x67, 0xe7, 0xc3, 0xa6, 0x67, 72 | 0x28, 0xda, 0xd8, 0xb5, 0x98, 0x48, 0xc7, 0x67, 73 | 0x0c, 0x94, 0xb2, 0x9b, 0x54, 0xd2, 0x37, 0x9e, 74 | 0x2e, 0x7a 75 | ], 76 | [ 77 | 0x49, 0x54, 0x64, 0x00, 0x00, 0x00, 0x56, 0x00, 78 | 0x00, 0x00, 0x54, 0x69, 0x6d, 0x47, 0xa5, 0x41, 79 | 0x85, 0x86, 0x00, 0x72, 0x9e, 0x0a, 0x5b, 0xa1, 80 | 0x90, 0x37, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 81 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 82 | 0xe1, 0x85, 0x47, 0xb8, 0xc2, 0x2e, 0x21, 0xd0, 83 | 0x1b, 0xfb, 0x6b, 0x3d, 0xe3, 0x25, 0xa2, 0x7b, 84 | 0x8f, 0xb3, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 85 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 86 | 0xe1, 0x85, 0x3b, 0x30, 0x70, 0x85, 0xef, 0x35, 87 | 0xf4, 0x3e, 0xaf, 0x44, 0xcf, 0x6f, 0xc7, 0x9c, 88 | 0x5d, 0x1e, 0x38, 0x27, 0x5b, 0x07, 0xf2, 0xd7, 89 | 0x3f, 0xc0, 0xa7, 0x38, 0xdc, 0x49, 0xd6, 0x0f, 90 | 0xfe, 0x9f 91 | ], 92 | [ 93 | 0x49, 0x54, 0x64, 0x00, 0x00, 0x00, 0x52, 0x00, 94 | 0x00, 0x00, 0x2f, 0x2e, 0xe9, 0x40, 0x93, 0x91, 95 | 0x9e, 0x15, 0x36, 0x09, 0x66, 0x26, 0x02, 0x81, 96 | 0xef, 0xa5, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 97 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 98 | 0xe1, 0x85, 0x47, 0xb8, 0xc2, 0x2e, 0x21, 0xd0, 99 | 0x1b, 0xfb, 0x6b, 0x3d, 0xe3, 0x25, 0xa2, 0x7b, 100 | 0x8f, 0xb3, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 101 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 102 | 0xe1, 0x85, 0xb7, 0x33, 0x0f, 0xb7, 0xc9, 0x57, 103 | 0x82, 0xfc, 0x3d, 0x67, 0xe7, 0xc3, 0xa6, 0x67, 104 | 0x28, 0xda, 0xd8, 0xb5, 0x98, 0x48, 0xc7, 0x67, 105 | 0x0c, 0x94, 0xb2, 0x9b, 0x54, 0xd2, 0x37, 0x9e, 106 | 0x2e, 0x7a 107 | ], 108 | [ 109 | 0x49, 0x54, 0x64, 0x00, 0x00, 0x00, 0x52, 0x00, 110 | 0x00, 0x00, 0x09, 0x2d, 0x16, 0x6d, 0xae, 0xbe, 111 | 0x5e, 0xc5, 0xdb, 0x47, 0x35, 0x07, 0x76, 0x7f, 112 | 0x88, 0x9b, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 113 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 114 | 0xe1, 0x85, 0x47, 0xb8, 0xc2, 0x2e, 0x21, 0xd0, 115 | 0x1b, 0xfb, 0x6b, 0x3d, 0xe3, 0x25, 0xa2, 0x7b, 116 | 0x8f, 0xb3, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 117 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 118 | 0xe1, 0x85, 0xb7, 0x33, 0x0f, 0xb7, 0xc9, 0x57, 119 | 0x82, 0xfc, 0x3d, 0x67, 0xe7, 0xc3, 0xa6, 0x67, 120 | 0x28, 0xda, 0xd8, 0xb5, 0x98, 0x48, 0xc7, 0x67, 121 | 0x0c, 0x94, 0xb2, 0x9b, 0x54, 0xd2, 0x37, 0x9e, 122 | 0x2e, 0x7a 123 | ], 124 | [ 125 | 0x49, 0x54, 0x64, 0x00, 0x00, 0x00, 0x52, 0x00, 126 | 0x00, 0x00, 0x0d, 0xcf, 0x53, 0x3a, 0x42, 0x51, 127 | 0x63, 0x8c, 0x31, 0x25, 0x94, 0x8b, 0x99, 0x09, 128 | 0xf1, 0xbd, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 129 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 130 | 0xe1, 0x85, 0x47, 0xb8, 0xc2, 0x2e, 0x21, 0xd0, 131 | 0x1b, 0xfb, 0x6b, 0x3d, 0xe3, 0x25, 0xa2, 0x7b, 132 | 0x8f, 0xb3, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 133 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 134 | 0xe1, 0x85, 0xb7, 0x33, 0x0f, 0xb7, 0xc9, 0x57, 135 | 0x82, 0xfc, 0x3d, 0x67, 0xe7, 0xc3, 0xa6, 0x67, 136 | 0x28, 0xda, 0xd8, 0xb5, 0x98, 0x48, 0xc7, 0x67, 137 | 0x0c, 0x94, 0xb2, 0x9b, 0x54, 0xd2, 0x37, 0x9e, 138 | 0x2e, 0x7a 139 | ], 140 | [ 141 | 0x49, 0x54, 0x64, 0x00, 0x00, 0x00, 0x5d, 0x00, 142 | 0x00, 0x00, 0xb5, 0x4f, 0x38, 0xc7, 0x5c, 0xe4, 143 | 0x9a, 0xca, 0xa9, 0x0a, 0x73, 0x7d, 0x7d, 0x84, 144 | 0xdb, 0xdc, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 145 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 146 | 0xe1, 0x85, 0x47, 0xb8, 0xc2, 0x2e, 0x21, 0xd0, 147 | 0x1b, 0xfb, 0x6b, 0x3d, 0xe3, 0x25, 0xa2, 0x7b, 148 | 0x8f, 0xb3, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 149 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 150 | 0xe1, 0x85, 0xee, 0x2e, 0x09, 0xa3, 0x9b, 0xdd, 151 | 0x05, 0xc8, 0x30, 0xa2, 0x81, 0xc8, 0x2a, 0x9e, 152 | 0xda, 0x7f, 0x49, 0x5c, 0xf8, 0xc7, 0x21, 0x31, 153 | 0xd4, 0x89, 0xa8, 0xcf, 0x63, 0x6e, 0xab, 0xbd, 154 | 0x64, 0x3c 155 | ] 156 | ] 157 | let identification2: Packet = [ 158 | 0x49, 0x54, 0x64, 0x00, 0x00, 0x00, 0x52, 0x00, 159 | 0x00, 0x00, 0x72, 0x98, 0xc0, 0x38, 0x9b, 0xc3, 160 | 0x72, 0xa7, 0x1a, 0x17, 0x4b, 0xd1, 0xb5, 0x14, 161 | 0xb3, 0xad, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 162 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 163 | 0xe1, 0x85, 0x47, 0xb8, 0xc2, 0x2e, 0x21, 0xd0, 164 | 0x1b, 0xfb, 0x6b, 0x3d, 0xe3, 0x25, 0xa2, 0x7b, 165 | 0x8f, 0xb3, 0xac, 0xef, 0x63, 0xf7, 0x71, 0x57, 166 | 0xab, 0x2f, 0x53, 0xe3, 0xf7, 0x68, 0xec, 0xd9, 167 | 0xe1, 0x85, 0xb7, 0x33, 0x0f, 0xb7, 0xc9, 0x57, 168 | 0x82, 0xfc, 0x3d, 0x67, 0xe7, 0xc3, 0xa6, 0x67, 169 | 0x28, 0xda, 0xd8, 0xb5, 0x98, 0x48, 0xc7, 0x67, 170 | 0x0c, 0x94, 0xb2, 0x9b, 0x54, 0xd2, 0x37, 0x9e, 171 | 0x2e, 0x7a 172 | ] 173 | 174 | let inputIdentification: Packet = [ 175 | 0x00, 0x00, 0x01, 0xa0, 0x00, 0x4f, 0x00, 0x03, 176 | 0xe6, 0x0b, 0x00, 0x00, 0x38, 0x24, 0xad, 0x49, 177 | 0x01, 0x5d, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 178 | 0xd0, 0x02, 0x40, 0x02, 0x00, 0x02, 0x00, 0x00, 179 | 0xe6, 0x0b, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00 180 | ] 181 | } 182 | -------------------------------------------------------------------------------- /Sources/QuadKit/InputController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuadControl.swift 3 | // QuadKit 4 | // 5 | // Copyright (c) <2017>, Gabriel O'Flaherty-Chan 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 1. Redistributions of source code must retain the above copyright 11 | // notice, this list of conditions and the following disclaimer. 12 | // 2. Redistributions in binary form must reproduce the above copyright 13 | // notice, this list of conditions and the following disclaimer in the 14 | // documentation and/or other materials provided with the distribution. 15 | // 3. All advertising materials mentioning features or use of this software 16 | // must display the following acknowledgement: 17 | // This product includes software developed by skysent. 18 | // 4. Neither the name of the skysent nor the 19 | // names of its contributors may be used to endorse or promote products 20 | // derived from this software without specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY skysent ''AS IS'' AND ANY 23 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL skysent BE LIABLE FOR ANY 26 | // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 29 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import Foundation 35 | import SwiftSocket 36 | 37 | // Represents the data (in bytes) sent to the quadcopter 38 | struct InputData { 39 | 40 | let start: (Byte, Byte) = (0x66, 0xCC) 41 | let end: (Byte, Byte) = (0x99, 0x33) 42 | let data: (Byte, Byte, Byte, Byte) 43 | 44 | init(_ data: (Byte, Byte, Byte, Byte)) { 45 | self.data = data 46 | } 47 | 48 | func createChecksum(_ bytes: [Byte]) -> Byte { 49 | return bytes.reduce ( 0, { $0 ^ $1 }) & 0xFF 50 | } 51 | 52 | public var formatted: [Byte] { 53 | let bytes = [data.0, data.1, data.2, data.3, 0x0] 54 | let checksum = createChecksum(bytes) 55 | return [start.0] + bytes + [checksum, end.0, start.1] + bytes + [checksum, end.1] 56 | } 57 | } 58 | 59 | struct InputController { 60 | 61 | var state = InputState() 62 | 63 | var data: [Byte] { 64 | 65 | var yaw: Byte = 0 66 | var roll: Byte = 0 67 | var pitch: Byte = 0 68 | var thrust: Byte = 0 69 | state.getFinalValues(roll: &roll, 70 | pitch: &pitch, 71 | thrust: &thrust, 72 | yaw: &yaw) 73 | 74 | // NOTE: This format may vary by model. Consider refactoring if this is the case. 75 | let data = InputData((roll, pitch, thrust, yaw)) 76 | return data.formatted 77 | } 78 | 79 | var prettyData: String { 80 | 81 | let hexData = data.map { "0x\(String(format: "%X", $0))" } 82 | return hexData.joined(separator: ", ") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/QuadKit/InputState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputState.swift 3 | // QuadKit 4 | // 5 | // Created by Gabriel O'Flaherty-Chan on 2017-03-01. 6 | // Copyright © 2017 Gabrieloc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func clamp(_ x: Double, min: Double, max: Double) -> Double { 12 | return Swift.min(max, Swift.max(min, x)) 13 | } 14 | 15 | func clamp(_ x: Int, min: Int, max: Int) -> Int { 16 | return Swift.min(max, Swift.max(min, x)) 17 | } 18 | 19 | extension Double { 20 | var byteValue: Byte { 21 | return Byte(clamp(self, min: 0.0, max: 1.0) * 0xFF) 22 | } 23 | } 24 | 25 | public struct InputState { 26 | 27 | public var trimPitch = 0.0 // Z Movement Calibration 28 | public var trimRoll = 0.0 // X Movement Calibration 29 | 30 | // Y Movement 31 | public var thrust: Double { 32 | get { return _trimPitch } 33 | set { _trimPitch = clamp(newValue, min: 0, max: 1) } 34 | } 35 | 36 | // X Movement 37 | public var pitch: Double { 38 | get { return _pitch } 39 | set { _pitch = clamp(newValue, min: -1, max: 1) } 40 | } 41 | 42 | // Z Movement 43 | public var roll: Double { 44 | get { return _roll } 45 | set { _roll = clamp(newValue, min: -1, max: 1) } 46 | } 47 | 48 | // Y Rotation 49 | public var yaw: Double { 50 | get { return _yaw } 51 | set { _yaw = clamp(newValue, min: -1, max: 1) } 52 | } 53 | 54 | private var _trimPitch = 0.0 55 | private var _trimRoll = 0.0 56 | private var _thrust = 0.0 57 | private var _pitch = 0.0 58 | private var _roll = 0.0 59 | private var _yaw = 0.0 60 | 61 | private enum InputType: Int { 62 | case thrust, pitch, roll, yaw 63 | } 64 | 65 | public init() {} 66 | 67 | func normalized() -> InputState { 68 | 69 | var n = InputState() 70 | 71 | n.trimPitch = trimPitch 72 | n.trimRoll = trimRoll 73 | 74 | n.thrust = thrust 75 | n.pitch = (pitch + 1.0) * 0.5 76 | n.roll = (roll + 1.0) * 0.5 77 | n.yaw = (yaw + 1.0) * 0.5 78 | 79 | return n 80 | } 81 | 82 | func getFinalValues(roll: inout Byte, pitch: inout Byte, thrust: inout Byte, yaw: inout Byte) { 83 | 84 | let normalized = self.normalized() 85 | 86 | let calibratedRoll = Int(normalized.roll.byteValue) + Int(normalized.trimRoll) 87 | let calibratedPitch = Int(normalized.pitch.byteValue) + Int(normalized.trimPitch) 88 | 89 | roll = Byte(clamp(calibratedRoll, min: 0, max: Int(Byte.max))) 90 | pitch = Byte(clamp(calibratedPitch, min: 0, max: Int(Byte.max))) 91 | thrust = normalized.thrust.byteValue 92 | yaw = normalized.yaw.byteValue 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/QuadKit/QuadClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client.swift 3 | // QuadKit 4 | // 5 | // Copyright (c) <2017>, Gabriel O'Flaherty-Chan 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 1. Redistributions of source code must retain the above copyright 11 | // notice, this list of conditions and the following disclaimer. 12 | // 2. Redistributions in binary form must reproduce the above copyright 13 | // notice, this list of conditions and the following disclaimer in the 14 | // documentation and/or other materials provided with the distribution. 15 | // 3. All advertising materials mentioning features or use of this software 16 | // must display the following acknowledgement: 17 | // This product includes software developed by skysent. 18 | // 4. Neither the name of the skysent nor the 19 | // names of its contributors may be used to endorse or promote products 20 | // derived from this software without specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY skysent ''AS IS'' AND ANY 23 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL skysent BE LIABLE FOR ANY 26 | // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 29 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import Foundation 35 | import SwiftSocket 36 | 37 | 38 | public class QuadClient { 39 | 40 | let udpClient = UDPClient(address: "172.16.10.1", port: 8895) 41 | let tcpClient = TCPClient(address: "172.16.10.1", port: 8888) 42 | var controller = InputController() 43 | 44 | var verbose = false 45 | 46 | public struct QuadClientError: Error { 47 | enum ErrorKind { 48 | case connectionTimeout 49 | case bindingFailed 50 | } 51 | let kind: ErrorKind 52 | let reason: String 53 | } 54 | 55 | func print(_ items: Any) { 56 | if verbose { 57 | Swift.print(items) 58 | } 59 | } 60 | 61 | public init() {} 62 | 63 | public func connect(withTimeout timeout: Int = 10, verbose: Bool = false) -> QuadClientError? { 64 | 65 | self.verbose = verbose 66 | 67 | if let connectionError = connectClient(withTimeout: timeout) { 68 | print("Couldn't connect: \(connectionError.reason)") 69 | return connectionError 70 | } 71 | 72 | if let bindingError = performBindingChoreography() { 73 | return bindingError 74 | } 75 | 76 | beginBroadcasting() 77 | 78 | return nil 79 | } 80 | 81 | func connectClient(withTimeout timeout: Int) -> QuadClientError? { 82 | 83 | switch tcpClient.connect(timeout: timeout) { 84 | case .failure(let socketError): 85 | return QuadClientError(kind: .connectionTimeout, reason: socketError.localizedDescription) 86 | default: 87 | return nil 88 | } 89 | } 90 | 91 | func performBindingChoreography() -> QuadClientError? { 92 | 93 | func isConversationValid(_ conversation: Conversation) -> Bool { 94 | 95 | let description = conversation.topic 96 | 97 | return conversation.packets.reduce(false, { (identified, message) -> Bool in 98 | 99 | print("\(description): Sending message \(message)") 100 | switch tcpClient.send(data: message) { 101 | case .success: 102 | let response = tcpClient.read(1024*10, timeout: 5) ?? [] 103 | print("\(description): Received response \(response.count) bytes long") 104 | return true 105 | case .failure(let error): 106 | print("\(description): Failed: \(error)") 107 | return false 108 | } 109 | }) 110 | } 111 | 112 | let identified = allIdentificationConversations.reduce(true, { 113 | $0 && isConversationValid($1) 114 | }) 115 | 116 | return identified ? nil : QuadClientError(kind: .bindingFailed, reason: "One or more binding errors") 117 | } 118 | 119 | // MARK: Input 120 | 121 | static let InputSyncInterval: TimeInterval = 0.05 122 | 123 | func beginBroadcasting() { 124 | 125 | udpClient.enableBroadcast() 126 | Timer.scheduledTimer(timeInterval: QuadClient.InputSyncInterval, 127 | target: self, 128 | selector: #selector(sendInputData), 129 | userInfo: nil, 130 | repeats: true) 131 | } 132 | 133 | public func updateInput(with state: InputState) { 134 | controller.state = state 135 | } 136 | 137 | @objc func sendInputData() { 138 | 139 | let inputData = controller.data 140 | switch udpClient.send(data: inputData) { 141 | case .success: 142 | print(controller.prettyData) 143 | break 144 | case .failure(let error): 145 | print("Failed to send input: \(error)") 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Tests/QuadKit.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "63F5E43C-2354-4343-A2B7-317E58D42EC7", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | 13 | }, 14 | "testTargets" : [ 15 | 16 | ], 17 | "version" : 1 18 | } 19 | -------------------------------------------------------------------------------- /Tests/QuadKitTests/InputStateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputStateTests.swift 3 | // QuadKit 4 | // 5 | // Copyright (c) <2017>, Gabriel O'Flaherty-Chan 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 1. Redistributions of source code must retain the above copyright 11 | // notice, this list of conditions and the following disclaimer. 12 | // 2. Redistributions in binary form must reproduce the above copyright 13 | // notice, this list of conditions and the following disclaimer in the 14 | // documentation and/or other materials provided with the distribution. 15 | // 3. All advertising materials mentioning features or use of this software 16 | // must display the following acknowledgement: 17 | // This product includes software developed by skysent. 18 | // 4. Neither the name of the skysent nor the 19 | // names of its contributors may be used to endorse or promote products 20 | // derived from this software without specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY skysent ''AS IS'' AND ANY 23 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL skysent BE LIABLE FOR ANY 26 | // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 29 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import XCTest 35 | @testable import QuadKit 36 | 37 | class InputStateTests: XCTestCase { 38 | 39 | func createInputStateMax() -> InputState { 40 | var inputState = InputState() 41 | inputState.thrust = Double.greatestFiniteMagnitude 42 | inputState.pitch = Double.greatestFiniteMagnitude 43 | inputState.roll = Double.greatestFiniteMagnitude 44 | inputState.yaw = Double.greatestFiniteMagnitude 45 | return inputState 46 | } 47 | 48 | func createInputStateMin() -> InputState { 49 | var inputState = InputState() 50 | inputState.thrust = -Double.greatestFiniteMagnitude 51 | inputState.pitch = -Double.greatestFiniteMagnitude 52 | inputState.roll = -Double.greatestFiniteMagnitude 53 | inputState.yaw = -Double.greatestFiniteMagnitude 54 | return inputState 55 | } 56 | 57 | func testInputStateMaxValues() { 58 | 59 | let inputState = createInputStateMax() 60 | XCTAssertEqual(inputState.thrust, 1) 61 | XCTAssertEqual(inputState.pitch, 1) 62 | XCTAssertEqual(inputState.roll, 1) 63 | XCTAssertEqual(inputState.yaw, 1) 64 | } 65 | 66 | func testInputStateMinValues() { 67 | 68 | let inputState = createInputStateMin() 69 | XCTAssertEqual(inputState.thrust, 0) 70 | XCTAssertEqual(inputState.pitch, -1) 71 | XCTAssertEqual(inputState.roll, -1) 72 | XCTAssertEqual(inputState.yaw, -1) 73 | } 74 | 75 | func testInputStateNormalizedValues() { 76 | 77 | let normalized = InputState().normalized() 78 | 79 | XCTAssertEqual(normalized.thrust, 0) 80 | XCTAssertEqual(normalized.pitch, 0.5) 81 | XCTAssertEqual(normalized.roll, 0.5) 82 | XCTAssertEqual(normalized.yaw, 0.5) 83 | } 84 | 85 | func testInputStateMaxNormalizedValues() { 86 | 87 | let inputState = createInputStateMax() 88 | let normalized = inputState.normalized() 89 | 90 | XCTAssertEqual(normalized.thrust, 1) 91 | XCTAssertEqual(normalized.pitch, 1) 92 | XCTAssertEqual(normalized.roll, 1) 93 | XCTAssertEqual(normalized.yaw, 1) 94 | } 95 | 96 | func testInputStateMinNormalizedValues() { 97 | 98 | let inputState = createInputStateMin() 99 | let normalized = inputState.normalized() 100 | 101 | XCTAssertEqual(normalized.thrust, 0) 102 | XCTAssertEqual(normalized.pitch, 0) 103 | XCTAssertEqual(normalized.roll, 0) 104 | XCTAssertEqual(normalized.yaw, 0) 105 | } 106 | 107 | func testInputStateMaxByteValues() { 108 | 109 | let inputState = createInputStateMax() 110 | 111 | var thrust = Byte() 112 | var pitch = Byte() 113 | var roll = Byte() 114 | var yaw = Byte() 115 | inputState.getFinalValues(roll: &roll, pitch: &pitch, thrust: &thrust, yaw: &yaw) 116 | 117 | XCTAssertEqual(thrust, Byte.max) 118 | XCTAssertEqual(pitch, Byte.max) 119 | XCTAssertEqual(roll, Byte.max) 120 | XCTAssertEqual(yaw, Byte.max) 121 | } 122 | 123 | func testInputStateMinByteValues() { 124 | 125 | let inputState = createInputStateMin() 126 | 127 | var thrust = Byte() 128 | var pitch = Byte() 129 | var roll = Byte() 130 | var yaw = Byte() 131 | inputState.getFinalValues(roll: &roll, pitch: &pitch, thrust: &thrust, yaw: &yaw) 132 | 133 | XCTAssertEqual(thrust, Byte.min) 134 | XCTAssertEqual(pitch, Byte.min) 135 | XCTAssertEqual(roll, Byte.min) 136 | XCTAssertEqual(yaw, Byte.min) 137 | } 138 | } 139 | --------------------------------------------------------------------------------