├── .github ├── FUNDING.yml └── workflows │ └── mobsf.yml ├── .gitignore ├── LICENSE.md ├── Podfile ├── Pods ├── Local Podspecs │ └── SwiftyZeroMQ5.podspec.json ├── Manifest.lock ├── Pods.xcodeproj │ └── project.pbxproj ├── SwiftyZeroMQ5 │ ├── LICENSE │ ├── Libraries │ │ └── libzmq-ios.a │ ├── README.md │ └── Sources │ │ ├── Context.swift │ │ ├── Poller.swift │ │ ├── Socket.swift │ │ ├── SocketEvents.swift │ │ ├── SocketSendRecvOption.swift │ │ ├── SocketType.swift │ │ ├── SwiftyZeroMQ.h │ │ ├── SwiftyZeroMQ.swift │ │ ├── ZeroMQError.swift │ │ └── zmq.h └── Target Support Files │ ├── Pods-WarDragon │ ├── Pods-WarDragon-Info.plist │ ├── Pods-WarDragon-acknowledgements.markdown │ ├── Pods-WarDragon-acknowledgements.plist │ ├── Pods-WarDragon-dummy.m │ ├── Pods-WarDragon-frameworks-Debug-input-files.xcfilelist │ ├── Pods-WarDragon-frameworks-Debug-output-files.xcfilelist │ ├── Pods-WarDragon-frameworks-Release-input-files.xcfilelist │ ├── Pods-WarDragon-frameworks-Release-output-files.xcfilelist │ ├── Pods-WarDragon-frameworks.sh │ ├── Pods-WarDragon-umbrella.h │ ├── Pods-WarDragon.debug.xcconfig │ ├── Pods-WarDragon.modulemap │ └── Pods-WarDragon.release.xcconfig │ └── SwiftyZeroMQ5 │ ├── SwiftyZeroMQ5-Info.plist │ ├── SwiftyZeroMQ5-dummy.m │ ├── SwiftyZeroMQ5-prefix.pch │ ├── SwiftyZeroMQ5-umbrella.h │ ├── SwiftyZeroMQ5.debug.xcconfig │ ├── SwiftyZeroMQ5.modulemap │ └── SwiftyZeroMQ5.release.xcconfig ├── README.md ├── Scripts ├── ANT_Spectrum.py ├── multitest.py └── test.py ├── WarDragon.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── WarDragon.xcscheme ├── WarDragon.xcworkspace └── contents.xcworkspacedata └── WarDragon ├── AppData ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 1.png │ │ ├── FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 2.png │ │ ├── FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 3.png │ │ ├── FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 4.png │ │ ├── FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 5.png │ │ ├── FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 6.png │ │ ├── FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 7.png │ │ ├── FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45.png │ │ ├── IMG_2697 1.png │ │ ├── dragonLogo2 1.jpg │ │ └── dragonLogo2.jpg │ ├── Contents.json │ ├── WickedDragon.imageset │ │ ├── Contents.json │ │ ├── IMG_2697 1.png │ │ └── IMG_2697 2x.png │ └── dragon2.imageset │ │ ├── Contents.json │ │ ├── dragon1x.png │ │ ├── dragon2xpng.png │ │ └── dragon3x.png ├── LaunchScreen.storyboard └── WarDragonApp.swift ├── Data Handling ├── Message Parsing │ ├── XMLParserDelegate.swift │ └── ZMQHandler.swift ├── RID │ ├── CoTViewModel.swift │ ├── DroneSignature.swift │ └── DroneSignatureGenerator.swift ├── ServiceManager │ ├── ServiceControl.swift │ └── ServiceViewModel.swift ├── SpectrumData.swift └── Storage │ ├── BackgroundManager.swift │ ├── DroneInfoEditor.swift │ └── DroneStorage.swift ├── FAA Lookup ├── FAAInfoView.swift └── FAAService.swift ├── Info.plist ├── Maps ├── LiveMapView.swift └── MapView.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json ├── Settings ├── Settings.swift └── SettingsView.swift ├── Status ├── StatusMessageView.swift └── StatusViewModel.swift ├── UI ├── ContentView.swift ├── DashboardView.swift ├── DroneDetailView.swift ├── FAALookupButton.swift ├── HistoryView.swift ├── MessageRow.swift ├── ServiceManagementView.swift ├── SpectrumView.swift ├── StatusListView.swift └── TacDial.swift └── WarDragon.entitlements /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | 4 | #ko_fi: lukeswitz 5 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 6 | #patreon: # Replace with a single Patreon username 7 | #open_collective: # Replace with a single Open Collective username 8 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 9 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 10 | #liberapay: # Replace with a single Liberapay username 11 | #issuehunt: # Replace with a single IssueHunt username 12 | #lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | #polar: # Replace with a single Polar username 14 | buy_me_a_coffee: lukeswitz 15 | #thanks_dev: # Replace with a single thanks.dev username 16 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 17 | -------------------------------------------------------------------------------- /.github/workflows/mobsf.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: MobSF 7 | 8 | on: 9 | push: 10 | branches: [ "main" ] 11 | pull_request: 12 | branches: [ "main" ] 13 | schedule: 14 | - cron: '37 17 * * 0' 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | mobile-security: 21 | permissions: 22 | contents: read # for actions/checkout to fetch code 23 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 24 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Setup python 31 | uses: actions/setup-python@v3 32 | with: 33 | python-version: 3.8 34 | 35 | - name: Run mobsfscan 36 | uses: MobSF/mobsfscan@a60d10a83af68e23e0b30611c6515da604f06f65 37 | with: 38 | args: . --sarif --output results.sarif || true 39 | 40 | - name: Upload mobsfscan report 41 | uses: github/codeql-action/upload-sarif@v3 42 | with: 43 | sarif_file: results.sarif 44 | -------------------------------------------------------------------------------- /.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 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | # CocoaPods 32 | # 33 | # We recommend against adding the Pods directory to your .gitignore. However 34 | # you should judge for yourself, the pros and cons are mentioned at: 35 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 36 | # 37 | # Pods/ 38 | # 39 | # Add this line if you want to avoid checking in source code from the Xcode workspace 40 | # *.xcworkspace 41 | 42 | # Carthage 43 | # 44 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 45 | # Carthage/Checkouts 46 | 47 | Carthage/Build 48 | 49 | # fastlane 50 | # 51 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 52 | # screenshots whenever they are needed. 53 | # For more information about the recommended setup visit: 54 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 55 | 56 | fastlane/report.xml 57 | fastlane/Preview.html 58 | fastlane/screenshots/**/*.png 59 | fastlane/test_output 60 | 61 | # Code Injection 62 | # 63 | # After new code Injection tools there's a generated folder /iOSInjectionProject 64 | # https://github.com/johnno1962/injectionforxcode 65 | 66 | iOSInjectionProject/ 67 | 68 | .DS_Store 69 | 70 | Podfile.lock 71 | 72 | multicastTest.py 73 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2024 Luke Switzer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '12.0' 3 | 4 | target 'WarDragon' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for WarDragon 9 | pod 'SwiftyZeroMQ5', :git => 'https://github.com/lukeswitz/SwiftyZeroMQ5.git' 10 | end 11 | -------------------------------------------------------------------------------- /Pods/Local Podspecs/SwiftyZeroMQ5.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SwiftyZeroMQ5", 3 | "version": "1.3.0", 4 | "summary": "ZeroMQ Swift 5 Bindings for iOS, macOS, tvOS and watchOS.", 5 | "description": "This library provides easy-to-use iOS, macOS, tvOS and watchOS Swift\nbindings for the ZeroMQ C++ library. It is written in Swift 3 and features a\nbundled stable libzmq library. It provides ZeroMQ's low-level API along with\nan object-oriented API.", 6 | "homepage": "https://github.com/olehs/SwiftyZeroMQ5", 7 | "license": "MIT", 8 | "authors": { 9 | "Ahmad M. Zawawi": "ahmad.zawawi@gmail.com" 10 | }, 11 | "source": { 12 | "git": "https://github.com/lukeswitz/SwiftyZeroMQ5.git", 13 | "tag": "1.3.0" 14 | }, 15 | "platforms": { 16 | "ios": "12.0", 17 | "osx": "10.11", 18 | "tvos": "12.0", 19 | "watchos": "2.0" 20 | }, 21 | "swift_versions": "5", 22 | "libraries": "c++", 23 | "source_files": "Sources/*.{h,swift}", 24 | "ios": { 25 | "vendored_libraries": "Libraries/libzmq-ios.a" 26 | }, 27 | "osx": { 28 | "vendored_libraries": "Libraries/libzmq-macos.a" 29 | }, 30 | "tvos": { 31 | "vendored_libraries": "Libraries/libzmq-tvos.a" 32 | }, 33 | "watchos": { 34 | "vendored_libraries": "Libraries/libzmq-watchos.a" 35 | }, 36 | "preserve_paths": "Sources/*.{a,h}", 37 | "swift_version": "5" 38 | } 39 | -------------------------------------------------------------------------------- /Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SwiftyZeroMQ5 (1.3.0) 3 | 4 | DEPENDENCIES: 5 | - SwiftyZeroMQ5 (from `https://github.com/lukeswitz/SwiftyZeroMQ5.git`) 6 | 7 | EXTERNAL SOURCES: 8 | SwiftyZeroMQ5: 9 | :git: https://github.com/lukeswitz/SwiftyZeroMQ5.git 10 | 11 | CHECKOUT OPTIONS: 12 | SwiftyZeroMQ5: 13 | :commit: 96d5a1dd1509953610aae38b17584a252bcbd0b8 14 | :git: https://github.com/lukeswitz/SwiftyZeroMQ5.git 15 | 16 | SPEC CHECKSUMS: 17 | SwiftyZeroMQ5: c90dc55fb029cda964523843791659a2ead798ed 18 | 19 | PODFILE CHECKSUM: a3b1bdd6123db7a6fd77986ffb730b7328a323e4 20 | 21 | COCOAPODS: 1.16.2 22 | -------------------------------------------------------------------------------- /Pods/SwiftyZeroMQ5/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017 Ahmad M. Zawawi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pods/SwiftyZeroMQ5/Libraries/libzmq-ios.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/Pods/SwiftyZeroMQ5/Libraries/libzmq-ios.a -------------------------------------------------------------------------------- /Pods/SwiftyZeroMQ5/README.md: -------------------------------------------------------------------------------- 1 | # SwiftyZeroMQ5 - ZeroMQ Swift 5 Bindings for iOS, macOS, tvOS and watchOS 2 | 3 | [![CI Status][travis-badge]][travis-url] 4 | [![Swift][swift-badge]][swift-url] 5 | [![ZeroMQ][zeromq-badge]][zeromq-url] 6 | [![Platform][platform-badge]][platform-url] 7 | 10 | [![License][mit-badge]][mit-url] 11 | 12 | This library provides easy-to-use iOS, macOS, tvOS and watchOS 13 | [Swift](http://swift.org) bindings for the [ZeroMQ](http://zeromq.org) C++ 14 | library. It is written in Swift 5 and features a bundled stable 15 | [`libzmq`](https://github.com/zeromq/libzmq) library. It provides ZeroMQ's 16 | low-level API along with an object-oriented API. 17 | 18 | WARNING: THIS VERISON USES DRAFT APIS SUCH AS ZMQ_DISH AND ZMQ_RADIO 19 | Modified files include: 20 | 21 | * SocketTypes.swift, 22 | * zmq.h 23 | * Socket.swift 24 | 25 | ## What is ZeroMQ? 26 | 27 | > ZeroMQ (also spelled ØMQ, 0MQ or ZMQ) is a high-performance asynchronous 28 | > messaging library, aimed at use in distributed or concurrent applications. It 29 | > provides a message queue, but unlike message-oriented middleware, a ZeroMQ 30 | > system can run without a dedicated message broker. The library's API is 31 | > designed to resemble that of Berkeley sockets. 32 | 33 | 34 | ## Requirements 35 | 36 | - iOS 9+ / macOS 10.11+ / tvOS 9.0+ / watchOS 2.0+ 37 | - Xcode 11.2 and Swift 5 38 | - Bitcode-enabled Xcode project for non-MacOS 39 | 40 | ## Usage 41 | 42 | Please consult the [**Documentation Manual**](Documentation/Manual.md) for more 43 | information. Older examples can also be found in the 44 | [examples](https://github.com/azawawi/swift-zmq-examples) github repository. 45 | 46 | ### Version 47 | 48 | ```swift 49 | import SwiftyZeroMQ5 50 | 51 | // Print ZeroMQ library and our framework version 52 | let (major, minor, patch, versionString) = SwiftyZeroMQ.version 53 | print("ZeroMQ library version is \(major).\(minor) with patch level .\(patch)") 54 | print("ZeroMQ library version is \(versionString)") 55 | print("SwiftyZeroMQ version is \(SwiftyZeroMQ.frameworkVersion)") 56 | ``` 57 | 58 | ### Request-reply Pattern 59 | 60 | ```swift 61 | import SwiftyZeroMQ5 62 | 63 | do { 64 | // Define a TCP endpoint along with the text that we are going to send/recv 65 | let endpoint = "tcp://127.0.0.1:5555" 66 | let textToBeSent = "Hello world" 67 | 68 | // Request socket 69 | let context = try SwiftyZeroMQ.Context() 70 | let requestor = try context.socket(.request) 71 | try requestor.connect(endpoint) 72 | 73 | // Reply socket 74 | let replier = try context.socket(.reply) 75 | try replier.bind(endpoint) 76 | 77 | // Send it without waiting and check the reply on other socket 78 | try requestor.send(string: textToBeSent, options: .dontWait) 79 | let reply = try replier.recv() 80 | if reply == textToBeSent { 81 | print("Match") 82 | } else { 83 | print("Mismatch") 84 | } 85 | 86 | } catch { 87 | print(error) 88 | } 89 | ``` 90 | 91 | ### Publish-Subscribe Pattern 92 | 93 | ```swift 94 | private let endpoint = "tcp://127.0.0.1:5550" 95 | 96 | let context = try SwiftyZeroMQ.Context() 97 | let publisher = try context.socket(.publish) 98 | let subscriber1 = try context.socket(.subscribe) 99 | let subscriber2 = try context.socket(.subscribe) 100 | let subscriber3 = try context.socket(.subscribe) 101 | 102 | try publisher.bind(endpoint) 103 | let subscribers = [ 104 | subscriber1: "Subscriber #1", 105 | subscriber2: "Subscriber #2", 106 | subscriber3: "Subscriber #3", 107 | ] 108 | try subscriber1.connect(endpoint) 109 | try subscriber2.connect(endpoint) 110 | try subscriber3.connect(endpoint) 111 | 112 | // Brief wait to let everything hook up 113 | usleep(1000) 114 | 115 | // Subscriber #1 and #2 should receive anything 116 | try subscriber2.setSubscribe(nil) 117 | 118 | // Subscriber #3 should receive only messages starting with "topic" 119 | try subscriber3.setSubscribe("topic") 120 | 121 | // Brief wait to let everything hook up 122 | usleep(250) 123 | 124 | let poller = SwiftyZeroMQ.Poller() 125 | try poller.register(socket: subscriber1, flags: .pollIn) 126 | try poller.register(socket: subscriber2, flags: .pollIn) 127 | try poller.register(socket: subscriber3, flags: .pollIn) 128 | 129 | func pollAndRecv() throws { 130 | let socks = try poller.poll(timeout: 1000) 131 | for subscriber in socks.keys { 132 | let name = subscribers[subscriber] 133 | if socks[subscriber] == SwiftyZeroMQ.PollFlags.pollIn { 134 | let text = try subscriber.recv(options: .dontWait) 135 | print("\(name): received '\(text)'") 136 | } else { 137 | print("\(name): Nothing") 138 | } 139 | } 140 | print("---") 141 | } 142 | 143 | // Send a message - expect only sub2 to receive 144 | try publisher.send(string: "message") 145 | 146 | // Wait a bit to let the message come through 147 | usleep(100) 148 | 149 | try pollAndRecv(); 150 | 151 | // Send a message - sub2 and sub3 should receive 152 | try publisher.send(string: "topic: test") 153 | 154 | // Wait a bit to let the message come through 155 | usleep(100) 156 | 157 | try pollAndRecv(); 158 | ``` 159 | 160 | ### Poller 161 | 162 | ```swift 163 | import SwiftyZeroMQ5 164 | 165 | do { 166 | // Define a TCP endpoint along with the text that we are going to send/recv 167 | let endpoint = "tcp://127.0.0.1:5555" 168 | 169 | // Request socket 170 | let context = try SwiftyZeroMQ.Context() 171 | let requestor = try context.socket(.request) 172 | try requestor.connect(endpoint) 173 | 174 | // Reply socket 175 | let replier = try context.socket(.reply) 176 | try replier.bind(endpoint) 177 | 178 | // Create a Poller and add both requestor and replier 179 | let poller = SwiftyZeroMQ.Poller() 180 | try poller.register(socket: requestor, flags: [.pollIn, .pollOut]) 181 | try poller.register(socket: replier, flags: [.pollIn, .pollOut]) 182 | 183 | try requestor.send(string: "Hello replier!") 184 | 185 | // wait to let request come through 186 | sleep(1) 187 | 188 | var updates = try poller.poll() 189 | if updates[replier] == SwiftyZeroMQ.PollFlags.pollIn { 190 | print("Replier has data to be received.") 191 | } 192 | else { 193 | print("Expected replier to be in pollIn state.") 194 | return 195 | } 196 | 197 | try _ = replier.recv() 198 | 199 | updates = try poller.poll() 200 | if updates[replier] == SwiftyZeroMQ.PollFlags.none { 201 | print("All data has been received") 202 | } 203 | else { 204 | print("Expected replier to be in none state.") 205 | return 206 | } 207 | } catch { 208 | print(error) 209 | } 210 | ``` 211 | 212 | ## Planned Features (aka TODO) 213 | 214 | - [ ] More official ZeroMQ examples written 215 | - [ ] More ZeroMQ API wrapped 216 | 217 | ## See Also 218 | 219 | - For Linux and macOS support with SwiftPM, please see [Zewo's ZeroMQ Swift bindings](https://github.com/ZewoGraveyard/ZeroMQ). 220 | 221 | ## Author & License 222 | 223 | Copyright (c) 2016-2017 [Ahmad M. Zawawi](https://github.com/azawawi) under the 224 | [MIT license](LICENSE). 225 | 226 | A prebuilt iOS, macOS, tvOS and watchOS universal 227 | [`libzmq`](https://github.com/zeromq/libzmq) library is bundled with this 228 | library under the [LGPL](https://github.com/zeromq/libzmq#license) license. 229 | 230 | [travis-badge]: https://travis-ci.com/Pooppap/SwiftyZeroMQ.svg?branch=Swift5 231 | [travis-url]: https://travis-ci.com/Pooppap/SwiftyZeroMQ 232 | 233 | [swift-badge]: https://img.shields.io/badge/Swift-5-orange.svg?style=flat 234 | [swift-url]: https://swift.org 235 | 236 | [zeromq-badge]: https://img.shields.io/badge/ZeroMQ-4.2.1-blue.svg?style=flat 237 | [zeromq-url]: https://zeromq.org 238 | 239 | [platform-badge]: https://img.shields.io/badge/Platforms-iOS%20|%20macOS%20|%20tvOS%20|%20watchOS-blue.svg?style=flat 240 | [platform-url]: http://cocoadocs.org/docsets/SwiftyZeroMQ 241 | 242 | 248 | 249 | [mit-badge]: https://img.shields.io/badge/License-MIT-blue.svg?style=flat 250 | [mit-url]: https://tldrlegal.com/license/mit-license 251 | -------------------------------------------------------------------------------- /Pods/SwiftyZeroMQ5/Sources/Context.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016-2017 Ahmad M. Zawawi (azawawi) 3 | // 4 | // This package is distributed under the terms of the MIT license. 5 | // Please see the accompanying LICENSE file for the full text of the license. 6 | // 7 | 8 | extension SwiftyZeroMQ { 9 | 10 | /** 11 | This represents a ZeroMQ context 12 | */ 13 | public class Context: Hashable { 14 | /** 15 | This is the low-level context pointer handle. Please be extra 16 | careful while using this one otherwise crashes and memory leaks may 17 | occur. 18 | */ 19 | public var handle : UnsafeMutableRawPointer? 20 | 21 | /** 22 | This is used internally to manage context handle cleanup in 23 | deinitialization 24 | */ 25 | private var cleanupNeeded : Bool 26 | 27 | /** 28 | Create a new ZeroMQ context 29 | 30 | - throws: ZeroMQError 31 | */ 32 | public init() throws { 33 | let contextHandle = zmq_ctx_new() 34 | if contextHandle == nil { 35 | throw ZeroMQError.last 36 | } 37 | 38 | handle = contextHandle 39 | cleanupNeeded = true 40 | } 41 | 42 | /** 43 | Called automatically by garbage collector to terminate context 44 | */ 45 | deinit { 46 | guard cleanupNeeded else { 47 | // No need to cleanup, user has already done that 48 | return 49 | } 50 | 51 | do { 52 | try terminate() 53 | } catch { 54 | print(error) 55 | } 56 | } 57 | 58 | /** 59 | Shutdown the current context without terminating the current context 60 | */ 61 | public func shutdown() throws { 62 | guard handle != nil else { 63 | return 64 | } 65 | 66 | let result = zmq_ctx_shutdown(handle) 67 | if result == -1 { 68 | throw ZeroMQError.last 69 | } else { 70 | handle = nil 71 | cleanupNeeded = false 72 | } 73 | } 74 | 75 | /** 76 | Terminate the current context and block until all open sockets 77 | are closed or their linger period has expired 78 | */ 79 | public func terminate() throws { 80 | guard handle != nil else { 81 | // No need to terminate 82 | return 83 | } 84 | 85 | let result = zmq_ctx_term(handle) 86 | if result == -1 { 87 | throw ZeroMQError.last 88 | } else { 89 | handle = nil 90 | } 91 | } 92 | 93 | /** 94 | An alias for `terminate` 95 | */ 96 | public func close() throws { 97 | try terminate() 98 | } 99 | 100 | /** 101 | Returns a ZMQ socket with the type provided 102 | - parameters: 103 | - type: socket type of type SocketType 104 | - returns: a ZeroMQ socket with the type provided 105 | */ 106 | public func socket(_ type : SwiftyZeroMQ.SocketType) throws -> Socket { 107 | return try Socket(context: self, type: type) 108 | } 109 | 110 | /** 111 | Returns the current context option value (private) 112 | 113 | - parameters: 114 | - name: option name 115 | - returns: the option value 116 | */ 117 | private func getOption(_ name : Int32) throws -> Int32 { 118 | let result = zmq_ctx_get(handle, name) 119 | if result == -1 { 120 | throw ZeroMQError.last 121 | } 122 | 123 | return result 124 | } 125 | 126 | /** 127 | Sets the current context option value (private) 128 | 129 | - parameters: 130 | - name: the option name 131 | - value: the option value to be set 132 | */ 133 | private func setOption(_ name: Int32, _ value: Int32) throws { 134 | let result = zmq_ctx_set(handle, name, value) 135 | if result == -1 { 136 | throw ZeroMQError.last 137 | } 138 | } 139 | 140 | /** 141 | Returns whether the `terminate` method call will block forever or 142 | not. Default option value is true. 143 | 144 | By default the context will block, forever, on a `.terminate` call. 145 | The assumption behind this behavior is that abrupt termination will 146 | cause message loss. Most real applications use some form of 147 | handshaking to ensure applications receive termination messages, and 148 | then terminate the context with `socket.setLinger(0)` on all 149 | sockets. This setting is an easier way to get the same result. When 150 | it is set to false, all new sockets are given a linger timeout of 151 | zero. **You must still close all sockets before calling terminate.** 152 | */ 153 | public func isBlocky() throws -> Bool { 154 | return try getOption(ZMQ_BLOCKY) == 1 155 | } 156 | 157 | /** 158 | Sets whether the `terminate` method call will block forever or not. 159 | Default option value is true. 160 | 161 | By default the context will block, forever, on a `.terminate` call. 162 | The assumption behind this behavior is that abrupt termination will 163 | cause message loss. Most real applications use some form of 164 | handshaking to ensure applications receive termination messages, and 165 | then terminate the context with `socket.setLinger(0)` on all 166 | sockets. This setting is an easier way to get the same result. When 167 | it is set to false, all new sockets are given a linger timeout of 168 | zero. **You must still close all sockets before calling terminate.** 169 | */ 170 | public func setBlocky(_ enabled : Bool = true) throws { 171 | try setOption(ZMQ_BLOCKY, enabled ? 1 : 0) 172 | } 173 | 174 | /** 175 | Returns the number of I/O threads for the current context 176 | 177 | Default value is 1 (read and write) 178 | 179 | returns: The number of I/O threads for the current context 180 | */ 181 | public func getIOThreads() throws -> Int { 182 | return try Int(getOption(ZMQ_IO_THREADS)) 183 | } 184 | 185 | /** 186 | Sets the number of I/O threads for the current context 187 | 188 | Default value is 1 (read and write) 189 | */ 190 | public func setIOThreads(_ value : Int = 1) throws { 191 | try setOption(ZMQ_IO_THREADS, Int32(value)) 192 | } 193 | 194 | /** 195 | Sets the scheduling policy for I/O threads for the current context 196 | 197 | Default value is -1 (write only) 198 | */ 199 | public func setThreadSchedulingPolicy(_ value : Int = -1) throws { 200 | try setOption(ZMQ_THREAD_SCHED_POLICY, Int32(value)) 201 | } 202 | 203 | /** 204 | Sets the scheduling priority for I/O threads for the current context 205 | 206 | Default value is -1 (write only) 207 | */ 208 | public func setThreadPriority(_ value : Int = -1) throws { 209 | try setOption(ZMQ_THREAD_PRIORITY, Int32(value)) 210 | } 211 | 212 | /** 213 | Returns the maximum allowed size of a message sent in the current 214 | context. Default value is Int32.max (i.e. 2147483647). 215 | 216 | Default value is Int32.max (i.e. 2147483647) 217 | */ 218 | public func getMaxMessageSize() throws -> Int { 219 | return try Int(getOption(ZMQ_MAX_MSGSZ)) 220 | } 221 | 222 | /** 223 | Sets the maximum allowed size of a message sent in the current 224 | context. Default value is Int32.max (i.e. 2147483647). 225 | */ 226 | public func setMaxMessageSize(_ size : Int = Int(Int32.max)) throws { 227 | try setOption(ZMQ_MAX_MSGSZ, Int32(size)) 228 | } 229 | 230 | /** 231 | Returns the maximum number of sockets associated with the current 232 | context 233 | 234 | Default value is 1024 (read/write) 235 | */ 236 | public func getMaxSockets() throws -> Int { 237 | return try Int(getOption(ZMQ_MAX_SOCKETS)) 238 | } 239 | 240 | /** 241 | Sets the maximum number of sockets associated with the current 242 | context 243 | 244 | Default value is 1024 (read/write) 245 | */ 246 | public func setMaxSockets(_ value : Int = 1024) throws { 247 | try setOption(ZMQ_MAX_SOCKETS, Int32(value)) 248 | } 249 | 250 | /** 251 | Returns whether the IPV6 is enabled or not for the current context 252 | 253 | Default value is false (read/write) 254 | */ 255 | public func isIPV6Enabled() throws -> Bool { 256 | return try getOption(ZMQ_IPV6) == 1 257 | } 258 | 259 | /** 260 | Sets whether the IPV6 is enabled or not for the current context 261 | 262 | Default value is false (read/write) 263 | */ 264 | public func setIPV6Enabled(_ enabled : Bool = false) throws { 265 | try setOption(ZMQ_IPV6, enabled ? 1 : 0) 266 | } 267 | 268 | /** 269 | The maximum socket limit associated with the current context 270 | 271 | Default value: (read only) 272 | */ 273 | public func getSocketLimit() throws -> Int { 274 | return try Int(getOption(ZMQ_SOCKET_LIMIT)) 275 | } 276 | 277 | // To be removed 278 | // /** 279 | // Hashable implementation 280 | // */ 281 | // public var hashValue: Int { 282 | // if let hashValue = handle?.hashValue { 283 | // return hashValue 284 | // } 285 | // else { 286 | // return 0 // todo: not clear what this corresponds to... 287 | // } 288 | // } 289 | 290 | public func hash(into hasher: inout Hasher) { 291 | hasher.combine(handle?.hashValue) 292 | } 293 | 294 | /** 295 | Equatable implementation (inherited from Hashable) 296 | */ 297 | public static func ==(lhs: Context, rhs: Context) -> Bool { 298 | return lhs.handle == rhs.handle 299 | } 300 | 301 | } 302 | 303 | } 304 | -------------------------------------------------------------------------------- /Pods/SwiftyZeroMQ5/Sources/Poller.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016-2017 Ahmad M. Zawawi (azawawi) 3 | // 4 | // This package is distributed under the terms of the MIT license. 5 | // Please see the accompanying LICENSE file for the full text of the license. 6 | // 7 | 8 | extension SwiftyZeroMQ { 9 | 10 | /** 11 | This represents the poll flags and is used in `register` and `modify` 12 | */ 13 | public struct PollFlags : OptionSet { 14 | public var rawValue: Int32 15 | 16 | public init(rawValue: Int32) { 17 | self.rawValue = rawValue 18 | } 19 | 20 | // Poll flags 21 | public static let pollIn = PollFlags(rawValue: ZMQ_POLLIN) 22 | public static let pollOut = PollFlags(rawValue: ZMQ_POLLOUT) 23 | public static let pollErr = PollFlags(rawValue: ZMQ_POLLERR) 24 | public static let `none`: PollFlags = [] 25 | } 26 | 27 | /** 28 | This represents a ZeroMQ poller implementation which provides socket 29 | I/O multiplexing 30 | */ 31 | public class Poller { 32 | private var sockets : [(Socket, PollFlags)] 33 | private var socketMap : [Socket:Int] 34 | 35 | /** 36 | Creates a new Poller object 37 | */ 38 | public init() { 39 | sockets = [(Socket, PollFlags)]() 40 | socketMap = [Socket:Int]() 41 | } 42 | 43 | /** 44 | Register a socket for the given flags 45 | If no flags are supplied, it will unregister the socket 46 | */ 47 | public func register( 48 | socket: Socket, 49 | flags: PollFlags = [.pollIn, .pollOut]) 50 | throws 51 | { 52 | if flags != PollFlags.none { 53 | // if there are any flags, update or add the socket as needed 54 | if let socketIndex = socketMap[socket] { 55 | sockets[socketIndex] = (socket, flags) 56 | } else { 57 | let socketIndex = sockets.count 58 | sockets.append((socket, flags)) 59 | socketMap[socket] = socketIndex 60 | } 61 | } else if (socketMap[socket] != nil) { 62 | // if no flags were supplied but currently registered then 63 | // unregister socket 64 | try unregister(socket: socket) 65 | } 66 | 67 | // if socket is not currently registered and no flags were supplied 68 | // then do nothing 69 | } 70 | 71 | /** 72 | Modify a socket registration. This is equivalent to calling 73 | `register(socket, flags)` 74 | */ 75 | public func modify( 76 | socket : Socket, 77 | flags : PollFlags = [.pollIn, .pollOut]) throws 78 | { 79 | try register(socket: socket, flags: flags) 80 | } 81 | 82 | /** 83 | Unregister the supplied socket 84 | */ 85 | public func unregister(socket: Socket) throws { 86 | let socketIndex = socketMap[socket]! 87 | sockets.remove(at: socketIndex) 88 | 89 | // Update indices of all other sockets in the socket map 90 | for (socket, _) in sockets.suffix(from: socketIndex) { 91 | let socketIndex = socketMap[socket]! 92 | socketMap[socket] = socketIndex - 1 93 | } 94 | } 95 | 96 | /** 97 | Poll the registered socket(s) for events 98 | */ 99 | public func poll(timeout: TimeInterval? = nil) throws 100 | -> [Socket: PollFlags] 101 | { 102 | // Now start polling 103 | let pollItems = buildPollItems() 104 | 105 | defer { 106 | // Clean up poll items on scope exit 107 | pollItems.deallocate() 108 | } 109 | 110 | let intTimeout = (timeout == nil) 111 | ? -1 112 | : Int(timeout!) 113 | let code = zmq_poll(pollItems, Int32(sockets.count), 114 | intTimeout) 115 | 116 | if code < 0 { 117 | // if code is negative, cleanup poll items before 118 | // throwing an error 119 | throw ZeroMQError.last 120 | } 121 | 122 | // Build hash map [Socket: PollFlags] 123 | let map = buildSocketPollFlagsMap(pollItems : pollItems) 124 | 125 | return map 126 | } 127 | 128 | /** 129 | Build socket to poll flags map from provided sockets and poll items 130 | */ 131 | private func buildSocketPollFlagsMap( 132 | pollItems : UnsafeMutablePointer 133 | ) -> [Socket: PollFlags] 134 | { 135 | // Assumption: sockets and poll items are in the same order 136 | var map = [Socket: PollFlags]() 137 | for (socketIndex, (socket, _)) in sockets.enumerated() { 138 | let pollItem = pollItems[socketIndex] 139 | let receivedFlags = PollFlags(rawValue: Int32(pollItem.revents)) 140 | map[socket] = receivedFlags 141 | } 142 | return map 143 | } 144 | 145 | /** 146 | Build poll items from a supplied list of socket flags 147 | */ 148 | private func buildPollItems() -> UnsafeMutablePointer 149 | { 150 | let pollItems = UnsafeMutablePointer.allocate( 151 | capacity: sockets.count 152 | ) 153 | for (socketIndex, (socket, flags)) in sockets.enumerated() { 154 | var pollItem = zmq_pollitem_t() 155 | pollItem.socket = socket.handle 156 | pollItem.events = Int16(flags.rawValue) 157 | pollItems[socketIndex] = pollItem 158 | } 159 | return pollItems 160 | } 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /Pods/SwiftyZeroMQ5/Sources/SocketEvents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016-2017 Ahmad M. Zawawi (azawawi) 3 | // 4 | // This package is distributed under the terms of the MIT license. 5 | // Please see the accompanying LICENSE file for the full text of the license. 6 | // 7 | 8 | extension SwiftyZeroMQ { 9 | 10 | /** 11 | An set of socket events that map out to a 32-bit integer 12 | */ 13 | public struct SocketEvents : OptionSet { 14 | public let rawValue: Int32 15 | 16 | public init(rawValue: Int32) { 17 | self.rawValue = rawValue 18 | } 19 | 20 | public static let connected = SocketEvents(rawValue: ZMQ_EVENT_CONNECTED) 21 | public static let connectDelayed = SocketEvents(rawValue: ZMQ_EVENT_CONNECT_DELAYED) 22 | public static let connectRetried = SocketEvents(rawValue: ZMQ_EVENT_CONNECT_RETRIED) 23 | public static let listening = SocketEvents(rawValue: ZMQ_EVENT_LISTENING) 24 | public static let bindFailed = SocketEvents(rawValue: ZMQ_EVENT_BIND_FAILED) 25 | public static let accepted = SocketEvents(rawValue: ZMQ_EVENT_ACCEPTED) 26 | public static let acceptFailed = SocketEvents(rawValue: ZMQ_EVENT_ACCEPT_FAILED) 27 | public static let closed = SocketEvents(rawValue: ZMQ_EVENT_CLOSED) 28 | public static let closeFailed = SocketEvents(rawValue: ZMQ_EVENT_CLOSE_FAILED) 29 | public static let disconnected = SocketEvents(rawValue: ZMQ_EVENT_DISCONNECTED) 30 | public static let monitorStopped = SocketEvents(rawValue: ZMQ_EVENT_MONITOR_STOPPED) 31 | public static let all = SocketEvents(rawValue: ZMQ_EVENT_ALL) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Pods/SwiftyZeroMQ5/Sources/SocketSendRecvOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016-2017 Ahmad M. Zawawi (azawawi) 3 | // 4 | // This package is distributed under the terms of the MIT license. 5 | // Please see the accompanying LICENSE file for the full text of the license. 6 | // 7 | 8 | extension SwiftyZeroMQ { 9 | 10 | /** 11 | An enumeration of socket send and receive options that map out to a 12 | 32-bit integer 13 | */ 14 | public enum SocketSendRecvOption : Int32 { 15 | case none 16 | case dontWait 17 | case sendMore 18 | case dontWaitSendMore 19 | 20 | /** 21 | This is a workaround to return dynamically loaded ZMQ_ constants 22 | */ 23 | public var rawValue: Int32 { 24 | switch self { 25 | case .none: return 0 26 | case .dontWait: return ZMQ_DONTWAIT 27 | case .sendMore: return ZMQ_SNDMORE 28 | case .dontWaitSendMore: return ZMQ_DONTWAIT | ZMQ_SNDMORE 29 | } 30 | } 31 | 32 | /** 33 | Returns whether the current option is a valid receive option or not 34 | */ 35 | public func isValidRecvOption() -> Bool { 36 | return self == .none || self == .dontWait 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Pods/SwiftyZeroMQ5/Sources/SwiftyZeroMQ.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016-2017 Ahmad M. Zawawi (azawawi) 3 | // 4 | // This package is distributed under the terms of the MIT license. 5 | // Please see the accompanying LICENSE file for the full text of the license. 6 | // 7 | 8 | // Provides target conditional macros 9 | #import "TargetConditionals.h" 10 | 11 | // Import appropriate library depending on target being iOS/tvOS/watchOS or 12 | // macOS 13 | #if TARGET_OS_IPHONE 14 | #import 15 | #else 16 | #import 17 | #endif 18 | 19 | // Import libzmq functions and constants into Swift 20 | #import "zmq.h" 21 | 22 | //! Project version number for SwiftySwiftyZeroMQ. 23 | FOUNDATION_EXPORT double SwiftyZeroMQVersionNumber; 24 | 25 | //! Project version string for SwiftySwiftyZeroMQ. 26 | FOUNDATION_EXPORT const unsigned char SwiftyZeroMQVersionString[]; 27 | -------------------------------------------------------------------------------- /Pods/SwiftyZeroMQ5/Sources/SwiftyZeroMQ.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016-2017 Ahmad M. Zawawi (azawawi) 3 | // 4 | // This package is distributed under the terms of the MIT license. 5 | // Please see the accompanying LICENSE file for the full text of the license. 6 | // 7 | 8 | 9 | /** 10 | Utility functions are provided here such as version, capability and proxy 11 | */ 12 | public struct SwiftyZeroMQ { 13 | 14 | /** 15 | Private constructor to prevent instansiation 16 | */ 17 | private init() { 18 | // Do nothing 19 | } 20 | 21 | /** 22 | Represents a capability or feature that ZeroMQ supports. 23 | 24 | * ipc - the library supports the `ipc://` protocol 25 | * pgm - the library supports the `pgm://` protocol 26 | * tipc - the library supports the `tipc://` protocol 27 | * norm - the library supports the `norm://` protocol 28 | * curve - the library supports the `CURVE` security mechanism 29 | * gssapi - the library supports the `GSSAPI` security mechanism 30 | */ 31 | public enum Capability : String { 32 | case ipc 33 | case pgm 34 | case tipc 35 | case norm 36 | case curve 37 | case gssapi 38 | } 39 | 40 | /** 41 | Returns the version information tuple as (.major, .minor, .patch, .versionString) 42 | */ 43 | public static var version : (major: Int, minor: Int, patch: Int, versionString: String) { 44 | var major: Int32 = 0 45 | var minor: Int32 = 0 46 | var patch: Int32 = 0 47 | zmq_version(&major, &minor, &patch) 48 | let versionString = "\(major).\(minor).\(patch)" 49 | 50 | return ( Int(major), Int(minor), Int(patch), versionString) 51 | } 52 | 53 | /** 54 | Returns the framework version as a string 55 | */ 56 | public static var frameworkVersion : String { 57 | return "1.2.1" 58 | } 59 | 60 | /** 61 | Returns whether the capability is enabled or not 62 | */ 63 | public static func has(_ capability : Capability) -> Bool { 64 | return zmq_has(capability.rawValue) == 1 65 | } 66 | 67 | /** 68 | The proxy connects a frontend socket to a backend socket. Conceptually, 69 | data flows from frontend to backend. Depending on the socket types, 70 | replies may flow in the opposite direction. The direction is conceptual 71 | only; the proxy is fully symmetric and there is no technical difference 72 | between frontend and backend. 73 | 74 | If the capture socket is not nil, the proxy shall send all messages, 75 | received on both frontend and backend, to the capture socket. The 76 | capture socket should be a .publish, .dealer, .push, or .pair typed 77 | socket. 78 | */ 79 | public static func proxy( 80 | frontend : SwiftyZeroMQ.Socket, 81 | backend : SwiftyZeroMQ.Socket, 82 | capture : SwiftyZeroMQ.Socket? = nil) throws 83 | { 84 | let result = zmq_proxy(frontend.handle, backend.handle, capture?.handle) 85 | if result == -1 { 86 | throw ZeroMQError.last 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Pods/SwiftyZeroMQ5/Sources/ZeroMQError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016-2017 Ahmad M. Zawawi (azawawi) 3 | // 4 | // This package is distributed under the terms of the MIT license. 5 | // Please see the accompanying LICENSE file for the full text of the license. 6 | // 7 | 8 | extension SwiftyZeroMQ { 9 | 10 | /** 11 | This provides a clean way to get the ZMQ library errors. This is usually 12 | thrown when a `-1` result is returned from a `libzmq` function call. 13 | */ 14 | public struct ZeroMQError : Error, CustomStringConvertible { 15 | /** 16 | The error description string 17 | */ 18 | public let description: String 19 | 20 | /** 21 | Returns the last ZMQ library error with a string error description 22 | */ 23 | public static var last : ZeroMQError { 24 | let errorCString = zmq_strerror(zmq_errno())! 25 | let description = String(validatingUTF8: errorCString)! 26 | return ZeroMQError(description: description) 27 | } 28 | 29 | /** 30 | Return an invalid option error 31 | */ 32 | public static var invalidOption : ZeroMQError { 33 | return ZeroMQError(description: "Invalid option") 34 | } 35 | 36 | /** 37 | Returns an unimplemented error 38 | */ 39 | public static var unimplemented : ZeroMQError { 40 | return ZeroMQError( 41 | description: "Unimplemented at the moment. PRs are welcome") 42 | } 43 | 44 | //TODO wrap EHOSTUNREACH 45 | //TODO wrap EAGAIN 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## SwiftyZeroMQ5 5 | 6 | MIT License 7 | 8 | Copyright (c) 2016-2017 Ahmad M. Zawawi 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | Generated by CocoaPods - https://cocoapods.org 29 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | MIT License 18 | 19 | Copyright (c) 2016-2017 Ahmad M. Zawawi 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | 39 | License 40 | MIT 41 | Title 42 | SwiftyZeroMQ5 43 | Type 44 | PSGroupSpecifier 45 | 46 | 47 | FooterText 48 | Generated by CocoaPods - https://cocoapods.org 49 | Title 50 | 51 | Type 52 | PSGroupSpecifier 53 | 54 | 55 | StringsTable 56 | Acknowledgements 57 | Title 58 | Acknowledgements 59 | 60 | 61 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_WarDragon : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_WarDragon 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon-frameworks-Debug-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-WarDragon/Pods-WarDragon-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/SwiftyZeroMQ5/SwiftyZeroMQ5.framework -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon-frameworks-Debug-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyZeroMQ5.framework -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon-frameworks-Release-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-WarDragon/Pods-WarDragon-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/SwiftyZeroMQ5/SwiftyZeroMQ5.framework -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon-frameworks-Release-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyZeroMQ5.framework -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | BCSYMBOLMAP_DIR="BCSymbolMaps" 23 | 24 | 25 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 26 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 27 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 28 | 29 | # Copies and strips a vendored framework 30 | install_framework() 31 | { 32 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 33 | local source="${BUILT_PRODUCTS_DIR}/$1" 34 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 35 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 36 | elif [ -r "$1" ]; then 37 | local source="$1" 38 | fi 39 | 40 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 41 | 42 | if [ -L "${source}" ]; then 43 | echo "Symlinked..." 44 | source="$(readlink -f "${source}")" 45 | fi 46 | 47 | if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then 48 | # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied 49 | find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do 50 | echo "Installing $f" 51 | install_bcsymbolmap "$f" "$destination" 52 | rm "$f" 53 | done 54 | rmdir "${source}/${BCSYMBOLMAP_DIR}" 55 | fi 56 | 57 | # Use filter instead of exclude so missing patterns don't throw errors. 58 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 59 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 60 | 61 | local basename 62 | basename="$(basename -s .framework "$1")" 63 | binary="${destination}/${basename}.framework/${basename}" 64 | 65 | if ! [ -r "$binary" ]; then 66 | binary="${destination}/${basename}" 67 | elif [ -L "${binary}" ]; then 68 | echo "Destination binary is symlinked..." 69 | dirname="$(dirname "${binary}")" 70 | binary="${dirname}/$(readlink "${binary}")" 71 | fi 72 | 73 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 74 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 75 | strip_invalid_archs "$binary" 76 | fi 77 | 78 | # Resign the code if required by the build settings to avoid unstable apps 79 | code_sign_if_enabled "${destination}/$(basename "$1")" 80 | 81 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 82 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 83 | local swift_runtime_libs 84 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 85 | for lib in $swift_runtime_libs; do 86 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 87 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 88 | code_sign_if_enabled "${destination}/${lib}" 89 | done 90 | fi 91 | } 92 | # Copies and strips a vendored dSYM 93 | install_dsym() { 94 | local source="$1" 95 | warn_missing_arch=${2:-true} 96 | if [ -r "$source" ]; then 97 | # Copy the dSYM into the targets temp dir. 98 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 99 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 100 | 101 | local basename 102 | basename="$(basename -s .dSYM "$source")" 103 | binary_name="$(ls "$source/Contents/Resources/DWARF")" 104 | binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" 105 | 106 | # Strip invalid architectures from the dSYM. 107 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then 108 | strip_invalid_archs "$binary" "$warn_missing_arch" 109 | fi 110 | if [[ $STRIP_BINARY_RETVAL == 0 ]]; then 111 | # Move the stripped file into its final destination. 112 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 113 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 114 | else 115 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 116 | mkdir -p "${DWARF_DSYM_FOLDER_PATH}" 117 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" 118 | fi 119 | fi 120 | } 121 | 122 | # Used as a return value for each invocation of `strip_invalid_archs` function. 123 | STRIP_BINARY_RETVAL=0 124 | 125 | # Strip invalid architectures 126 | strip_invalid_archs() { 127 | binary="$1" 128 | warn_missing_arch=${2:-true} 129 | # Get architectures for current target binary 130 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 131 | # Intersect them with the architectures we are building for 132 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 133 | # If there are no archs supported by this binary then warn the user 134 | if [[ -z "$intersected_archs" ]]; then 135 | if [[ "$warn_missing_arch" == "true" ]]; then 136 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 137 | fi 138 | STRIP_BINARY_RETVAL=1 139 | return 140 | fi 141 | stripped="" 142 | for arch in $binary_archs; do 143 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 144 | # Strip non-valid architectures in-place 145 | lipo -remove "$arch" -output "$binary" "$binary" 146 | stripped="$stripped $arch" 147 | fi 148 | done 149 | if [[ "$stripped" ]]; then 150 | echo "Stripped $binary of architectures:$stripped" 151 | fi 152 | STRIP_BINARY_RETVAL=0 153 | } 154 | 155 | # Copies the bcsymbolmap files of a vendored framework 156 | install_bcsymbolmap() { 157 | local bcsymbolmap_path="$1" 158 | local destination="${BUILT_PRODUCTS_DIR}" 159 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" 160 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" 161 | } 162 | 163 | # Signs a framework with the provided identity 164 | code_sign_if_enabled() { 165 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 166 | # Use the current code_sign_identity 167 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 168 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 169 | 170 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 171 | code_sign_cmd="$code_sign_cmd &" 172 | fi 173 | echo "$code_sign_cmd" 174 | eval "$code_sign_cmd" 175 | fi 176 | } 177 | 178 | if [[ "$CONFIGURATION" == "Debug" ]]; then 179 | install_framework "${BUILT_PRODUCTS_DIR}/SwiftyZeroMQ5/SwiftyZeroMQ5.framework" 180 | fi 181 | if [[ "$CONFIGURATION" == "Release" ]]; then 182 | install_framework "${BUILT_PRODUCTS_DIR}/SwiftyZeroMQ5/SwiftyZeroMQ5.framework" 183 | fi 184 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 185 | wait 186 | fi 187 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_WarDragonVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_WarDragonVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyZeroMQ5" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyZeroMQ5/SwiftyZeroMQ5.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/SwiftyZeroMQ5/Libraries" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -framework "SwiftyZeroMQ5" 9 | OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyZeroMQ5" 10 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 11 | PODS_BUILD_DIR = ${BUILD_DIR} 12 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 13 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 14 | PODS_ROOT = ${SRCROOT}/Pods 15 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 16 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_WarDragon { 2 | umbrella header "Pods-WarDragon-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-WarDragon/Pods-WarDragon.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyZeroMQ5" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyZeroMQ5/SwiftyZeroMQ5.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/SwiftyZeroMQ5/Libraries" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -framework "SwiftyZeroMQ5" 9 | OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyZeroMQ5" 10 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 11 | PODS_BUILD_DIR = ${BUILD_DIR} 12 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 13 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 14 | PODS_ROOT = ${SRCROOT}/Pods 15 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 16 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyZeroMQ5/SwiftyZeroMQ5-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.3.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyZeroMQ5/SwiftyZeroMQ5-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_SwiftyZeroMQ5 : NSObject 3 | @end 4 | @implementation PodsDummy_SwiftyZeroMQ5 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyZeroMQ5/SwiftyZeroMQ5-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyZeroMQ5/SwiftyZeroMQ5-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | #import "SwiftyZeroMQ.h" 14 | #import "zmq.h" 15 | 16 | FOUNDATION_EXPORT double SwiftyZeroMQ5VersionNumber; 17 | FOUNDATION_EXPORT const unsigned char SwiftyZeroMQ5VersionString[]; 18 | 19 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyZeroMQ5/SwiftyZeroMQ5.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftyZeroMQ5 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/SwiftyZeroMQ5/Libraries" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 5 | OTHER_LDFLAGS = $(inherited) -l"c++" -l"zmq-ios" 6 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 7 | PODS_BUILD_DIR = ${BUILD_DIR} 8 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 9 | PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} 10 | PODS_ROOT = ${SRCROOT} 11 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftyZeroMQ5 12 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 13 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 14 | SKIP_INSTALL = YES 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyZeroMQ5/SwiftyZeroMQ5.modulemap: -------------------------------------------------------------------------------- 1 | framework module SwiftyZeroMQ5 { 2 | umbrella header "SwiftyZeroMQ5-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftyZeroMQ5/SwiftyZeroMQ5.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftyZeroMQ5 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/SwiftyZeroMQ5/Libraries" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 5 | OTHER_LDFLAGS = $(inherited) -l"c++" -l"zmq-ios" 6 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 7 | PODS_BUILD_DIR = ${BUILD_DIR} 8 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 9 | PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} 10 | PODS_ROOT = ${SRCROOT} 11 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftyZeroMQ5 12 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 13 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 14 | SKIP_INSTALL = YES 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DragonSync iOS 2 | 3 | [![MobSF](https://github.com/Root-Down-Digital/DragonSync-iOS/actions/workflows/mobsf.yml/badge.svg)](https://github.com/Root-Down-Digital/DragonSync-iOS/actions/workflows/mobsf.yml) 4 | 5 |
6 | DragonSync Logo 7 |
8 |
9 |
10 | Real-time drone detection and monitoring for iOS/macOS, powered by locally-hosted decoding. Enjoy professional-grade detection with advanced signal analysis and tracking. 11 |
12 |
13 |
14 | 15 | [![TestFlight Beta](https://img.shields.io/badge/TestFlight-Join_Beta-blue.svg?style=for-the-badge&logo=apple)](https://testflight.apple.com/join/QKDKMSfA) 16 | 17 |
18 | 19 | **App** 20 | - [Features](#features) 21 | - [Detection & Tracking](#detection--tracking) 22 | - [History & Analysis](#history--analysis) 23 | - [App Settings Config](#app-settings) 24 | - [Build Instructions](#build-instructions) 25 | 26 | **Backend Data** 27 | - [Hardware Requirements](#hardware-requirements) 28 | - [Software Setup](#software-requirements) 29 | - [Connection Choices](#connection-choices) 30 | - [Command Reference](#backend-data-guide) 31 | 32 | **About** 33 | - [Credits, Disclaimer & License](#credits-disclaimer--license) 34 | - [Contributing & Contact](#contributing--contact) 35 | - [Notes](#notes) 36 | 37 | --- 38 | 39 | ## Features 40 | 41 | ### Real-Time Monitoring 42 | - Live tracking of Remote/Drone ID–compliant drones 43 | - Decodes Ocusync and others 44 | - Instant flight path visualization and telemetry 45 | - Multi-protocol (ZMQ & multicast) with tri-source detection 46 | 47 | ### Spoof Detection 48 | - Advanced analysis: signal strength, position consistency, transmission patterns, and flight physics 49 | 50 |
51 | Spoof Detection Screenshot 52 |
53 | 54 | ### Visualize Encrypted Drones 55 | - No GPS, no problem. Using the RSSI lets us estimate distance to target. 56 | 57 |
58 | Drone Encounter Screenshot 59 |
60 | 61 | ### MAC Randomization Detection 62 | - Real-time alerts for MAC changes with historical tracking and origin ID association 63 | 64 |
65 | MAC Randomization Detection Screenshot 66 |
67 | 68 | ### Multi-Source Signal Analysis 69 | - Identifies WiFi, BT, and SDR signals with source MAC tracking and signal strength monitoring 70 | 71 |
72 | Signal Analysis Interface 73 |
74 | 75 |
76 | Signal Analysis Interface 77 |
78 | 79 | ### System Monitoring 80 | - Real-time performance metrics: memory, CPU load, temperature, GPS & ANTSDR status 81 | 82 |
83 | System Monitoring Dashboard 84 |
85 | 86 | ## Detection & Tracking 87 | 88 | ### Dashboard Display 89 | - Overview of live signal counts, system health, and active drones with proximity alerts 90 | 91 |
92 | Dashboard View 93 |
94 | 95 | ### Live Drone View 96 | - Interactive maps with live flight paths, spoof analysis, and MAC randomization details 97 | 98 |
99 | Drone Detection Screenshot 100 |
101 | 102 | > **Tip:** Tap the "Live" map button for full-screen tracking and select an active drone for details. 103 | 104 | ## History & Analysis 105 | 106 | ### Encounter History 107 | - Logs each drone encounter automatically with options to search, sort, review, export, or delete records. 108 | 109 |
110 | Encounter History View 111 |
112 | 113 | ### FAA Database Analysis 114 | 115 | ![image](https://github.com/user-attachments/assets/3c5165f1-4177-4934-8a79-4196f3824ba3) 116 | 117 | 118 | ## App Settings 119 | 120 | ### Settings & Warning Dials 121 | - Customize warning thresholds, proximity alerts, and display preferences. 122 | - Set limits for CPU usage, temperature (including PLUTO and ZYNQ), memory, and RSSI. 123 | 124 |
125 | Warning Configuration 126 |
127 | 128 | --- 129 | 130 | ## Hardware Requirements 131 | 132 | ### Option 1: [WarDragon/Pro](https://cemaxecuter.com/?post_type=product) 133 | 134 | ### Option 2: DIY Setup 135 | 136 | Configuration A. WiFi & BT Adapters 137 | - ESP32 with WiFi RID Firmware, or a a WiFi adapter using DroneID `wifi_sniffer` below 138 | - Sniffle-compatible BT dongle (Catsniffer, Sonoff) flashed with Sniffle FW. 139 | 140 | Configuration B. Single Xiao ESP32S3 141 | - Flash it with this [firmware](https://github.com/lukeswitz/T-Halow/blob/master/firmware/xiao_s3dualcoreRIDfirmware.bin) 142 | - Change port name and firmware filepath: 143 | ```esptool.py --chip esp32s3 --port /dev/yourportname --baud 115200 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 16MB 0x10000 firmwareFile.bin``` 144 | 145 | - Swap in updated zmq decoder that handles both types over UART [here](https://github.com/lukeswitz/DroneID/blob/dual-esp32-rid/zmq_decoder.py) 146 | 147 | - (Optional) ANTSDR E200 & DJI FW 148 | 149 | --- 150 | 151 | ## Software Requirements 152 | 153 | This section covers setting up the backend Python environment on Linux, macOS, and Windows. 154 | 155 | **Required** 156 | - [Sniffle](https://github.com/nccgroup/Sniffle) 157 | - [DroneID](https://github.com/alphafox02/DroneID) 158 | 159 | **Optional** 160 | - [DJI Firmware - E200](https://github.com/alphafox02/antsdr_dji_droneid), [WiFi Remote ID Firmware](https://github.com/alphafox02/T-Halow/tree/wifi_rid/examples/DragonOS_RID_Scanner), [DragonSync Python](https://github.com/alphafox02/DragonSync) 161 | 162 | 163 | ### Python Tools Setup Instructions 164 | 165 | #### Linux 166 | 1. **Install Dependencies:** 167 | 168 | sudo apt update && sudo apt install -y python3 python3-pip git gpsd gpsd-clients lm-sensors 169 | 170 | 2. **Clone & Setup:** 171 | 172 | git clone https://github.com/alphafox02/DroneID.git 173 | git clone https://github.com/alphafox02/DragonSync.git 174 | cd DroneID 175 | git submodule update --init 176 | ./setup.sh 177 | 178 | #### macOS 179 | 1. **Install Homebrew & Dependencies:** 180 | 181 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 182 | brew install python3 git gpsd 183 | 184 | 2. **Clone & Setup:** 185 | 186 | git clone https://github.com/alphafox02/DroneID.git 187 | git clone https://github.com/alphafox02/DragonSync.git 188 | cd DroneID 189 | git submodule update --init 190 | ./setup.sh 191 | 192 | #### Windows (Using WSL or Native) 193 | - **WSL (Recommended):** 194 | Install WSL (`wsl --install`) and follow the Linux instructions. 195 | - **Native Setup:** 196 | Install Python and Git from [python.org](https://www.python.org/downloads/) and [git-scm.com](https://git-scm.com/download/win), then clone and set up using Git commands above. 197 | - **Install Backend Dependencies** 198 | 199 | # DroneID Setup 200 | git clone https://github.com/alphafox02/DroneID.git 201 | cd DroneID 202 | git submodule update --init 203 | ./setup.sh 204 | 205 | # Install additional dependencies: 206 | sudo apt update && sudo apt install lm-sensors gpsd gpsd-clients 207 | cd .. 208 | git clone https://github.com/alphafox02/DragonSync/ 209 | 210 | 211 | ## Connection Choices 212 | 213 | ### ZMQ Server (JSON) – Recommended 214 | 215 | The ZMQ Server option provides direct JSON-based communication with full data access. Ideal for detailed monitoring and SDR decoding. 216 | 217 | ### Multicast (CoT) – Experimental 218 | 219 | The Multicast option uses Cursor on Target (CoT) to transmit data for integration with TAK/ATAK systems. It supports multiple instances but may offer less detailed data compared to ZMQ. 220 | 221 | --- 222 | 223 | ## Backend Data Guide 224 | 225 | ### ZMQ Commands 226 | 227 | > **Monitoring & Decoding Options** 228 | 229 | | **Task** | **Command** | **Notes** | 230 | |------------------------------|-------------------------------------------------------------------------------------------|-----------------------------------| 231 | | **System Monitor** | `python3 wardragon_monitor.py --zmq_host 0.0.0.0 --zmq_port 4225 --interval 30` | Works on most Linux systems | 232 | | **SDR Decoding (DroneID)** | `python3 zmq_decoder.py --dji -z --zmqsetting 0.0.0.0:4224` | Required for DroneID SDR decoding | 233 | 234 | > **Starting Sniffers & Decoders** 235 | 236 | | **Sniffer Type** | **Command** | **Notes** | 237 | |---------------------------------------|----------------------------------------------------------------------------------------------------------------|-------------------------------------| 238 | | **BT Sniffer for Sonoff (no `-b`)** | `python3 Sniffle/python_cli/sniff_receiver.py -l -e -a -z -b 2000000` | Requires Sniffle | 239 | | **WiFi Sniffer (Wireless Adapter)** | `python3 wifi_receiver.py --interface wlan0 -z --zmqsetting 127.0.0.1:4223` | Requires compatible WiFi adapter | 240 | | **WiFi Adapter/BT Decoder** | `python3 zmq_decoder.py -z --zmqsetting 0.0.0.0:4224 --zmqclients 127.0.0.1:4222,127.0.0.1:4223 -v` | Run after starting WiFi sniffer | 241 | | **ESP32/BT Decoder** | `python3 zmq_decoder.py -z --uart /dev/esp0 --zmqsetting 0.0.0.0:4224 --zmqclients 127.0.0.1:4222 -v` | Replace `/dev/esp0` with actual port | 242 | 243 | 244 | --- 245 | 246 | ## Build Instructions 247 | 248 | 1. **Clone Repository:** 249 | 250 | git clone https://github.com/Root-Down-Digital/DragonSync-iOS.git 251 | 252 | 2. **Build the iOS App:** 253 | 254 | cd DragonSync-iOS 255 | pod install 256 | 257 | 3. **Open in Xcode:** 258 | Open `WarDragon.xcworkspace` 259 | 260 | 4. **Deploy:** 261 | Run the backend scripts as described; then build and deploy to your iOS device or use TestFlight. 262 | 263 | --- 264 | 265 | ## Credits, Disclaimer & License 266 | 267 | - **Credits:** 268 | - [DragonSync](https://github.com/alphafox02/DragonSync) 269 | - [DroneID](https://github.com/alphafox02/DroneID) 270 | - [Sniffle](https://github.com/nccgroup/Sniffle) 271 | - Special thanks to [@alphafox02](https://github.com/alphafox02) and [@bkerler](https://github.com/bkerler) 272 | 273 | - **Disclaimer:** 274 | This software is provided as-is without warranty. Use at your own risk and in compliance with local regulations. 275 | 276 | - **License:** 277 | MIT License. See `LICENSE.md` for details. 278 | 279 | --- 280 | 281 | ## Contributing & Contact 282 | 283 | - **Contributing:** Contributions are welcome via pull requests or by opening an issue. 284 | - **Contact:** For support, please open an issue in this repository. 285 | 286 | --- 287 | 288 | ## Notes 289 | 290 | **DragonSync is under active development; features may change or have bugs. Feedback welcome** 291 | 292 | > [!IMPORTANT] 293 | > Keep your WarDragon DragonOS image updated for optimal compatibility. 294 | 295 | > [!TIP] 296 | > Ensure your iOS device and backend system are on the same local network for best performance. 297 | 298 | > [!CAUTION] 299 | > Use in compliance with local regulations to avoid legal issues. 300 | -------------------------------------------------------------------------------- /Scripts/ANT_Spectrum.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import time 5 | import zmq 6 | import argparse 7 | import numpy as np 8 | from threading import Thread, Event 9 | import iio 10 | from scipy import signal 11 | 12 | class ANTSDRSpectrumAnalyzer: 13 | def __init__(self, uri="ip:192.168.1.10", buffer_size=1024, sample_rate=30.72e6, averaging=8): 14 | self.ctx = iio.Context(uri) 15 | self.phy = self.ctx.find_device("ad9361-phy") 16 | self.rx = self.ctx.find_device("cf-ad9361-lpc") 17 | self.buffer_size = buffer_size 18 | self.sample_rate = sample_rate 19 | self.center_freq = 915e6 20 | self.bandwidth = 20e6 21 | self.rx_gain = 50.0 22 | self.stop_signal = Event() 23 | self.window = signal.windows.hann(buffer_size) 24 | self.spectrum_buffer = [] 25 | self.averaging = averaging 26 | 27 | def configure_device(self): 28 | self.phy.channels[0].attrs["frequency"].value = str(int(self.center_freq)) 29 | self.phy.channels[0].attrs["sampling_frequency"].value = str(int(self.sample_rate)) 30 | self.phy.channels[0].attrs["rf_bandwidth"].value = str(int(self.bandwidth)) 31 | self.phy.channels[0].attrs["hardwaregain"].value = str(int(self.rx_gain)) 32 | self.rx_channel = self.rx.channels[0] 33 | self.rx_buffer = iio.Buffer(self.rx, self.buffer_size, False) 34 | 35 | def get_spectrum_data(self): 36 | self.rx_buffer.refill() 37 | data = self.rx_buffer.read() 38 | samples = np.frombuffer(data, dtype=np.int16) 39 | iq = samples[::2] + 1j * samples[1::2] 40 | 41 | segments = signal.windows.get_window('hann', self.buffer_size) 42 | freqs, times, Sxx = signal.spectrogram(iq, window=segments, 43 | noverlap=int(self.buffer_size * 0.75)) 44 | 45 | if len(self.spectrum_buffer) >= self.averaging: 46 | self.spectrum_buffer.pop(0) 47 | self.spectrum_buffer.append(10 * np.log10(np.abs(Sxx))) 48 | 49 | return np.mean(self.spectrum_buffer, axis=0) 50 | 51 | def start_streaming(self, zmq_socket): 52 | self.configure_device() 53 | while not self.stop_signal.is_set(): 54 | try: 55 | spectrum_data = self.get_spectrum_data() 56 | freq_points = np.linspace( 57 | self.center_freq - self.bandwidth/2, 58 | self.center_freq + self.bandwidth/2, 59 | len(spectrum_data) 60 | ) 61 | 62 | data_dict = { 63 | "timestamp": time.time(), 64 | "center_freq": self.center_freq, 65 | "bandwidth": self.bandwidth, 66 | "sample_rate": self.sample_rate, 67 | "gain": self.rx_gain, 68 | "spectrum": spectrum_data.tolist(), 69 | "frequency_points": freq_points.tolist() 70 | } 71 | zmq_socket.send_json(data_dict) 72 | time.sleep(0.1) 73 | except Exception as e: 74 | print(f"Streaming error: {e}") 75 | break 76 | 77 | def stop_streaming(self): 78 | self.stop_signal.set() 79 | 80 | def main(): 81 | parser = argparse.ArgumentParser() 82 | parser.add_argument("--uri", default="ip:192.168.1.10", help="ANTSDR URI") 83 | parser.add_argument("--zmq-host", default="0.0.0.0", help="ZMQ host") 84 | parser.add_argument("--zmq-port", type=int, default=4226, help="ZMQ port") 85 | parser.add_argument("--buffer-size", type=int, default=1024, help="Buffer size") 86 | parser.add_argument("--sample-rate", type=float, default=30.72e6, help="Sample rate") 87 | parser.add_argument("--averaging", type=int, default=8, help="Number of averages") 88 | args = parser.parse_args() 89 | 90 | context = zmq.Context() 91 | socket = context.socket(zmq.PUB) 92 | socket.bind(f"tcp://{args.zmq_host}:{args.zmq_port}") 93 | 94 | analyzer = ANTSDRSpectrumAnalyzer( 95 | args.uri, 96 | args.buffer_size, 97 | args.sample_rate, 98 | args.averaging 99 | ) 100 | stream_thread = Thread(target=analyzer.start_streaming, args=(socket,)) 101 | stream_thread.start() 102 | 103 | try: 104 | while True: 105 | time.sleep(1) 106 | except KeyboardInterrupt: 107 | analyzer.stop_streaming() 108 | stream_thread.join() 109 | socket.close() 110 | context.term() 111 | 112 | if __name__ == "__main__": 113 | main() -------------------------------------------------------------------------------- /WarDragon.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /WarDragon.xcodeproj/xcshareddata/xcschemes/WarDragon.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /WarDragon.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dragonLogo2.jpg", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "filename" : "dragonLogo2 1.jpg", 17 | "idiom" : "universal", 18 | "platform" : "ios", 19 | "size" : "1024x1024" 20 | }, 21 | { 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "tinted" 26 | } 27 | ], 28 | "idiom" : "universal", 29 | "platform" : "ios", 30 | "size" : "1024x1024" 31 | }, 32 | { 33 | "idiom" : "mac", 34 | "scale" : "1x", 35 | "size" : "16x16" 36 | }, 37 | { 38 | "filename" : "FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 7.png", 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "16x16" 42 | }, 43 | { 44 | "filename" : "FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 6.png", 45 | "idiom" : "mac", 46 | "scale" : "1x", 47 | "size" : "32x32" 48 | }, 49 | { 50 | "filename" : "FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 5.png", 51 | "idiom" : "mac", 52 | "scale" : "2x", 53 | "size" : "32x32" 54 | }, 55 | { 56 | "filename" : "FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 4.png", 57 | "idiom" : "mac", 58 | "scale" : "1x", 59 | "size" : "128x128" 60 | }, 61 | { 62 | "filename" : "FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 3.png", 63 | "idiom" : "mac", 64 | "scale" : "2x", 65 | "size" : "128x128" 66 | }, 67 | { 68 | "filename" : "FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 2.png", 69 | "idiom" : "mac", 70 | "scale" : "1x", 71 | "size" : "256x256" 72 | }, 73 | { 74 | "filename" : "FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 1.png", 75 | "idiom" : "mac", 76 | "scale" : "2x", 77 | "size" : "256x256" 78 | }, 79 | { 80 | "filename" : "FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45.png", 81 | "idiom" : "mac", 82 | "scale" : "1x", 83 | "size" : "512x512" 84 | }, 85 | { 86 | "filename" : "IMG_2697 1.png", 87 | "idiom" : "mac", 88 | "scale" : "2x", 89 | "size" : "512x512" 90 | } 91 | ], 92 | "info" : { 93 | "author" : "xcode", 94 | "version" : 1 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 1.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 2.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 3.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 4.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 5.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 6.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45 7.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/FFEF0CF7-F032-4CDD-B4F6-E7FDABE91C45.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/IMG_2697 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/IMG_2697 1.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/dragonLogo2 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/dragonLogo2 1.jpg -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/dragonLogo2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/AppIcon.appiconset/dragonLogo2.jpg -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/WickedDragon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "IMG_2697 2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "IMG_2697 1.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/WickedDragon.imageset/IMG_2697 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/WickedDragon.imageset/IMG_2697 1.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/WickedDragon.imageset/IMG_2697 2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/WickedDragon.imageset/IMG_2697 2x.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/dragon2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dragon1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "dragon2xpng.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "dragon3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/dragon2.imageset/dragon1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/dragon2.imageset/dragon1x.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/dragon2.imageset/dragon2xpng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/dragon2.imageset/dragon2xpng.png -------------------------------------------------------------------------------- /WarDragon/AppData/Assets.xcassets/dragon2.imageset/dragon3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Root-Down-Digital/DragonSync-iOS/433be7eab6f100d3f6d4d0c1c8f61457cb10de12/WarDragon/AppData/Assets.xcassets/dragon2.imageset/dragon3x.png -------------------------------------------------------------------------------- /WarDragon/AppData/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 | 30 | 36 | 45 | 51 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /WarDragon/AppData/WarDragonApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WarDragonApp.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 11/18/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Font { 11 | static let appDefault = Font.system(.body, design: .monospaced) 12 | static let appHeadline = Font.system(.headline, design: .monospaced) 13 | static let appSubheadline = Font.system(.subheadline, design: .monospaced) 14 | static let appCaption = Font.system(.caption, design: .monospaced) 15 | } 16 | 17 | 18 | @main 19 | struct WarDragonApp: App { 20 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 21 | 22 | var body: some Scene { 23 | WindowGroup { 24 | ContentView() 25 | } 26 | } 27 | } 28 | 29 | class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { 30 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 31 | 32 | // Register notifications 33 | UNUserNotificationCenter.current().delegate = self 34 | 35 | // Register for app lifecycle notifications 36 | setupAppLifecycleObservers() 37 | 38 | return true 39 | } 40 | 41 | func applicationDidBecomeActive(_ application: UIApplication) { 42 | if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { 43 | windowScene.windows.first?.frame = windowScene.windows.first?.frame ?? .zero 44 | } 45 | } 46 | 47 | func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { 48 | completionHandler([.banner, .sound]) 49 | } 50 | 51 | private func setupAppLifecycleObservers() { 52 | NotificationCenter.default.addObserver( 53 | self, 54 | selector: #selector(appMovingToBackground), 55 | name: UIApplication.didEnterBackgroundNotification, 56 | object: nil 57 | ) 58 | 59 | NotificationCenter.default.addObserver( 60 | self, 61 | selector: #selector(appMovingToForeground), 62 | name: UIApplication.willEnterForegroundNotification, 63 | object: nil 64 | ) 65 | } 66 | 67 | @objc private func appMovingToBackground() { 68 | // Start background processing if listening is active 69 | if Settings.shared.isListening && Settings.shared.enableBackgroundDetection { 70 | BackgroundManager.shared.startBackgroundProcessing() 71 | } 72 | } 73 | 74 | @objc private func appMovingToForeground() { 75 | // Nothing to do, let normal operation resume 76 | } 77 | 78 | deinit { 79 | NotificationCenter.default.removeObserver(self) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /WarDragon/Data Handling/ServiceManager/ServiceControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceControl.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 1/10/25. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct ServiceControl: Identifiable, Hashable { 12 | let id: String 13 | let service: String 14 | let category: ServiceCategory 15 | let description: String 16 | let dependencies: [String] 17 | let isCritical: Bool 18 | var status: ServiceStatus 19 | var resources: ResourceUsage? 20 | var issues: [ServiceIssue] 21 | 22 | enum ServiceCategory: String { 23 | case radio = "radio" 24 | case sensors = "sensors" 25 | case comms = "comms" 26 | 27 | var icon: String { 28 | switch self { 29 | case .radio: return "antenna.radiowaves.left.and.right" 30 | case .sensors: return "sensor" 31 | case .comms: return "network" 32 | } 33 | } 34 | 35 | var color: Color { 36 | switch self { 37 | case .radio: return .blue 38 | case .sensors: return .green 39 | case .comms: return .orange 40 | } 41 | } 42 | } 43 | 44 | struct ServiceStatus { 45 | var isActive: Bool 46 | var isEnabled: Bool 47 | var statusText: String 48 | var rawStatus: String? 49 | var healthStatus: HealthStatus 50 | 51 | enum HealthStatus { 52 | case healthy 53 | case warning 54 | case critical 55 | case unknown 56 | 57 | var color: Color { 58 | switch self { 59 | case .healthy: return .green 60 | case .warning: return .yellow 61 | case .critical: return .red 62 | case .unknown: return .gray 63 | } 64 | } 65 | } 66 | } 67 | 68 | struct ResourceUsage: Hashable { 69 | var cpuPercent: Double 70 | var memoryPercent: Double 71 | } 72 | 73 | struct ServiceIssue: Identifiable, Hashable { 74 | let id = UUID() 75 | let message: String 76 | let severity: IssueSeverity 77 | 78 | enum IssueSeverity: Hashable { 79 | case high 80 | case medium 81 | case warning 82 | 83 | var color: Color { 84 | switch self { 85 | case .high: return .red 86 | case .medium: return .orange 87 | case .warning: return .yellow 88 | } 89 | } 90 | } 91 | } 92 | 93 | // Hashable conformance 94 | func hash(into hasher: inout Hasher) { 95 | hasher.combine(id) 96 | hasher.combine(service) 97 | hasher.combine(category) 98 | hasher.combine(description) 99 | hasher.combine(dependencies) 100 | hasher.combine(isCritical) 101 | hasher.combine(status.isActive) 102 | hasher.combine(status.isEnabled) 103 | hasher.combine(status.statusText) 104 | hasher.combine(resources?.cpuPercent) 105 | hasher.combine(resources?.memoryPercent) 106 | hasher.combine(issues) 107 | } 108 | 109 | static func == (lhs: ServiceControl, rhs: ServiceControl) -> Bool { 110 | lhs.id == rhs.id && 111 | lhs.service == rhs.service && 112 | lhs.category == rhs.category && 113 | lhs.description == rhs.description && 114 | lhs.dependencies == rhs.dependencies && 115 | lhs.isCritical == rhs.isCritical && 116 | lhs.status.isActive == rhs.status.isActive && 117 | lhs.status.isEnabled == rhs.status.isEnabled && 118 | lhs.status.statusText == rhs.status.statusText && 119 | lhs.resources?.cpuPercent == rhs.resources?.cpuPercent && 120 | lhs.resources?.memoryPercent == rhs.resources?.memoryPercent && 121 | lhs.issues == rhs.issues 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /WarDragon/Data Handling/ServiceManager/ServiceViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceViewModel.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 1/10/25. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | class ServiceViewModel: ObservableObject { 12 | @Published var services: [ServiceControl] = [] 13 | @Published var healthReport: HealthReport? 14 | @Published var isLoading = false 15 | @Published var error: String? 16 | 17 | private let zmqHandler = ZMQHandler() 18 | 19 | struct HealthReport { 20 | var overallHealth: String 21 | var issues: [ServiceControl.ServiceIssue] 22 | var timestamp: Date 23 | 24 | var statusColor: Color { 25 | switch overallHealth.lowercased() { 26 | case "healthy": return .green 27 | case "degraded": return .yellow 28 | default: return .red 29 | } 30 | } 31 | } 32 | 33 | func startMonitoring() { 34 | zmqHandler.connect( 35 | host: Settings.shared.zmqHost, 36 | zmqTelemetryPort: UInt16(Settings.shared.zmqTelemetryPort), 37 | zmqStatusPort: UInt16(Settings.shared.zmqStatusPort), 38 | onTelemetry: { _ in }, // Undefined for unused telemetry port 39 | onStatus: { [weak self] message in 40 | self?.handleStatusUpdate(message) 41 | } 42 | ) 43 | } 44 | 45 | func stopMonitoring() { 46 | zmqHandler.disconnect() 47 | } 48 | 49 | func toggleService(_ service: ServiceControl) { 50 | isLoading = true 51 | 52 | let command = [ 53 | "command": [ 54 | "type": "service_control", 55 | "service": service.id, 56 | "action": service.status.isActive ? "disable" : "enable", 57 | "timestamp": Date().timeIntervalSince1970 58 | ] 59 | ] 60 | 61 | zmqHandler.sendServiceCommand(command) { [weak self] (success: Bool, response: Any?) in 62 | DispatchQueue.main.async { 63 | self?.isLoading = false 64 | if !success { 65 | self?.error = response as? String 66 | } 67 | } 68 | } 69 | } 70 | 71 | func restartService(_ service: ServiceControl) { 72 | isLoading = true 73 | 74 | let command = [ 75 | "command": [ 76 | "type": "service_control", 77 | "service": service.id, 78 | "action": "restart", 79 | "timestamp": Date().timeIntervalSince1970 80 | ] 81 | ] 82 | 83 | zmqHandler.sendServiceCommand(command) { [weak self] (success: Bool, response: Any?) in 84 | DispatchQueue.main.async { 85 | self?.isLoading = false 86 | if !success { 87 | self?.error = response as? String 88 | } 89 | } 90 | } 91 | } 92 | 93 | private func handleStatusUpdate(_ message: String) { 94 | guard let data = message.data(using: .utf8), 95 | let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], 96 | let stats = json["system_stats"] as? [String: Any], 97 | let services = stats["services"] as? [String: Any] else { 98 | return 99 | } 100 | 101 | var updatedServices: [ServiceControl] = [] 102 | 103 | // Parse the services 104 | if let categories = services["by_category"] as? [String: [String: Any]] { 105 | for (category, serviceDict) in categories { 106 | for (serviceName, details) in serviceDict { 107 | guard let details = details as? [String: Any] else { continue } 108 | 109 | // Get status info 110 | let status = details["status"] as? [String: Any] ?? [:] 111 | let active = status["active"] as? Bool ?? false 112 | let enabled = status["enabled"] as? Bool ?? false 113 | 114 | // Get resource usage if available 115 | let resources = details["resources"] as? [String: Any] ?? [:] 116 | let cpuPercent = resources["cpu_percent"] as? Double ?? 0 117 | let memPercent = resources["mem_percent"] as? Double ?? 0 118 | 119 | // Parse any issues 120 | let issues = parseIssues(from: details["issues"] as? [[String: Any]] ?? []) 121 | 122 | // Create service status 123 | let serviceStatus = ServiceControl.ServiceStatus( 124 | isActive: active, 125 | isEnabled: enabled, 126 | statusText: active ? "Running" : "Stopped", 127 | rawStatus: status["raw_status"] as? String, 128 | healthStatus: determineHealthStatus( 129 | active: active, 130 | issues: issues 131 | ) 132 | ) 133 | 134 | // Create resource usage info if available 135 | let resourceUsage = cpuPercent > 0 || memPercent > 0 136 | ? ServiceControl.ResourceUsage( 137 | cpuPercent: cpuPercent, 138 | memoryPercent: memPercent) 139 | : nil 140 | 141 | let service = ServiceControl( 142 | id: serviceName, 143 | service: serviceName, 144 | category: ServiceControl.ServiceCategory(rawValue: category) ?? .comms, 145 | description: details["description"] as? String ?? serviceName, 146 | dependencies: details["dependencies"] as? [String] ?? [], 147 | isCritical: details["critical"] as? Bool ?? false, 148 | status: serviceStatus, 149 | resources: resourceUsage, 150 | issues: issues 151 | ) 152 | 153 | updatedServices.append(service) 154 | } 155 | } 156 | } 157 | 158 | // Update health report 159 | if let healthReport = services["health_report"] as? [String: Any] { 160 | self.healthReport = HealthReport( 161 | overallHealth: healthReport["overall_health"] as? String ?? "unknown", 162 | issues: parseIssues(from: healthReport["issues"] as? [[String: Any]] ?? []), 163 | timestamp: Date() 164 | ) 165 | } 166 | 167 | DispatchQueue.main.async { 168 | self.services = updatedServices 169 | } 170 | } 171 | 172 | 173 | private func parseServiceDetails(name: String, category: String, details: [String: Any]) -> ServiceControl { 174 | let serviceInfo = details["status"] as? [String: Any] ?? [:] 175 | let resources = details["resources"] as? [String: Any] ?? [:] 176 | let dependencies = (details["dependencies"] as? [String]) ?? [] 177 | let issues = parseIssues(from: details["issues"] as? [[String: Any]] ?? []) 178 | 179 | let status = ServiceControl.ServiceStatus( 180 | isActive: serviceInfo["active"] as? Bool ?? false, 181 | isEnabled: serviceInfo["enabled"] as? Bool ?? false, 182 | statusText: serviceInfo["status"] as? String ?? "unknown", 183 | rawStatus: details["raw_status"] as? String, 184 | healthStatus: determineHealthStatus( 185 | active: serviceInfo["active"] as? Bool ?? false, 186 | issues: issues 187 | ) 188 | ) 189 | 190 | let resourceUsage = ServiceControl.ResourceUsage( 191 | cpuPercent: resources["cpu_percent"] as? Double ?? 0.0, 192 | memoryPercent: resources["mem_percent"] as? Double ?? 0.0 193 | ) 194 | 195 | return ServiceControl( 196 | id: name, 197 | service: details["service"] as? String ?? name, 198 | category: ServiceControl.ServiceCategory(rawValue: category) ?? .comms, 199 | description: details["description"] as? String ?? "", 200 | dependencies: dependencies, 201 | isCritical: details["critical"] as? Bool ?? false, 202 | status: status, 203 | resources: resourceUsage, 204 | issues: issues 205 | ) 206 | } 207 | 208 | private func parseIssues(from issues: [[String: Any]]) -> [ServiceControl.ServiceIssue] { 209 | return issues.compactMap { issue in 210 | guard let message = issue["error"] as? String, 211 | let severityStr = issue["severity"] as? String else { 212 | return nil 213 | } 214 | 215 | let severity: ServiceControl.ServiceIssue.IssueSeverity 216 | switch severityStr { 217 | case "high": severity = .high 218 | case "medium": severity = .medium 219 | default: severity = .warning 220 | } 221 | 222 | return ServiceControl.ServiceIssue( 223 | message: message, 224 | severity: severity 225 | ) 226 | } 227 | } 228 | 229 | private func determineHealthStatus( 230 | active: Bool, 231 | issues: [ServiceControl.ServiceIssue] 232 | ) -> ServiceControl.ServiceStatus.HealthStatus { 233 | if !active { 234 | return .critical 235 | } 236 | 237 | if issues.contains(where: { $0.severity == .high }) { 238 | return .critical 239 | } 240 | 241 | if issues.contains(where: { $0.severity == .medium }) { 242 | return .warning 243 | } 244 | 245 | return active ? .healthy : .unknown 246 | } 247 | 248 | private func parseHealthReport(_ report: [String: Any]) -> HealthReport { 249 | let issues = (report["issues"] as? [[String: Any]] ?? []).compactMap { issueDict -> ServiceControl.ServiceIssue? in 250 | guard let message = issueDict["error"] as? String, 251 | let severityStr = issueDict["severity"] as? String else { 252 | return nil 253 | } 254 | 255 | let severity: ServiceControl.ServiceIssue.IssueSeverity 256 | switch severityStr { 257 | case "high": 258 | severity = .high 259 | case "medium": 260 | severity = .medium 261 | default: 262 | severity = .warning 263 | } 264 | 265 | return ServiceControl.ServiceIssue( 266 | message: message, 267 | severity: severity 268 | ) 269 | } 270 | 271 | return HealthReport( 272 | overallHealth: report["overall_health"] as? String ?? "unknown", 273 | issues: issues, 274 | timestamp: Date() 275 | ) 276 | } 277 | 278 | func servicesByCategory() -> [ServiceControl.ServiceCategory: [ServiceControl]] { 279 | Dictionary(grouping: services) { $0.category } 280 | } 281 | 282 | func criticalServices() -> [ServiceControl] { 283 | services.filter { $0.isCritical } 284 | } 285 | 286 | func servicesWithIssues() -> [ServiceControl] { 287 | services.filter { !$0.issues.isEmpty } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /WarDragon/Data Handling/SpectrumData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpectrumData.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 12/26/24. 6 | // 7 | 8 | import Foundation 9 | import Network 10 | 11 | struct SpectrumData: Codable, Identifiable { 12 | static let SUSCAN_REMOTE_FRAGMENT_HEADER_MAGIC: UInt32 = 0xABCD0123 13 | static let SUSCAN_ANALYZER_SUPERFRAME_TYPE_PSD: UInt8 = 0x02 14 | 15 | struct RemoteHeader { 16 | let magic: UInt32 // 0xABCD0123 17 | let sfType: UInt8 // Type 0x02 for PSD 18 | let size: UInt16 // Fragment size 19 | let sfId: UInt8 // Fragment ID 20 | let sfSize: UInt32 // Total data size 21 | let sfOffset: UInt32 // Offset in data 22 | 23 | init(data: Data) { 24 | var offset = 0 25 | magic = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt32.self) } 26 | offset += MemoryLayout.size 27 | sfType = data[offset]; offset += 1 28 | size = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt16.self) } 29 | offset += MemoryLayout.size 30 | sfId = data[offset]; offset += 1 31 | sfSize = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt32.self) } 32 | offset += MemoryLayout.size 33 | sfOffset = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt32.self) } 34 | } 35 | 36 | static var headerSize: Int { 37 | return MemoryLayout.size + 1 + MemoryLayout.size + 1 + MemoryLayout.size * 2 38 | } 39 | } 40 | 41 | var id: UUID 42 | var fc: Int // Center frequency 43 | var timestamp: Double 44 | var sampleRate: Float // Sample rate in Hz 45 | var data: [Float] // FFT data points 46 | 47 | @MainActor 48 | class SpectrumViewModel: ObservableObject { 49 | @Published private(set) var spectrumData: [SpectrumData] = [] 50 | @Published private(set) var isListening = false 51 | @Published var connectionError: String? 52 | 53 | private var connection: NWConnection? 54 | private let queue = DispatchQueue(label: "com.wardragon.spectrum") 55 | private var fragmentBuffer: [UInt8: (timestamp: Double, data: Data)] = [:] 56 | 57 | func startListening(port: UInt16) { 58 | guard !isListening else { return } 59 | 60 | let parameters = NWParameters.udp 61 | parameters.allowLocalEndpointReuse = true 62 | parameters.prohibitedInterfaceTypes = [.cellular] 63 | parameters.requiredInterfaceType = .wifi 64 | 65 | connection = NWConnection( 66 | to: NWEndpoint.hostPort( 67 | host: NWEndpoint.Host("0.0.0.0"), 68 | port: NWEndpoint.Port(integerLiteral: port) 69 | ), 70 | using: parameters 71 | ) 72 | 73 | connection?.stateUpdateHandler = { [weak self] state in 74 | Task { @MainActor in 75 | switch state { 76 | case .ready: 77 | self?.isListening = true 78 | self?.connectionError = nil 79 | await self?.receiveData() 80 | case .failed(let error): 81 | self?.isListening = false 82 | self?.connectionError = error.localizedDescription 83 | case .cancelled: 84 | self?.isListening = false 85 | self?.connectionError = nil 86 | default: 87 | break 88 | } 89 | } 90 | } 91 | 92 | connection?.start(queue: queue) 93 | } 94 | 95 | private func receiveData() async { 96 | connection?.receiveMessage { [weak self] content, _, _, error in 97 | guard let self = self else { return } 98 | 99 | if let error = error { 100 | Task { @MainActor in 101 | self.connectionError = error.localizedDescription 102 | } 103 | return 104 | } 105 | 106 | if let data = content { 107 | Task { @MainActor in 108 | await self.processFragment(data) 109 | } 110 | } 111 | 112 | Task { @MainActor in 113 | if self.isListening { 114 | await self.receiveData() 115 | } 116 | } 117 | } 118 | } 119 | 120 | private func processFragment(_ data: Data) async { 121 | guard data.count >= RemoteHeader.headerSize else { return } 122 | 123 | let header = RemoteHeader(data: data) 124 | guard header.magic == SpectrumData.SUSCAN_REMOTE_FRAGMENT_HEADER_MAGIC else { return } 125 | guard header.sfType == SpectrumData.SUSCAN_ANALYZER_SUPERFRAME_TYPE_PSD else { return } 126 | 127 | let payload = data.dropFirst(RemoteHeader.headerSize) 128 | let now = Date().timeIntervalSince1970 129 | 130 | // Store fragment 131 | if fragmentBuffer[header.sfId] == nil { 132 | fragmentBuffer[header.sfId] = (timestamp: now, data: Data()) 133 | } 134 | fragmentBuffer[header.sfId]?.data.append(payload) 135 | 136 | // Check if fragment is complete 137 | if let fragment = fragmentBuffer[header.sfId], fragment.data.count >= header.sfSize { 138 | let spectralData = fragment.data.withUnsafeBytes { ptr -> [Float] in 139 | Array(UnsafeBufferPointer( 140 | start: ptr.baseAddress?.assumingMemoryBound(to: Float.self), 141 | count: fragment.data.count / MemoryLayout.size)) 142 | } 143 | 144 | // First float is center freq, second is sample rate, rest is FFT data 145 | let spectrum = SpectrumData( 146 | id: UUID(), 147 | fc: Int(spectralData[0]), 148 | timestamp: fragment.timestamp, 149 | sampleRate: spectralData[1], 150 | data: Array(spectralData.dropFirst(2)) 151 | ) 152 | 153 | self.spectrumData.append(spectrum) 154 | if self.spectrumData.count > 100 { 155 | self.spectrumData.removeFirst() 156 | } 157 | 158 | fragmentBuffer[header.sfId] = nil 159 | } 160 | } 161 | 162 | func stopListening() { 163 | connection?.cancel() 164 | connection = nil 165 | isListening = false 166 | connectionError = nil 167 | fragmentBuffer.removeAll() 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /WarDragon/Data Handling/Storage/BackgroundManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundManager.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 4/16/25. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import Network 11 | 12 | class BackgroundManager { 13 | static let shared = BackgroundManager() 14 | 15 | private var backgroundTask: UIBackgroundTaskIdentifier = .invalid 16 | private var timer: Timer? 17 | @Published var isBackgroundModeActive = false 18 | private var networkMonitor: NWPathMonitor? 19 | private var hasActiveConnection = true 20 | private var isInBackground = false 21 | 22 | // Weak reference to the CoTViewModel to avoid reference cycles 23 | private weak var cotViewModel: CoTViewModel? 24 | 25 | // Setup method to be called during app initialization 26 | func configure(with viewModel: CoTViewModel) { 27 | self.cotViewModel = viewModel 28 | // Register for app state change notifications 29 | setupNotifications() 30 | startNetworkMonitoring() 31 | } 32 | 33 | private func setupNotifications() { 34 | NotificationCenter.default.addObserver( 35 | self, 36 | selector: #selector(applicationWillResignActive), 37 | name: UIApplication.willResignActiveNotification, 38 | object: nil 39 | ) 40 | 41 | NotificationCenter.default.addObserver( 42 | self, 43 | selector: #selector(applicationDidBecomeActive), 44 | name: UIApplication.didBecomeActiveNotification, 45 | object: nil 46 | ) 47 | 48 | NotificationCenter.default.addObserver( 49 | self, 50 | selector: #selector(applicationDidEnterBackground), 51 | name: UIApplication.didEnterBackgroundNotification, 52 | object: nil 53 | ) 54 | 55 | NotificationCenter.default.addObserver( 56 | self, 57 | selector: #selector(applicationWillTerminate), 58 | name: UIApplication.willTerminateNotification, 59 | object: nil 60 | ) 61 | } 62 | 63 | @objc private func applicationWillResignActive() { 64 | // App is about to go into the background 65 | if Settings.shared.isListening { 66 | startBackgroundProcessing() 67 | } 68 | } 69 | 70 | @objc private func applicationDidBecomeActive() { 71 | // App has returned to the foreground 72 | isInBackground = false 73 | 74 | // Check if we need to reconnect 75 | if Settings.shared.isListening { 76 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 77 | self.cotViewModel?.reconnectIfNeeded() 78 | } 79 | } 80 | 81 | // Stop background processing if it was active 82 | if isBackgroundModeActive { 83 | stopBackgroundProcessing() 84 | } 85 | } 86 | 87 | @objc private func applicationDidEnterBackground() { 88 | isInBackground = true 89 | 90 | // Start background processing if we should be listening 91 | if Settings.shared.isListening && !isBackgroundModeActive { 92 | startBackgroundProcessing() 93 | } 94 | } 95 | 96 | @objc private func applicationWillTerminate() { 97 | // Clean up resources when app is terminating 98 | stopBackgroundProcessing() 99 | networkMonitor?.cancel() 100 | networkMonitor = nil 101 | } 102 | 103 | // Network monitoring to detect changes while in background 104 | private func startNetworkMonitoring() { 105 | networkMonitor = NWPathMonitor() 106 | networkMonitor?.pathUpdateHandler = { [weak self] path in 107 | let newConnectionState = path.status == .satisfied 108 | let connectionChanged = newConnectionState != self?.hasActiveConnection 109 | 110 | self?.hasActiveConnection = newConnectionState 111 | 112 | // If we're in the background and network status changed, handle it 113 | if connectionChanged && self?.isInBackground == true { 114 | if newConnectionState { 115 | // Network came back - try to reconnect 116 | self?.handleNetworkReturn() 117 | } else { 118 | // Network lost - notify the app 119 | self?.handleNetworkLoss() 120 | } 121 | } 122 | } 123 | 124 | networkMonitor?.start(queue: DispatchQueue.global(qos: .background)) 125 | } 126 | 127 | private func handleNetworkReturn() { 128 | // Network became available while in background 129 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 130 | self.cotViewModel?.reconnectIfNeeded() 131 | } 132 | } 133 | 134 | private func handleNetworkLoss() { 135 | // Network was lost while in background 136 | // Could implement safe shutdown of connection resources here 137 | } 138 | 139 | func startBackgroundProcessing() { 140 | // Prevent duplicate starts 141 | guard !isBackgroundModeActive else { return } 142 | 143 | // Begin background task 144 | beginBackgroundTask() 145 | 146 | // Start a timer to periodically refresh the background task 147 | startKeepAliveTimer() 148 | 149 | isBackgroundModeActive = true 150 | isInBackground = true 151 | 152 | // Notify that we're entering background mode 153 | NotificationCenter.default.post(name: NSNotification.Name("EnteringBackgroundMode"), object: nil) 154 | } 155 | 156 | func stopBackgroundProcessing() { 157 | // End background task 158 | endBackgroundTask() 159 | 160 | isBackgroundModeActive = false 161 | 162 | // Notify that we're leaving background mode 163 | NotificationCenter.default.post(name: NSNotification.Name("LeavingBackgroundMode"), object: nil) 164 | } 165 | 166 | private func beginBackgroundTask() { 167 | // End existing task if any 168 | endBackgroundTask() 169 | 170 | // Begin a new background task with a safety buffer for cleanup 171 | backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in 172 | // Immediately end the task in the expiration handler 173 | if let self = self { 174 | let taskToEnd = self.backgroundTask 175 | self.backgroundTask = .invalid 176 | UIApplication.shared.endBackgroundTask(taskToEnd) 177 | } 178 | } 179 | } 180 | 181 | 182 | private func endBackgroundTask() { 183 | timer?.invalidate() 184 | timer = nil 185 | 186 | // End the background task if active 187 | if backgroundTask != .invalid { 188 | let taskToEnd = backgroundTask 189 | backgroundTask = .invalid 190 | UIApplication.shared.endBackgroundTask(taskToEnd) 191 | } 192 | } 193 | 194 | private func startKeepAliveTimer() { 195 | timer?.invalidate() 196 | 197 | // Periodically refresh the background task 198 | timer = Timer.scheduledTimer(withTimeInterval: 25, repeats: true) { [weak self] _ in 199 | self?.refreshBackgroundTask() 200 | } 201 | RunLoop.current.add(timer!, forMode: .common) 202 | } 203 | 204 | private func refreshBackgroundTask() { 205 | // End the current task and begin a new one to extend the runtime 206 | if backgroundTask != .invalid { 207 | let oldTask = backgroundTask 208 | 209 | // Start a new task before ending the old one to ensure continuity 210 | backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in 211 | // Immediately end the task in the expiration handler 212 | if let self = self { 213 | let taskToEnd = self.backgroundTask 214 | self.backgroundTask = .invalid 215 | UIApplication.shared.endBackgroundTask(taskToEnd) 216 | } 217 | } 218 | 219 | // End the old task only after creating a new one 220 | UIApplication.shared.endBackgroundTask(oldTask) 221 | 222 | // Do a lightweight connection check 223 | performLightweightConnectionCheck() 224 | } 225 | } 226 | 227 | private func performLightweightConnectionCheck() { 228 | // Notify that connections should be checked in a lightweight manner 229 | // Use a distinct notification name to differentiate from heavier processing 230 | NotificationCenter.default.post(name: NSNotification.Name("LightweightConnectionCheck"), object: nil) 231 | 232 | // If we have direct access to the view model, we could check more directly 233 | if let cotViewModel = self.cotViewModel, Settings.shared.isListening { 234 | // Only check if network is available and we should be listening 235 | if hasActiveConnection { 236 | // Add ZMQHandler checkAlive" method TODO 237 | cotViewModel.checkConnectionStatus() 238 | } 239 | } 240 | } 241 | 242 | func isNetworkAvailable() -> Bool { 243 | return hasActiveConnection 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /WarDragon/Data Handling/Storage/DroneInfoEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DroneInfoEditor.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 4/6/25. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct DroneInfoEditor: View { 12 | let droneId: String 13 | @State private var customName: String 14 | @State private var trustStatus: DroneSignature.UserDefinedInfo.TrustStatus 15 | @Environment(\.dismiss) private var dismiss 16 | @ObservedObject var droneStorage = DroneStorageManager.shared 17 | 18 | init(droneId: String) { 19 | self.droneId = droneId 20 | let encounter = DroneStorageManager.shared.encounters[droneId] 21 | _customName = State(initialValue: encounter?.customName ?? "") 22 | _trustStatus = State(initialValue: encounter?.trustStatus ?? .unknown) 23 | } 24 | 25 | var body: some View { 26 | VStack(alignment: .leading, spacing: 16) { 27 | TextField("Drone Name", text: $customName) 28 | .textFieldStyle(RoundedBorderTextFieldStyle()) 29 | .font(.appDefault) 30 | .padding(.bottom, 8) 31 | 32 | Text("Trust Status") 33 | .font(.appSubheadline) 34 | .padding(.bottom, 4) 35 | 36 | HStack(spacing: 16) { 37 | TrustButton( 38 | title: "Trusted", 39 | icon: "checkmark.shield.fill", 40 | color: .green, 41 | isSelected: trustStatus == .trusted, 42 | action: { trustStatus = .trusted } 43 | ) 44 | 45 | TrustButton( 46 | title: "Unknown", 47 | icon: "shield.fill", 48 | color: .gray, 49 | isSelected: trustStatus == .unknown, 50 | action: { trustStatus = .unknown } 51 | ) 52 | 53 | TrustButton( 54 | title: "Untrusted", 55 | icon: "xmark.shield.fill", 56 | color: .red, 57 | isSelected: trustStatus == .untrusted, 58 | action: { trustStatus = .untrusted } 59 | ) 60 | } 61 | .padding(.bottom, 16) 62 | 63 | Button(action: saveChanges) { 64 | Text("Save") 65 | .frame(maxWidth: .infinity) 66 | .padding() 67 | .background(Color.blue) 68 | .foregroundColor(.white) 69 | .cornerRadius(8) 70 | } 71 | 72 | Spacer() 73 | } 74 | .padding() 75 | } 76 | 77 | private func saveChanges() { 78 | // Use the proper method in DroneStorageManager 79 | DroneStorageManager.shared.updateDroneInfo( 80 | id: droneId, 81 | name: customName, 82 | trustStatus: trustStatus 83 | ) 84 | dismiss() 85 | } 86 | 87 | } 88 | 89 | struct TrustButton: View { 90 | let title: String 91 | let icon: String 92 | let color: Color 93 | let isSelected: Bool 94 | let action: () -> Void 95 | 96 | var body: some View { 97 | Button(action: action) { 98 | VStack { 99 | Image(systemName: icon) 100 | .font(.system(size: 24)) 101 | .foregroundColor(isSelected ? .white : color) 102 | Text(title) 103 | .font(.appCaption) 104 | .foregroundColor(isSelected ? .white : color) 105 | } 106 | .frame(maxWidth: .infinity) 107 | .padding(.vertical, 12) 108 | .background(isSelected ? color : Color.clear) 109 | .cornerRadius(8) 110 | .overlay( 111 | RoundedRectangle(cornerRadius: 8) 112 | .stroke(color, lineWidth: 2) 113 | ) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /WarDragon/FAA Lookup/FAAInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FAAInfoView.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 4/25/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FAAInfoView: View { 11 | let faaData: [String: Any] 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 8) { 15 | Text("FAA REGISTRATION") 16 | .font(.appHeadline) 17 | .frame(maxWidth: .infinity, alignment: .center) 18 | .padding(.bottom, 5) 19 | 20 | if let items = faaData["items"] as? [[String: Any]], 21 | let firstItem = items.first { 22 | InfoRow(title: "Status", value: firstItem["status"] as? String ?? "Unknown") 23 | InfoRow(title: "Brand", value: firstItem["brand"] as? String ?? "Unknown") 24 | InfoRow(title: "Model", value: firstItem["model"] as? String ?? "Unknown") 25 | InfoRow(title: "Manufacturer Code", value: firstItem["manufacturerCode"] as? String ?? "Unknown") 26 | InfoRow(title: "Product Type", value: firstItem["productType"] as? String ?? "Unknown") 27 | InfoRow(title: "Operation Rules", value: firstItem["operationRules"] as? String ?? "Unknown") 28 | } else if let data = faaData["data"] as? [String: Any], 29 | let items = data["items"] as? [[String: Any]], 30 | let firstItem = items.first { 31 | InfoRow(title: "Make", value: firstItem["makeName"] as? String ?? "Unknown") 32 | InfoRow(title: "Model", value: firstItem["modelName"] as? String ?? "Unknown") 33 | InfoRow(title: "Series", value: firstItem["series"] as? String ?? "Unknown") 34 | InfoRow(title: "Remote ID", value: firstItem["trackingNumber"] as? String ?? "Unknown") 35 | InfoRow(title: "Compliance", value: firstItem["complianceCategories"] as? String ?? "Unknown") 36 | InfoRow(title: "Updated", value: firstItem["updatedAt"] as? String ?? "Unknown") 37 | } else { 38 | VStack { 39 | Text("No registration data found") 40 | .font(.appCaption) 41 | .foregroundColor(.secondary) 42 | .padding(.bottom, 5) 43 | 44 | Text("Response Structure:") 45 | .font(.appCaption) 46 | .foregroundColor(.secondary) 47 | Text(debugDescription(for: faaData)) 48 | .font(.appCaption) 49 | .foregroundColor(.red) 50 | } 51 | .frame(maxWidth: .infinity, alignment: .center) 52 | .padding() 53 | } 54 | } 55 | .padding() 56 | .background(Color(UIColor.secondarySystemBackground)) 57 | .cornerRadius(12) 58 | } 59 | 60 | private func debugDescription(for data: [String: Any]) -> String { 61 | var description = "" 62 | for (key, value) in data { 63 | if let dictValue = value as? [String: Any] { 64 | description += "\(key): [dictionary: \(dictValue.count) items]\n" 65 | } else if let arrayValue = value as? [[String: Any]] { 66 | description += "\(key): [array: \(arrayValue.count) items]\n" 67 | } else { 68 | description += "\(key): \(type(of: value))\n" 69 | } 70 | } 71 | return String(description.prefix(300)) + (description.count > 300 ? "..." : "") 72 | } 73 | } 74 | 75 | struct InfoRow: View { 76 | let title: String 77 | let value: String 78 | 79 | var body: some View { 80 | HStack(alignment: .top) { 81 | Text(title) 82 | .font(.appHeadline) 83 | .foregroundColor(.secondary) 84 | .layoutPriority(1) 85 | 86 | Spacer() 87 | 88 | Text(value) 89 | .font(.appCaption) 90 | .foregroundColor(.primary) 91 | .multilineTextAlignment(.trailing) 92 | .lineLimit(nil) 93 | } 94 | .frame(minHeight: 20) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /WarDragon/FAA Lookup/FAAService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FAAService.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 4/25/25. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class FAAService: ObservableObject { 12 | static let shared = FAAService() 13 | 14 | @Published var isFetching = false 15 | @Published var error: String? 16 | 17 | private let faaBaseURL = "https://uasdoc.faa.gov" 18 | private let faaAPIEndpoint = "/api/v1/serialNumbers" 19 | 20 | // Create a session with custom headers 21 | private lazy var session: URLSession = { 22 | let config = URLSessionConfiguration.default 23 | config.httpAdditionalHeaders = [ 24 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0", 25 | "Accept": "application/json, text/plain, */*", 26 | "Accept-Language": "en-US,en;q=0.5", 27 | "Referer": "https://uasdoc.faa.gov/listdocs", 28 | "client": "external" 29 | ] 30 | config.timeoutIntervalForRequest = 30 31 | config.timeoutIntervalForResource = 30 32 | 33 | // Enable cookie handling 34 | config.httpCookieStorage = HTTPCookieStorage.shared 35 | config.httpCookieAcceptPolicy = .always 36 | config.httpShouldSetCookies = true 37 | 38 | return URLSession(configuration: config) 39 | }() 40 | 41 | // Refresh the FAA cookie by visiting the homepage first 42 | private func refreshCookie() async throws { 43 | let homepageURL = URL(string: "\(faaBaseURL)/listdocs")! 44 | do { 45 | // Clear existing cookies first 46 | if let cookies = HTTPCookieStorage.shared.cookies(for: homepageURL) { 47 | for cookie in cookies { 48 | HTTPCookieStorage.shared.deleteCookie(cookie) 49 | } 50 | } 51 | 52 | // Make request to homepage to get new cookie 53 | var request = URLRequest(url: homepageURL) 54 | request.httpMethod = "GET" 55 | 56 | let (_, response) = try await session.data(for: request) 57 | if let httpResponse = response as? HTTPURLResponse { 58 | print("FAA homepage response code: \(httpResponse.statusCode)") 59 | 60 | // Log cookies for debugging 61 | if let cookies = HTTPCookieStorage.shared.cookies(for: homepageURL) { 62 | print("Got \(cookies.count) cookies from FAA homepage") 63 | for cookie in cookies { 64 | print("Cookie: \(cookie.name) = \(cookie.value)") 65 | } 66 | } 67 | } 68 | } catch { 69 | print("Error refreshing FAA cookie: \(error)") 70 | throw error 71 | } 72 | } 73 | 74 | func queryFAAData(mac: String, remoteId: String) async -> [String: Any]? { 75 | guard !remoteId.isEmpty else { 76 | DispatchQueue.main.async { 77 | self.error = "Remote ID is empty" 78 | } 79 | return nil 80 | } 81 | 82 | // Check cache first 83 | if let cachedData = checkCache(mac: mac, remoteId: remoteId) { 84 | print("FAA Cache Hit") 85 | return cachedData 86 | } 87 | 88 | DispatchQueue.main.async { 89 | self.isFetching = true 90 | self.error = nil 91 | } 92 | 93 | var retryCount = 0 94 | let maxRetries = 3 95 | 96 | while retryCount < maxRetries { 97 | do { 98 | // Refresh cookie before each attempt 99 | try await refreshCookie() 100 | 101 | // Small delay to ensure cookie is properly set 102 | try await Task.sleep(nanoseconds: 500_000_000) 103 | 104 | // Build the FAA query URL 105 | var components = URLComponents(string: "\(faaBaseURL)\(faaAPIEndpoint)")! 106 | components.queryItems = [ 107 | URLQueryItem(name: "itemsPerPage", value: "8"), 108 | URLQueryItem(name: "pageIndex", value: "0"), 109 | URLQueryItem(name: "orderBy[0]", value: "updatedAt"), 110 | URLQueryItem(name: "orderBy[1]", value: "DESC"), 111 | URLQueryItem(name: "findBy", value: "serialNumber"), 112 | URLQueryItem(name: "serialNumber", value: remoteId) 113 | ] 114 | 115 | guard let url = components.url else { 116 | throw NSError(domain: "FAAService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]) 117 | } 118 | 119 | print("FAA Request URL: \(url.absoluteString)") 120 | 121 | // Create request with proper headers 122 | var request = URLRequest(url: url) 123 | request.httpMethod = "GET" 124 | 125 | // Make the request 126 | let (data, response) = try await session.data(for: request) 127 | 128 | if let httpResponse = response as? HTTPURLResponse { 129 | print("FAA Response status: \(httpResponse.statusCode)") 130 | 131 | switch httpResponse.statusCode { 132 | case 502: 133 | // Handle 502 Proxy Error specifically 134 | if retryCount < maxRetries - 1 { 135 | retryCount += 1 136 | print("502 Proxy Error, retrying in \(Double(retryCount) * 2) seconds...") 137 | try await Task.sleep(nanoseconds: UInt64(Double(retryCount) * 2_000_000_000)) 138 | continue 139 | } else { 140 | throw NSError(domain: "FAAService", 141 | code: 502, 142 | userInfo: [NSLocalizedDescriptionKey: "The FAA service is temporarily unavailable (502 Proxy Error). Please try again later."]) 143 | } 144 | case 200: 145 | // Success 146 | if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { 147 | cacheResult(mac: mac, remoteId: remoteId, data: json) 148 | DispatchQueue.main.async { 149 | self.isFetching = false 150 | } 151 | return json 152 | } 153 | default: 154 | // Other HTTP errors 155 | throw NSError(domain: "FAAService", 156 | code: httpResponse.statusCode, 157 | userInfo: [NSLocalizedDescriptionKey: "FAA HTTP error: \(httpResponse.statusCode)"]) 158 | } 159 | } 160 | 161 | } catch { 162 | if retryCount < maxRetries - 1 { 163 | retryCount += 1 164 | print("Error on attempt \(retryCount): \(error.localizedDescription)") 165 | try? await Task.sleep(nanoseconds: UInt64(Double(retryCount) * 2_000_000_000)) 166 | continue 167 | } else { 168 | DispatchQueue.main.async { 169 | self.isFetching = false 170 | self.error = error.localizedDescription 171 | } 172 | return nil 173 | } 174 | } 175 | } 176 | 177 | DispatchQueue.main.async { 178 | self.isFetching = false 179 | } 180 | 181 | return nil 182 | } 183 | 184 | // Cache functions 185 | private func cacheResult(mac: String, remoteId: String, data: [String: Any]) { 186 | let key = "\(mac)_\(remoteId)" 187 | UserDefaults.standard.set(data, forKey: "faa_cache_\(key)") 188 | } 189 | 190 | private func checkCache(mac: String, remoteId: String) -> [String: Any]? { 191 | let key = "\(mac)_\(remoteId)" 192 | return UserDefaults.standard.dictionary(forKey: "faa_cache_\(key)") 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /WarDragon/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIBackgroundModes 6 | 7 | fetch 8 | processing 9 | remote-notification 10 | 11 | BGTaskSchedulerPermittedIdentifiers 12 | 13 | com.wardragon.processMessages 14 | com.wardragon.updateStatus 15 | com.wardragon.refreshConnections 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /WarDragon/Maps/MapView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapView.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 11/18/24. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | struct MapView: View { 12 | let message: CoTViewModel.CoTMessage 13 | @ObservedObject var cotViewModel: CoTViewModel 14 | @State private var region: MKCoordinateRegion 15 | 16 | init(message: CoTViewModel.CoTMessage, cotViewModel: CoTViewModel) { 17 | self.message = message 18 | self.cotViewModel = cotViewModel 19 | 20 | let lat = Double(message.lat) ?? 0 21 | let lon = Double(message.lon) ?? 0 22 | let droneId = message.uid 23 | 24 | // Initialize with default region 25 | let defaultRegion = MKCoordinateRegion( 26 | center: CLLocationCoordinate2D(latitude: lat, longitude: lon), 27 | span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) 28 | ) 29 | 30 | _region = State(initialValue: defaultRegion) 31 | } 32 | 33 | var body: some View { 34 | Map { 35 | // Show marker only if coordinates are not 0,0 36 | if let coordinate = message.coordinate, 37 | coordinate.latitude != 0 || coordinate.longitude != 0 { 38 | Marker(message.uid, coordinate: coordinate) 39 | } 40 | 41 | // Now safely access cotViewModel since we're in the body 42 | if let ring = cotViewModel.alertRings.first(where: { $0.droneId == message.uid }), 43 | Double(message.lat) ?? 0 == 0 && Double(message.lon) ?? 0 == 0 { 44 | MapCircle(center: ring.centerCoordinate, radius: ring.radius) 45 | .foregroundStyle(.yellow.opacity(0.1)) 46 | .stroke(.yellow, lineWidth: 2) 47 | 48 | Marker("\(Int(ring.radius))m", coordinate: ring.centerCoordinate) 49 | .tint(.yellow) 50 | } 51 | } 52 | .frame(height: 200) 53 | .onAppear { 54 | // Update the region if there's an alert ring 55 | if let ring = cotViewModel.alertRings.first(where: { $0.droneId == message.uid }), 56 | Double(message.lat) ?? 0 == 0 && Double(message.lon) ?? 0 == 0 { 57 | let newRegion = MKCoordinateRegion( 58 | center: ring.centerCoordinate, 59 | span: MKCoordinateSpan( 60 | latitudeDelta: max(ring.radius / 1000 * 2, 0.01), 61 | longitudeDelta: max(ring.radius / 1000 * 2, 0.01) 62 | ) 63 | ) 64 | DispatchQueue.main.async { 65 | region = newRegion 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /WarDragon/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /WarDragon/Settings/Settings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Settings.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 11/23/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | enum ConnectionMode: String, Codable, CaseIterable { 12 | case multicast = "Multicast" 13 | case zmq = "Direct ZMQ" 14 | // case both = "Both" 15 | 16 | var icon: String { 17 | switch self { 18 | case .multicast: 19 | return "antenna.radiowaves.left.and.right" 20 | case .zmq: 21 | return "network" 22 | } 23 | } 24 | } 25 | 26 | //MARK: - Local stored vars (nothing sensitive) 27 | 28 | class Settings: ObservableObject { 29 | static let shared = Settings() 30 | 31 | @AppStorage("connectionMode") var connectionMode: ConnectionMode = .multicast { 32 | didSet { 33 | objectWillChange.send() 34 | } 35 | } 36 | @AppStorage("zmqHost") var zmqHost: String = "0.0.0.0" { 37 | didSet { 38 | objectWillChange.send() 39 | } 40 | } 41 | @AppStorage("multicastHost") var multicastHost: String = "224.0.0.1" { 42 | didSet { 43 | objectWillChange.send() 44 | } 45 | } 46 | @AppStorage("notificationsEnabled") var notificationsEnabled = true { 47 | didSet { 48 | objectWillChange.send() 49 | } 50 | } 51 | @AppStorage("keepScreenOn") var keepScreenOn = false { 52 | didSet { 53 | objectWillChange.send() 54 | UIApplication.shared.isIdleTimerDisabled = keepScreenOn 55 | } 56 | } 57 | @AppStorage("enableBackgroundDetection") var enableBackgroundDetection = false { 58 | didSet { 59 | objectWillChange.send() 60 | } 61 | } 62 | @AppStorage("multicastPort") var multicastPort: Int = 6969 { 63 | didSet { 64 | objectWillChange.send() 65 | } 66 | } 67 | @AppStorage("zmqTelemetryPort") var zmqTelemetryPort: Int = 4224 { 68 | didSet { 69 | objectWillChange.send() 70 | } 71 | } 72 | @AppStorage("zmqStatusPort") var zmqStatusPort: Int = 4225 { 73 | didSet { 74 | objectWillChange.send() 75 | } 76 | } 77 | @AppStorage("isListening") var isListening = false { 78 | didSet { 79 | objectWillChange.send() 80 | } 81 | } 82 | @AppStorage("spoofDetectionEnabled") var spoofDetectionEnabled = true { 83 | didSet { 84 | objectWillChange.send() 85 | } 86 | } 87 | @AppStorage("zmqSpectrumPort") var zmqSpectrumPort: Int = 4226 { 88 | didSet { 89 | objectWillChange.send() 90 | } 91 | } 92 | @AppStorage("zmqHostHistory") var zmqHostHistoryJson: String = "[]" { 93 | didSet { 94 | objectWillChange.send() 95 | } 96 | } 97 | @AppStorage("multicastHostHistory") var multicastHostHistoryJson: String = "[]" { 98 | didSet { 99 | objectWillChange.send() 100 | } 101 | } 102 | //MARK: - Warning Thresholds 103 | @AppStorage("cpuWarningThreshold") var cpuWarningThreshold: Double = 80.0 { // 80% CPU 104 | didSet { 105 | objectWillChange.send() 106 | } 107 | } 108 | 109 | @AppStorage("tempWarningThreshold") var tempWarningThreshold: Double = 70.0 { // 70°C 110 | didSet { 111 | objectWillChange.send() 112 | } 113 | } 114 | 115 | @AppStorage("memoryWarningThreshold") var memoryWarningThreshold: Double = 0.85 { // 85% 116 | didSet { 117 | objectWillChange.send() 118 | } 119 | } 120 | 121 | @AppStorage("plutoTempThreshold") var plutoTempThreshold: Double = 85.0 { // 85°C 122 | didSet { 123 | objectWillChange.send() 124 | } 125 | } 126 | 127 | @AppStorage("zynqTempThreshold") var zynqTempThreshold: Double = 85.0 { // 85°C 128 | didSet { 129 | objectWillChange.send() 130 | } 131 | } 132 | 133 | @AppStorage("proximityThreshold") var proximityThreshold: Int = -60 { // -60 dBm 134 | didSet { 135 | objectWillChange.send() 136 | } 137 | } 138 | 139 | @AppStorage("enableWarnings") var enableWarnings = true { 140 | didSet { 141 | objectWillChange.send() 142 | } 143 | } 144 | 145 | @AppStorage("systemWarningsEnabled") var systemWarningsEnabled = true { 146 | didSet { 147 | objectWillChange.send() 148 | } 149 | } 150 | @AppStorage("enableProximityWarnings") var enableProximityWarnings = true 151 | @AppStorage("messageProcessingInterval") var messageProcessingInterval: Int = 100 152 | 153 | //MARK: - Connection 154 | 155 | private init() { 156 | toggleListening(false) 157 | UIApplication.shared.isIdleTimerDisabled = keepScreenOn 158 | } 159 | 160 | func updateConnection(mode: ConnectionMode, host: String? = nil, isZmqHost: Bool = false) { 161 | if let host = host { 162 | if isZmqHost { 163 | zmqHost = host 164 | updateConnectionHistory(host: host, isZmq: true) 165 | } else { 166 | multicastHost = host 167 | updateConnectionHistory(host: host, isZmq: false) 168 | } 169 | } 170 | 171 | connectionMode = mode 172 | } 173 | 174 | func isHostConfigurationValid() -> Bool { 175 | switch connectionMode { 176 | case .multicast: 177 | return !multicastHost.isEmpty 178 | case .zmq: 179 | return !zmqHost.isEmpty 180 | } 181 | } 182 | 183 | func toggleListening(_ active: Bool) { 184 | if active == isListening { 185 | return 186 | } 187 | 188 | isListening = active 189 | objectWillChange.send() 190 | 191 | // Start or stop background processing if enabled 192 | if isListening && enableBackgroundDetection { 193 | BackgroundManager.shared.startBackgroundProcessing() 194 | } else if !isListening { 195 | BackgroundManager.shared.stopBackgroundProcessing() 196 | } 197 | } 198 | 199 | var zmqHostHistory: [String] { 200 | get { 201 | if let data = zmqHostHistoryJson.data(using: .utf8), 202 | let array = try? JSONDecoder().decode([String].self, from: data) { 203 | return array 204 | } 205 | return [] 206 | } 207 | set { 208 | if let data = try? JSONEncoder().encode(newValue), 209 | let json = String(data: data, encoding: .utf8) { 210 | zmqHostHistoryJson = json 211 | } 212 | } 213 | } 214 | 215 | var multicastHostHistory: [String] { 216 | get { 217 | if let data = multicastHostHistoryJson.data(using: .utf8), 218 | let array = try? JSONDecoder().decode([String].self, from: data) { 219 | return array 220 | } 221 | return [] 222 | } 223 | set { 224 | if let data = try? JSONEncoder().encode(newValue), 225 | let json = String(data: data, encoding: .utf8) { 226 | multicastHostHistoryJson = json 227 | } 228 | } 229 | } 230 | 231 | func updateConnectionHistory(host: String, isZmq: Bool) { 232 | if isZmq { 233 | var history = zmqHostHistory 234 | history.removeAll { $0 == host } 235 | history.insert(host, at: 0) 236 | if history.count > 5 { 237 | history = Array(history.prefix(5)) 238 | } 239 | zmqHostHistory = history 240 | } else { 241 | var history = multicastHostHistory 242 | history.removeAll { $0 == host } 243 | history.insert(host, at: 0) 244 | if history.count > 5 { 245 | history = Array(history.prefix(5)) 246 | } 247 | multicastHostHistory = history 248 | } 249 | } 250 | 251 | var messageProcessingIntervalSeconds: Double { 252 | Double(messageProcessingInterval) / 1000.0 253 | } 254 | 255 | func updatePreferences(notifications: Bool, screenOn: Bool) { 256 | notificationsEnabled = notifications 257 | keepScreenOn = screenOn 258 | } 259 | 260 | func updateWarningThresholds( 261 | cpu: Double? = nil, 262 | temp: Double? = nil, 263 | memory: Double? = nil, 264 | plutoTemp: Double? = nil, 265 | zynqTemp: Double? = nil, 266 | proximity: Int? = nil 267 | ) { 268 | if let cpu = cpu { cpuWarningThreshold = cpu } 269 | if let temp = temp { tempWarningThreshold = temp } 270 | if let memory = memory { memoryWarningThreshold = memory } 271 | if let plutoTemp = plutoTemp { plutoTempThreshold = plutoTemp } 272 | if let zynqTemp = zynqTemp { zynqTempThreshold = zynqTemp } 273 | if let proximity = proximity { proximityThreshold = proximity } 274 | objectWillChange.send() 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /WarDragon/Status/StatusViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusViewModel.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 11/18/24. 6 | // 7 | 8 | import Foundation 9 | import CoreLocation 10 | import SwiftUI 11 | 12 | class StatusViewModel: ObservableObject { 13 | @Published var statusMessages: [StatusMessage] = [] 14 | 15 | struct StatusMessage: Identifiable { 16 | var id: String { uid } 17 | let uid: String 18 | var serialNumber: String 19 | var timestamp: Double 20 | var gpsData: GPSData 21 | var systemStats: SystemStats 22 | var antStats: ANTStats 23 | 24 | 25 | struct GPSData { 26 | var latitude: Double 27 | var longitude: Double 28 | var altitude: Double 29 | var speed: Double 30 | 31 | var coordinate: CLLocationCoordinate2D { 32 | CLLocationCoordinate2D(latitude: latitude, longitude: longitude) 33 | } 34 | } 35 | 36 | struct SystemStats { 37 | var cpuUsage: Double 38 | var memory: MemoryStats 39 | var disk: DiskStats 40 | var temperature: Double 41 | var uptime: Double 42 | 43 | struct MemoryStats { 44 | var total: Int64 45 | var available: Int64 46 | var percent: Double 47 | var used: Int64 48 | var free: Int64 49 | var active: Int64 50 | var inactive: Int64 51 | var buffers: Int64 52 | var cached: Int64 53 | var shared: Int64 54 | var slab: Int64 55 | } 56 | 57 | struct DiskStats { 58 | var total: Int64 59 | var used: Int64 60 | var free: Int64 61 | var percent: Double 62 | } 63 | } 64 | 65 | struct ANTStats { 66 | var plutoTemp: Double 67 | var zynqTemp: Double 68 | } 69 | } 70 | } 71 | 72 | extension StatusViewModel { 73 | func checkSystemThresholds() { 74 | guard Settings.shared.systemWarningsEnabled, 75 | let lastMessage = statusMessages.last else { 76 | return 77 | } 78 | 79 | // Check CPU usage 80 | if lastMessage.systemStats.cpuUsage > Settings.shared.cpuWarningThreshold { 81 | sendSystemNotification( 82 | title: "High CPU Usage", 83 | message: "CPU usage at \(Int(lastMessage.systemStats.cpuUsage))%" 84 | ) 85 | } 86 | 87 | // Check system temperature 88 | if lastMessage.systemStats.temperature > Settings.shared.tempWarningThreshold { 89 | sendSystemNotification( 90 | title: "High System Temperature", 91 | message: "Temperature at \(Int(lastMessage.systemStats.temperature))°C" 92 | ) 93 | } 94 | 95 | // Check memory usage 96 | let memoryUsage = Double(lastMessage.systemStats.memory.used) / Double(lastMessage.systemStats.memory.total) 97 | if memoryUsage > Settings.shared.memoryWarningThreshold { 98 | sendSystemNotification( 99 | title: "High Memory Usage", 100 | message: "Memory usage at \(Int(memoryUsage * 100))%" 101 | ) 102 | } 103 | 104 | // Check ANTSDR temperatures 105 | if lastMessage.antStats.plutoTemp > Settings.shared.plutoTempThreshold { 106 | sendSystemNotification( 107 | title: "High Pluto Temperature", 108 | message: "Temperature at \(Int(lastMessage.antStats.plutoTemp))°C" 109 | ) 110 | } 111 | 112 | if lastMessage.antStats.zynqTemp > Settings.shared.zynqTempThreshold { 113 | sendSystemNotification( 114 | title: "High Zynq Temperature", 115 | message: "Temperature at \(Int(lastMessage.antStats.zynqTemp))°C" 116 | ) 117 | } 118 | } 119 | 120 | private func sendSystemNotification(title: String, message: String) { 121 | guard Settings.shared.notificationsEnabled else { return } 122 | 123 | let content = UNMutableNotificationContent() 124 | content.title = title 125 | content.body = message 126 | 127 | let request = UNNotificationRequest( 128 | identifier: UUID().uuidString, 129 | content: content, 130 | trigger: nil 131 | ) 132 | 133 | UNUserNotificationCenter.current().add(request) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /WarDragon/UI/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 11/18/24. 6 | // 7 | 8 | import SwiftUI 9 | import Network 10 | import UserNotifications 11 | 12 | struct ContentView: View { 13 | @StateObject private var statusViewModel = StatusViewModel() 14 | @StateObject private var spectrumViewModel = SpectrumData.SpectrumViewModel() 15 | @StateObject private var droneStorage = DroneStorageManager.shared 16 | @StateObject private var cotViewModel: CoTViewModel 17 | @StateObject private var settings = Settings.shared 18 | @State private var showAlert = false 19 | @State private var latestMessage: CoTViewModel.CoTMessage? 20 | @State private var selectedTab: Int 21 | 22 | 23 | init() { 24 | // Create temporary non-StateObject instances for initialization 25 | let statusVM = StatusViewModel() 26 | let cotVM = CoTViewModel(statusViewModel: statusVM) 27 | 28 | // Initialize the StateObject properties 29 | self._statusViewModel = StateObject(wrappedValue: statusVM) 30 | self._cotViewModel = StateObject(wrappedValue: cotVM) 31 | self._selectedTab = State(initialValue: Settings.shared.isListening ? 0 : 3) 32 | 33 | // Configure background manager with the created instance, not the StateObject wrapper 34 | BackgroundManager.shared.configure(with: cotVM) 35 | 36 | // Add lightweight connection check listener 37 | NotificationCenter.default.addObserver( 38 | forName: NSNotification.Name("LightweightConnectionCheck"), 39 | object: nil, 40 | queue: .main 41 | ) { [weak cotVM] _ in 42 | cotVM?.checkConnectionStatus() 43 | } 44 | 45 | // Add notification for background task expiry 46 | NotificationCenter.default.addObserver( 47 | forName: NSNotification.Name("BackgroundTaskExpiring"), 48 | object: nil, 49 | queue: .main 50 | ) { [weak cotVM] _ in 51 | // Perform urgent cleanup when background task is about to expire 52 | cotVM?.prepareForBackgroundExpiry() 53 | } 54 | } 55 | 56 | 57 | var body: some View { 58 | TabView(selection: $selectedTab) { 59 | NavigationStack { 60 | DashboardView( 61 | statusViewModel: statusViewModel, 62 | cotViewModel: cotViewModel, 63 | spectrumViewModel: spectrumViewModel 64 | ) 65 | .navigationTitle("Dashboard") 66 | } 67 | .tabItem { 68 | Label("Dashboard", systemImage: "gauge") 69 | } 70 | .tag(0) 71 | 72 | NavigationStack { 73 | VStack { 74 | ScrollViewReader { proxy in 75 | List(cotViewModel.parsedMessages) { item in 76 | MessageRow(message: item, cotViewModel: cotViewModel) 77 | } 78 | .listStyle(.inset) 79 | .onChange(of: cotViewModel.parsedMessages) { oldMessages, newMessages in 80 | // Only proceed if we have more messages than before 81 | if oldMessages.count < newMessages.count { 82 | // Get the newest message 83 | if let latest = newMessages.last { 84 | // Check if this message ID wasn't in the old messages 85 | if !oldMessages.contains(where: { $0.id == latest.id }) { 86 | latestMessage = latest 87 | showAlert = false 88 | withAnimation { 89 | proxy.scrollTo(latest.id, anchor: .bottom) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | .navigationTitle("DragonSync") 98 | .toolbar { 99 | ToolbarItem(placement: .topBarTrailing) { 100 | Menu { 101 | Button(action: { 102 | cotViewModel.parsedMessages.removeAll() 103 | cotViewModel.droneSignatures.removeAll() 104 | cotViewModel.macIdHistory.removeAll() 105 | cotViewModel.macProcessing.removeAll() 106 | cotViewModel.alertRings.removeAll() 107 | }) { 108 | Label("Clear All", systemImage: "trash") 109 | } 110 | 111 | // Add option to clear just active tracking but keep history 112 | Button(action: { 113 | cotViewModel.parsedMessages.removeAll() 114 | cotViewModel.droneSignatures.removeAll() 115 | cotViewModel.alertRings.removeAll() 116 | }) { 117 | Label("Stop All Tracking", systemImage: "eye.slash") 118 | } 119 | 120 | // Add option to delete all from history 121 | Button(role: .destructive, action: { 122 | droneStorage.deleteAllEncounters() 123 | cotViewModel.parsedMessages.removeAll() 124 | cotViewModel.droneSignatures.removeAll() 125 | cotViewModel.macIdHistory.removeAll() 126 | cotViewModel.macProcessing.removeAll() 127 | cotViewModel.alertRings.removeAll() 128 | }) { 129 | Label("Delete All History", systemImage: "trash.fill") 130 | } 131 | } label: { 132 | Image(systemName: "ellipsis.circle") 133 | } 134 | } 135 | } 136 | .alert("New Message", isPresented: $showAlert) { 137 | Button("OK", role: .cancel) {} 138 | } message: { 139 | if let message = latestMessage { 140 | Text("From: \(message.uid)\nType: \(message.type)\nLocation: \(message.lat), \(message.lon)") 141 | } 142 | } 143 | } 144 | 145 | .tabItem { 146 | Label("Drones", systemImage: "airplane.circle") 147 | } 148 | .tag(1) 149 | 150 | NavigationStack { 151 | StatusListView(statusViewModel: statusViewModel) 152 | .toolbar { 153 | ToolbarItem(placement: .topBarTrailing) { 154 | Button(action: { statusViewModel.statusMessages.removeAll() }) { 155 | Image(systemName: "trash") 156 | } 157 | } 158 | } 159 | } 160 | .tabItem { 161 | Label("Status", systemImage: "server.rack") 162 | } 163 | .tag(2) 164 | 165 | NavigationStack { 166 | SettingsView(cotHandler: cotViewModel) 167 | } 168 | .tabItem { 169 | Label("Settings", systemImage: "gear") 170 | } 171 | .tag(3) 172 | NavigationStack { 173 | StoredEncountersView(cotViewModel: cotViewModel) 174 | } 175 | .tabItem { 176 | Label("History", systemImage: "clock.arrow.circlepath") 177 | } 178 | .tag(4) 179 | 180 | // NavigationStack { 181 | // SpectrumView(viewModel: spectrumViewModel) 182 | // .navigationTitle("Spectrum") 183 | // } 184 | // .tabItem { 185 | // Label("Spectrum", systemImage: "waveform") 186 | // } 187 | // .tag(4) 188 | } 189 | 190 | .onChange(of: settings.isListening) { 191 | if settings.isListening { 192 | cotViewModel.startListening() 193 | } else { 194 | cotViewModel.stopListening() 195 | } 196 | } 197 | .onChange(of: selectedTab) { oldValue, newValue in 198 | if newValue != 3 { // Spectrum tab 199 | // spectrumViewModel.stopListening() 200 | } else if settings.isListening { 201 | let port = UInt16(UserDefaults.standard.integer(forKey: "spectrumPort")) 202 | // spectrumViewModel.startListening(port: port) 203 | } 204 | } 205 | .onChange(of: settings.connectionMode) { 206 | if settings.isListening { 207 | // Handle switch when enabled, for now just do not allow 208 | } 209 | } 210 | } 211 | 212 | 213 | } 214 | -------------------------------------------------------------------------------- /WarDragon/UI/FAALookupButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FAALookupButton.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 4/25/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | struct FAALookupButton: View { 12 | let mac: String? 13 | let remoteId: String? 14 | @StateObject private var faaService = FAAService.shared 15 | @State private var showingFAAInfo = false 16 | @State private var showingError = false 17 | @State private var faaData: [String: Any]? 18 | @State private var isLoading = false 19 | 20 | var body: some View { 21 | Group { 22 | if let mac = mac, let remoteId = remoteId { 23 | Button(action: { 24 | isLoading = true 25 | Task { 26 | if let data = await faaService.queryFAAData(mac: mac, remoteId: remoteId) { 27 | faaData = data 28 | showingFAAInfo = true 29 | } else if let error = faaService.error { 30 | showingError = true 31 | } 32 | isLoading = false 33 | } 34 | }) { 35 | HStack { 36 | if isLoading { 37 | ProgressView() 38 | .progressViewStyle(CircularProgressViewStyle(tint: .white)) 39 | .scaleEffect(0.8) 40 | } else { 41 | Image(systemName: "airplane.departure") 42 | } 43 | Text(isLoading ? "Loading..." : "FAA Lookup") 44 | } 45 | .padding(.horizontal, 12) 46 | .padding(.vertical, 6) 47 | .background(isLoading ? Color.gray : Color.blue) 48 | .foregroundColor(.white) 49 | .cornerRadius(8) 50 | } 51 | .disabled(faaService.isFetching || isLoading) 52 | .alert("FAA Lookup Error", isPresented: $showingError) { 53 | Button("OK", role: .cancel) {} 54 | } message: { 55 | Text(faaService.error ?? "Unknown error occurred") 56 | } 57 | } 58 | } 59 | .sheet(isPresented: $showingFAAInfo) { 60 | if let data = faaData { 61 | ZStack { 62 | Color.clear 63 | VStack(spacing: 0) { 64 | HStack { 65 | Spacer() 66 | Button("Done") { 67 | showingFAAInfo = false 68 | } 69 | .padding(.trailing) 70 | .padding(.top, 8) 71 | } 72 | .background(Color.clear) 73 | 74 | // FAA Info View 75 | FAAInfoView(faaData: data) 76 | .padding(.horizontal) 77 | } 78 | } 79 | .background(Color.clear) 80 | .presentationDetents([.height(350)]) // TODO dont hardcode this 81 | .presentationBackground(.clear) 82 | .presentationDragIndicator(.visible) 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /WarDragon/UI/ServiceManagementView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceManagementView.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 1/10/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ServiceManagementView: View { 11 | @ObservedObject var viewModel: ServiceViewModel 12 | @Environment(\.colorScheme) var colorScheme 13 | @State private var selectedService: ServiceControl? 14 | @State private var showingActionSheet = false 15 | @State private var pendingAction: ServiceAction? 16 | 17 | init(viewModel: ServiceViewModel) { 18 | _viewModel = ObservedObject(wrappedValue: viewModel) 19 | } 20 | 21 | enum ServiceAction: Identifiable { 22 | case toggle 23 | case restart 24 | 25 | var id: String { 26 | switch self { 27 | case .toggle: return "toggle" 28 | case .restart: return "restart" 29 | } 30 | } 31 | 32 | var title: String { 33 | switch self { 34 | case .toggle: return "Toggle Service" 35 | case .restart: return "Restart Service" 36 | } 37 | } 38 | 39 | var message: String { 40 | switch self { 41 | case .toggle: return "Are you sure you want to toggle this service?" 42 | case .restart: return "Are you sure you want to restart this service?" 43 | } 44 | } 45 | } 46 | 47 | var body: some View { 48 | VStack(spacing: 0) { 49 | healthStatusBar 50 | 51 | ScrollView { 52 | VStack(spacing: 16) { 53 | if !viewModel.criticalServices().isEmpty { 54 | criticalServicesSection 55 | } 56 | 57 | ForEach(Array(viewModel.servicesByCategory().keys.sorted(by: { $0.rawValue < $1.rawValue })), id: \.self) { category in 58 | if let services = viewModel.servicesByCategory()[category] { 59 | serviceSection(category: category, services: services) 60 | } 61 | } 62 | } 63 | .padding() 64 | } 65 | } 66 | .background(Color(UIColor.systemBackground)) 67 | .onAppear { 68 | // viewModel.startMonitoring() 69 | } 70 | .onDisappear { 71 | // viewModel.stopMonitoring() 72 | } 73 | .alert("Error", isPresented: .init( 74 | get: { viewModel.error != nil }, 75 | set: { if !$0 { viewModel.error = nil } } 76 | )) { 77 | Button("OK", role: .cancel) {} 78 | } message: { 79 | if let error = viewModel.error { 80 | Text(error) 81 | } 82 | } 83 | .confirmationDialog( 84 | selectedService?.description ?? "", 85 | isPresented: $showingActionSheet, 86 | presenting: pendingAction 87 | ) { action in 88 | Button(action.title, role: .destructive) { 89 | if let service = selectedService { 90 | switch action { 91 | case .toggle: 92 | viewModel.toggleService(service) 93 | case .restart: 94 | viewModel.restartService(service) 95 | } 96 | } 97 | } 98 | Button("Cancel", role: .cancel) {} 99 | } message: { action in 100 | Text(action.message) 101 | } 102 | } 103 | 104 | private var healthStatusBar: some View { 105 | HStack(spacing: 12) { 106 | Circle() 107 | .fill(viewModel.healthReport?.statusColor ?? .gray) 108 | .frame(width: 12, height: 12) 109 | 110 | Text(viewModel.healthReport?.overallHealth.uppercased() ?? "NO CONNECTION") 111 | .font(.appHeadline) 112 | 113 | Spacer() 114 | 115 | Text(viewModel.healthReport?.timestamp.formatted(date: .omitted, time: .standard) ?? "") 116 | .font(.system(.caption, design: .monospaced)) 117 | .foregroundColor(.secondary) 118 | } 119 | .padding() 120 | .background(colorScheme == .dark ? Color.black : Color.white) 121 | .overlay( 122 | Rectangle() 123 | .frame(height: 1) 124 | .foregroundColor(.gray.opacity(0.3)), 125 | alignment: .bottom 126 | ) 127 | } 128 | 129 | private var criticalServicesSection: some View { 130 | VStack(alignment: .leading, spacing: 8) { 131 | Text("CRITICAL SERVICES") 132 | .font(.system(.caption, design: .monospaced)) 133 | .foregroundColor(.red) 134 | .padding(.horizontal, 4) 135 | 136 | ForEach(viewModel.criticalServices()) { service in 137 | ServiceRowView( 138 | service: service, 139 | viewModel: viewModel, 140 | selectedService: $selectedService, 141 | showingActionSheet: $showingActionSheet, 142 | pendingAction: $pendingAction 143 | ) 144 | } 145 | } 146 | .padding() 147 | .background( 148 | RoundedRectangle(cornerRadius: 12) 149 | .fill(Color.red.opacity(0.1)) 150 | .overlay( 151 | RoundedRectangle(cornerRadius: 12) 152 | .strokeBorder(Color.red.opacity(0.3), lineWidth: 1) 153 | ) 154 | ) 155 | } 156 | 157 | private func serviceSection(category: ServiceControl.ServiceCategory, services: [ServiceControl]) -> some View { 158 | VStack(alignment: .leading, spacing: 8) { 159 | HStack { 160 | Image(systemName: category.icon) 161 | Text(category.rawValue.uppercased()) 162 | .font(.system(.caption, design: .monospaced)) 163 | } 164 | .foregroundColor(category.color) 165 | .padding(.horizontal, 4) 166 | 167 | ForEach(services) { service in 168 | ServiceRowView( 169 | service: service, 170 | viewModel: viewModel, 171 | selectedService: $selectedService, 172 | showingActionSheet: $showingActionSheet, 173 | pendingAction: $pendingAction 174 | ) 175 | } 176 | } 177 | .padding() 178 | .background( 179 | RoundedRectangle(cornerRadius: 12) 180 | .fill(category.color.opacity(0.1)) 181 | .overlay( 182 | RoundedRectangle(cornerRadius: 12) 183 | .strokeBorder(category.color.opacity(0.3), lineWidth: 1) 184 | ) 185 | ) 186 | } 187 | } 188 | 189 | struct ServiceRowView: View { 190 | let service: ServiceControl 191 | @ObservedObject var viewModel: ServiceViewModel 192 | @Binding var selectedService: ServiceControl? 193 | @Binding var showingActionSheet: Bool 194 | @Binding var pendingAction: ServiceManagementView.ServiceAction? 195 | @State private var showDetails = false 196 | 197 | var body: some View { 198 | VStack(spacing: 4) { 199 | Button(action: { showDetails.toggle() }) { 200 | HStack { 201 | // Status indicator 202 | Circle() 203 | .fill(service.status.healthStatus.color) 204 | .frame(width: 8, height: 8) 205 | 206 | // Service name and description 207 | VStack(alignment: .leading) { 208 | Text(service.description) 209 | .font(.system(.body, design: .monospaced)) 210 | if !service.status.statusText.isEmpty { 211 | Text(service.status.statusText) 212 | .font(.system(.caption, design: .monospaced)) 213 | .foregroundColor(.secondary) 214 | } 215 | } 216 | 217 | Spacer() 218 | 219 | // Resource usage if available 220 | if let resources = service.resources { 221 | HStack(spacing: 8) { 222 | ResourceIndicator( 223 | value: resources.cpuPercent, 224 | icon: "cpu", 225 | color: resourceColor(percent: resources.cpuPercent) 226 | ) 227 | ResourceIndicator( 228 | value: resources.memoryPercent, 229 | icon: "memorychip", 230 | color: resourceColor(percent: resources.memoryPercent) 231 | ) 232 | } 233 | } 234 | 235 | // Active/Inactive toggle 236 | Button { 237 | selectedService = service 238 | pendingAction = .toggle 239 | showingActionSheet = true 240 | } label: { 241 | Image(systemName: service.status.isActive ? "checkmark.circle.fill" : "circle") 242 | .foregroundColor(service.status.isActive ? .green : .gray) 243 | } 244 | } 245 | } 246 | .buttonStyle(.plain) 247 | 248 | if showDetails { 249 | serviceDetails 250 | } 251 | } 252 | .padding(8) 253 | .background( 254 | RoundedRectangle(cornerRadius: 8) 255 | .fill(Color(UIColor.secondarySystemBackground)) 256 | ) 257 | } 258 | 259 | struct ResourceIndicator: View { 260 | let value: Double 261 | let icon: String 262 | let color: Color 263 | 264 | var body: some View { 265 | Label( 266 | String(format: "%.0f%%", value), 267 | systemImage: icon 268 | ) 269 | .font(.system(.caption, design: .monospaced)) 270 | .foregroundColor(color) 271 | } 272 | } 273 | 274 | private func resourceColor(percent: Double) -> Color { 275 | switch percent { 276 | case 0..<60: return .green 277 | case 60..<80: return .yellow 278 | default: return .red 279 | } 280 | } 281 | 282 | private var serviceDetails: some View { 283 | VStack(alignment: .leading, spacing: 8) { 284 | Group { 285 | detailRow("Service:", service.service) 286 | detailRow("Status:", service.status.statusText) 287 | if !service.dependencies.isEmpty { 288 | detailRow("Dependencies:", service.dependencies.joined(separator: ", ")) 289 | } 290 | } 291 | .font(.system(.caption, design: .monospaced)) 292 | 293 | if !service.issues.isEmpty { 294 | Divider() 295 | Text("ISSUES") 296 | .font(.system(.caption2, design: .monospaced)) 297 | .foregroundColor(.red) 298 | 299 | ForEach(service.issues) { issue in 300 | HStack(spacing: 4) { 301 | Circle() 302 | .fill(issue.severity.color) 303 | .frame(width: 6, height: 6) 304 | Text(issue.message) 305 | .font(.system(.caption, design: .monospaced)) 306 | .foregroundColor(.secondary) 307 | } 308 | } 309 | } 310 | 311 | HStack { 312 | Spacer() 313 | Button { 314 | selectedService = service 315 | pendingAction = .restart 316 | showingActionSheet = true 317 | } label: { 318 | Label("Restart", systemImage: "arrow.counterclockwise") 319 | .font(.system(.caption, design: .monospaced)) 320 | } 321 | .disabled(viewModel.isLoading) 322 | } 323 | } 324 | .padding(.top, 8) 325 | } 326 | 327 | private func detailRow(_ label: String, _ value: String) -> some View { 328 | HStack(alignment: .top) { 329 | Text(label) 330 | .foregroundColor(.secondary) 331 | Text(value) 332 | } 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /WarDragon/UI/SpectrumView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpectrumView.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 12/26/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SpectrumView: View { 11 | @ObservedObject var viewModel: SpectrumData.SpectrumViewModel 12 | @State private var spectrumPort: String = String(UserDefaults.standard.integer(forKey: "spectrumPort")) 13 | @State private var showSettings = false 14 | 15 | var body: some View { 16 | VStack(spacing: 16) { 17 | HStack { 18 | Button { 19 | if viewModel.isListening { 20 | viewModel.stopListening() 21 | } else { 22 | guard let port = Int(spectrumPort), port > 0 && port < 65536 else { return } 23 | viewModel.startListening(port: UInt16(port)) 24 | } 25 | } label: { 26 | Image(systemName: viewModel.isListening ? "stop.circle.fill" : "play.circle.fill") 27 | .font(.title) 28 | .foregroundColor(viewModel.isListening ? .red : .green) 29 | } 30 | .padding(.horizontal) 31 | 32 | if let latest = viewModel.spectrumData.last { 33 | VStack(alignment: .leading) { 34 | Text("Center: \(formatFrequency(Double(latest.fc)))") 35 | Text("Sample Rate: \(formatFrequency(Double(latest.sampleRate)))") 36 | Text("FFT Size: \(latest.data.count)") 37 | } 38 | .font(.system(.caption, design: .monospaced)) 39 | } 40 | 41 | Spacer() 42 | 43 | Button { 44 | showSettings.toggle() 45 | } label: { 46 | Image(systemName: "gear") 47 | } 48 | } 49 | .padding(.horizontal) 50 | 51 | if let error = viewModel.connectionError { 52 | Text(error) 53 | .foregroundColor(.red) 54 | .font(.appCaption) 55 | } 56 | 57 | if let latest = viewModel.spectrumData.last { 58 | SpectrumGraphView(data: latest) 59 | .frame(height: 300) 60 | .background(Color.black) 61 | .cornerRadius(12) 62 | .overlay( 63 | RoundedRectangle(cornerRadius: 12) 64 | .strokeBorder(Color.green.opacity(0.3), lineWidth: 1) 65 | ) 66 | 67 | WaterfallView(data: viewModel.spectrumData) 68 | .frame(height: 200) 69 | .background(Color.black) 70 | .cornerRadius(12) 71 | .overlay( 72 | RoundedRectangle(cornerRadius: 12) 73 | .strokeBorder(Color.green.opacity(0.3), lineWidth: 1) 74 | ) 75 | } else { 76 | Text("No spectrum data") 77 | .foregroundColor(.secondary) 78 | .frame(maxHeight: .infinity) 79 | } 80 | } 81 | .padding() 82 | .navigationTitle("Spectrum") 83 | .sheet(isPresented: $showSettings) { 84 | NavigationView { 85 | Form { 86 | Section("UDP Connection") { 87 | HStack { 88 | Text("Port") 89 | Spacer() 90 | TextField("Port", text: $spectrumPort) 91 | .keyboardType(.numberPad) 92 | .textFieldStyle(.roundedBorder) 93 | .frame(width: 100) 94 | .multilineTextAlignment(.trailing) 95 | .onChange(of: spectrumPort) { _, newValue in 96 | let filtered = newValue.filter { $0.isNumber } 97 | if filtered != newValue { 98 | spectrumPort = filtered 99 | } 100 | 101 | if let port = Int(filtered), 102 | port > 0 && port < 65536 { 103 | UserDefaults.standard.set(port, forKey: "spectrumPort") 104 | if viewModel.isListening { 105 | viewModel.stopListening() 106 | viewModel.startListening(port: UInt16(port)) 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | .navigationTitle("Spectrum Settings") 114 | .navigationBarTitleDisplayMode(.inline) 115 | .toolbar { 116 | ToolbarItem(placement: .navigationBarTrailing) { 117 | Button("Done") { 118 | showSettings = false 119 | } 120 | } 121 | } 122 | } 123 | .presentationDetents([.medium]) 124 | } 125 | } 126 | 127 | private func formatFrequency(_ hz: Double) -> String { 128 | switch hz { 129 | case _ where hz >= 1e9: 130 | return String(format: "%.3f GHz", hz/1e9) 131 | case _ where hz >= 1e6: 132 | return String(format: "%.3f MHz", hz/1e6) 133 | case _ where hz >= 1e3: 134 | return String(format: "%.3f kHz", hz/1e3) 135 | default: 136 | return String(format: "%.0f Hz", hz) 137 | } 138 | } 139 | } 140 | 141 | struct SpectrumGraphView: View { 142 | let data: SpectrumData 143 | 144 | var body: some View { 145 | GeometryReader { geometry in 146 | Path { path in 147 | let step = geometry.size.width / CGFloat(data.data.count) 148 | let values = data.data 149 | let minValue = values.min() ?? 0 150 | let maxValue = values.max() ?? 1 151 | let difference = maxValue - minValue 152 | let scale = difference != 0 ? geometry.size.height / CGFloat(difference) : 0 153 | 154 | path.move(to: CGPoint( 155 | x: 0, 156 | y: geometry.size.height - CGFloat(values[0] - minValue) * scale 157 | )) 158 | 159 | for i in 1.. String { 184 | switch hz { 185 | case _ where hz >= 1e9: 186 | return String(format: "%.3f GHz", hz/1e9) 187 | case _ where hz >= 1e6: 188 | return String(format: "%.3f MHz", hz/1e6) 189 | case _ where hz >= 1e3: 190 | return String(format: "%.3f kHz", hz/1e3) 191 | default: 192 | return String(format: "%.0f Hz", hz) 193 | } 194 | } 195 | } 196 | 197 | struct WaterfallView: View { 198 | let data: [SpectrumData] 199 | 200 | var body: some View { 201 | GeometryReader { geometry in 202 | Canvas { context, size in 203 | guard !data.isEmpty else { return } 204 | 205 | let rowHeight = size.height / CGFloat(data.count) 206 | let colWidth = size.width / CGFloat(data[0].data.count) 207 | 208 | for (rowIndex, spectrum) in data.enumerated() { 209 | let y = size.height - CGFloat(rowIndex + 1) * rowHeight 210 | 211 | let minPower = spectrum.data.min() ?? 0 212 | let maxPower = spectrum.data.max() ?? 1 213 | let range = maxPower - minPower 214 | 215 | for (binIndex, power) in spectrum.data.enumerated() { 216 | let x = CGFloat(binIndex) * colWidth 217 | let normalizedPower = range != 0 ? Float((power - minPower) / range) : 0 218 | let color = powerToColor(Double(normalizedPower)) 219 | 220 | let rect = CGRect(x: x, y: y, width: colWidth + 1, height: rowHeight + 1) 221 | context.fill(Path(rect), with: .color(color)) 222 | } 223 | } 224 | } 225 | } 226 | } 227 | 228 | private func powerToColor(_ power: Double) -> Color { 229 | Color( 230 | hue: 0.75 - (power * 0.75), 231 | saturation: 1, 232 | brightness: power 233 | ) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /WarDragon/UI/StatusListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusListView.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 11/18/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StatusListView: View { 11 | @ObservedObject var statusViewModel: StatusViewModel 12 | @StateObject private var serviceViewModel = ServiceViewModel() 13 | @State private var showServiceManagement = false 14 | 15 | var body: some View { 16 | GeometryReader { geometry in 17 | ScrollViewReader { proxy in 18 | List { 19 | // Service Status Widget 20 | // Section { 21 | // ServiceStatusWidget( 22 | // healthReport: serviceViewModel.healthReport, 23 | // criticalServices: serviceViewModel.criticalServices(), 24 | // showServiceManagement: $showServiceManagement 25 | // ) 26 | // } 27 | // .listRowInsets(EdgeInsets()) 28 | // .listRowBackground(Color.clear) 29 | 30 | // System status messages 31 | Section { 32 | ForEach(statusViewModel.statusMessages) { message in 33 | StatusMessageView(message: message) 34 | } 35 | } 36 | } 37 | .frame(maxWidth: .infinity, maxHeight: .infinity) 38 | .frame(minHeight: geometry.size.height, alignment: .center) 39 | .onChange(of: statusViewModel.statusMessages.count) { _, _ in 40 | if let latest = statusViewModel.statusMessages.last { 41 | withAnimation { 42 | proxy.scrollTo(latest.id, anchor: .bottom) 43 | } 44 | } 45 | } 46 | // Option to start/stop status listening (needs handler to see if running) 47 | // .onAppear { 48 | // serviceViewModel.startMonitoring() 49 | // } 50 | // .onDisappear { 51 | // serviceViewModel.stopMonitoring() 52 | // } 53 | } 54 | } 55 | .navigationTitle("System Status") 56 | .sheet(isPresented: $showServiceManagement) { 57 | NavigationView { 58 | ServiceManagementView(viewModel: serviceViewModel) 59 | .navigationTitle("Service Management") 60 | .navigationBarTitleDisplayMode(.inline) 61 | .toolbar { 62 | ToolbarItem(placement: .navigationBarLeading) { 63 | Button("Done") { 64 | showServiceManagement = false 65 | } 66 | } 67 | } 68 | } 69 | // Force the status monitor ZMQ/Multicast to listen when tapping system services 70 | // .onAppear { 71 | // serviceViewModel.startMonitoring() 72 | // } 73 | // .onDisappear { 74 | // serviceViewModel.stopMonitoring() 75 | // } 76 | } 77 | } 78 | } 79 | 80 | struct ServiceStatusWidget: View { 81 | let healthReport: ServiceViewModel.HealthReport? 82 | let criticalServices: [ServiceControl] 83 | @Binding var showServiceManagement: Bool 84 | 85 | var body: some View { 86 | Button(action: { showServiceManagement = true }) { 87 | VStack(spacing: 4) { 88 | // Health Status Bar 89 | // HStack(spacing: 12) { 90 | // Circle() 91 | // .fill(healthReport?.statusColor ?? .gray) 92 | // .frame(width: 12, height: 12) 93 | // 94 | // Text(healthReport?.overallHealth.uppercased() ?? "SYSTEM SERVICES") 95 | // .font(.appHeadline) 96 | // 97 | // Spacer() 98 | // 99 | // Image(systemName: "chevron.right") 100 | // .foregroundColor(.secondary) 101 | // } 102 | 103 | // Critical Services Preview 104 | if !criticalServices.isEmpty { 105 | VStack(alignment: .leading, spacing: 8) { 106 | Text("CRITICAL SERVICES") 107 | .font(.system(.caption, design: .monospaced)) 108 | .foregroundColor(.red) 109 | 110 | ForEach(criticalServices.prefix(2)) { service in 111 | HStack { 112 | Circle() 113 | .fill(service.status.healthStatus.color) 114 | .frame(width: 8, height: 8) 115 | Text(service.description) 116 | .font(.system(.caption, design: .monospaced)) 117 | Spacer() 118 | } 119 | } 120 | 121 | if criticalServices.count > 2 { 122 | Text("+ \(criticalServices.count - 2) more") 123 | .font(.system(.caption, design: .monospaced)) 124 | .foregroundColor(.secondary) 125 | } 126 | } 127 | .padding(.top, 4) 128 | } 129 | } 130 | .padding() 131 | .background( 132 | RoundedRectangle(cornerRadius: 12) 133 | .fill(Color(UIColor.secondarySystemBackground)) 134 | ) 135 | .padding(.horizontal) 136 | } 137 | .buttonStyle(PlainButtonStyle()) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /WarDragon/UI/TacDial.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TacDial.swift 3 | // WarDragon 4 | // 5 | // Created by Luke on 1/20/25. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import UIKit 11 | 12 | struct TacDial: View { 13 | let title: String 14 | let value: Binding 15 | let range: ClosedRange 16 | let step: Double 17 | let unit: String 18 | let color: Color 19 | 20 | private let lineWidth: CGFloat = 3 21 | private let radius: CGFloat = 60 22 | 23 | var body: some View { 24 | VStack { 25 | ZStack { 26 | // Outer ring 27 | Circle() 28 | .stroke(Color.gray.opacity(0.3), lineWidth: lineWidth) 29 | 30 | // Value arc 31 | Circle() 32 | .trim(from: 0, to: CGFloat((value.wrappedValue - range.lowerBound) / (range.upperBound - range.lowerBound))) 33 | .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) 34 | .rotationEffect(.degrees(-90)) 35 | 36 | // Current value 37 | VStack(spacing: 2) { 38 | Text("\(Int(value.wrappedValue))") 39 | .font(.system(.title2, design: .monospaced)) 40 | .foregroundColor(color) 41 | Text(unit) 42 | .font(.system(.caption, design: .monospaced)) 43 | .foregroundColor(color) 44 | } 45 | 46 | // Control knob 47 | Circle() 48 | .fill(color) 49 | .frame(width: 12, height: 12) 50 | .offset(y: -radius) 51 | .rotationEffect(.degrees(360 * (value.wrappedValue - range.lowerBound) / (range.upperBound - range.lowerBound))) 52 | .gesture( 53 | DragGesture(minimumDistance: 0) 54 | .onChanged { gesture in 55 | let center = CGPoint(x: radius, y: radius) 56 | let location = gesture.location 57 | let dx = location.x - center.x 58 | let dy = -(location.y - center.y) 59 | let angle = (dx == 0 && dy == 0) ? 0 : atan2(dx, dy) 60 | var degrees = angle * 180 / .pi 61 | if degrees < 0 { degrees += 360 } 62 | 63 | let normalizedValue = degrees / 360.0 64 | let newValue = range.lowerBound + normalizedValue * (range.upperBound - range.lowerBound) 65 | let steppedValue = (round(newValue / step) * step) 66 | if range.contains(steppedValue) { 67 | value.wrappedValue = steppedValue 68 | } 69 | } 70 | ) 71 | } 72 | .frame(width: radius * 2, height: radius * 2) 73 | 74 | Text(title) 75 | .font(.system(.caption2, design: .monospaced)) 76 | .foregroundColor(.gray) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /WarDragon/WarDragon.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.aps-environment 8 | development 9 | com.apple.developer.networking.wifi-info 10 | 11 | com.apple.developer.networking.multicast 12 | 13 | com.apple.developer.usernotifications.time-sensitive 14 | 15 | com.apple.security.app-sandbox 16 | 17 | 18 | 19 | --------------------------------------------------------------------------------