├── .gitattributes ├── .github └── workflows │ └── GithubAction.yml ├── .gitignore ├── FileTransferClient ├── .gitignore ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Package.swift ├── README.md ├── Sources │ └── FileTransferClient │ │ ├── AdafruitKit │ │ ├── Ble │ │ │ ├── AdafruitSensors │ │ │ │ ├── BleDataProcessingQueue.swift │ │ │ │ ├── BlePeripheral+AdafruitCommon.swift │ │ │ │ ├── BlePeripheral+FileTransfer.swift │ │ │ │ └── BlePeripheral+ManufacturerAdafruit.swift │ │ │ └── BleCentralMode │ │ │ │ ├── BleManager.swift │ │ │ │ ├── BlePeripheral+Battery.swift │ │ │ │ └── BlePeripheral.swift │ │ └── Utils │ │ │ ├── AppEnvironment.swift │ │ │ ├── CommandQueue.swift │ │ │ ├── Data+LittleEndianTypes.swift │ │ │ ├── Data+ScanValue.swift │ │ │ ├── HexUtils.swift │ │ │ ├── Int+Bytes.swift │ │ │ ├── LogHelper.swift │ │ │ ├── String+DeletingPrefix.swift │ │ │ └── Types+Data.swift │ │ ├── FileTransferClient.swift │ │ ├── FileTransferConnectionManager.swift │ │ └── FileTransferPathUtils.swift └── Tests │ └── FileTransferClientTests │ └── GliderTests.swift ├── LICENSE.txt ├── PyLeap.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ ├── WorkspaceSettings.xcsettings │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── trevorbeaton.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcshareddata │ └── xcschemes │ └── PyLeap.xcscheme ├── PyLeap ├── AppDelegate.swift ├── AppEnvironment.swift ├── Assets.xcassets │ ├── .DS_Store │ ├── AppIcon.appiconset │ │ ├── 120-1.png │ │ ├── 120.png │ │ ├── 167-1.png │ │ ├── 167-2.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 40.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 80.png │ │ ├── 87.png │ │ ├── Contents.json │ │ └── py.jpg │ ├── BGColor.colorset │ │ └── Contents.json │ ├── BlinkaLoading.imageset │ │ ├── BlinkaLoading.png │ │ └── Contents.json │ ├── ClueBoard.imageset │ │ ├── ClueBoard.png │ │ └── Contents.json │ ├── Colors │ │ ├── Contents.json │ │ ├── background_default.colorset │ │ │ └── Contents.json │ │ ├── button_primary_accent.colorset │ │ │ └── Contents.json │ │ ├── button_primary_disabled.colorset │ │ │ └── Contents.json │ │ ├── button_primary_text.colorset │ │ │ └── Contents.json │ │ └── button_warning_text.colorset │ │ │ └── Contents.json │ ├── Contents.json │ ├── Module Overlays │ │ ├── Contents.json │ │ ├── blinkCPB.imageset │ │ │ ├── Contents.json │ │ │ └── blinkCPB.png │ │ ├── glassesCPB.imageset │ │ │ ├── Contents.json │ │ │ └── glassesCPB.png │ │ └── rainbowsCPB.imageset │ │ │ ├── Contents.json │ │ │ └── rainbowsCPB.png │ ├── Onboard1.imageset │ │ ├── Contents.json │ │ └── Untitled_Artwork 6.png │ ├── Onboard2.imageset │ │ ├── Contents.json │ │ └── Untitled_Artwork 7.png │ ├── Onboard3.imageset │ │ ├── Contents.json │ │ └── Layer_1.png │ ├── adafruitLogoBlack.imageset │ │ ├── Contents.json │ │ └── adafruitLogoBlack.png │ ├── adafruit_blue.colorset │ │ └── Contents.json │ ├── adafruit_logo.imageset │ │ ├── Contents.json │ │ ├── info_adafruit_logo.png │ │ └── info_adafruit_logo@2x.png │ ├── adafruit_logo_black.imageset │ │ ├── Contents.json │ │ └── adafruit_logo_black.png │ ├── adafruit_logo_original.imageset │ │ ├── 181069.png │ │ └── Contents.json │ ├── adafruit_pyleap_logo_final1.imageset │ │ ├── Contents.json │ │ └── adafruit_pyleap_logo_final1.png │ ├── alt-gray.colorset │ │ └── Contents.json │ ├── bg.colorset │ │ └── Contents.json │ ├── bluetooth.imageset │ │ ├── Contents.json │ │ └── outline_bluetooth_black_24pt_3x.png │ ├── bluetoothLogo.imageset │ │ ├── Contents.json │ │ └── bluetoothLogo.png │ ├── bluetooth_button_color.colorset │ │ └── Contents.json │ ├── check.imageset │ │ ├── Contents.json │ │ └── check.png │ ├── clue.imageset │ │ ├── Contents.json │ │ ├── board_clue_front.png │ │ ├── board_clue_front@2x.png │ │ └── board_clue_front@3x.png │ ├── connectionRequest.imageset │ │ ├── Contents.json │ │ └── connectionRequest.png │ ├── cpb.imageset │ │ ├── Contents.json │ │ ├── board_cpb.png │ │ ├── board_cpb@2x.png │ │ └── board_cpb@3x.png │ ├── led-glasses.imageset │ │ ├── Contents.json │ │ └── led-glasses.png │ ├── logo.imageset │ │ ├── 181069.png │ │ └── Contents.json │ ├── pyleapLogo.imageset │ │ ├── Contents.json │ │ └── adafruit_pyleap_logo_final.png │ ├── pyleap_blue.colorset │ │ └── Contents.json │ ├── pyleap_burg.colorset │ │ └── Contents.json │ ├── pyleap_gray.colorset │ │ └── Contents.json │ ├── pyleap_green.colorset │ │ └── Contents.json │ ├── pyleap_logo_white.imageset │ │ ├── Contents.json │ │ └── pyleap_logo_white.png │ ├── pyleap_pink.colorset │ │ └── Contents.json │ ├── pyleap_purple.colorset │ │ └── Contents.json │ ├── pyleap_spotlight.colorset │ │ └── Contents.json │ ├── pyleap_yellow.colorset │ │ └── Contents.json │ ├── rainbow-cpb.imageset │ │ ├── Contents.json │ │ └── rainbow-cpb.png │ ├── rainbow.imageset │ │ ├── Contents.json │ │ └── rainbow.png │ ├── slide1.imageset │ │ ├── Contents.json │ │ └── slide1.png │ ├── slide2.imageset │ │ ├── Contents.json │ │ └── slide2.png │ ├── slide3.imageset │ │ ├── Contents.json │ │ └── slide3.png │ ├── slide4.imageset │ │ ├── Contents.json │ │ └── slide4.png │ ├── warning_icon.imageset │ │ ├── Contents.json │ │ └── warning_icon.png │ └── x-mark.imageset │ │ ├── Contents.json │ │ └── x-mark.png ├── Download View │ ├── DownloadViewModel.swift │ └── FileManagerCheck.swift ├── ExampleView.swift ├── Info.plist ├── Model │ ├── DataStore.swift │ ├── ProjectsModel.swift │ └── WebDirectoryModel.swift ├── Models │ ├── .DS_Store │ ├── NumbersOnly.swift │ └── Scanner │ │ ├── PeripheralAutoConnect.swift │ │ └── PeripheralList.swift ├── Networking │ ├── Image Extension.swift │ ├── ImageCaching.swift │ ├── JSONDecoderHelper.swift │ ├── NetworkError.swift │ ├── NetworkManager.swift │ ├── NetworkMonitor.swift │ └── Networking.swift ├── Outline.md ├── Preview Content │ ├── .DS_Store │ ├── Preview Assets.xcassets │ │ └── Contents.json │ └── Resources │ │ └── DefaultPreferences.plist ├── PyLeap.entitlements ├── PyLeapApp.swift ├── Resource │ ├── AddedFont │ │ └── ReadexPro │ │ │ ├── ReadexPro-Bold.ttf │ │ │ ├── ReadexPro-Light.ttf │ │ │ ├── ReadexPro-Medium.ttf │ │ │ ├── ReadexPro-Regular.ttf │ │ │ └── ReadexPro-SemiBold.ttf │ ├── GifImage.swift │ └── ViewModifier.swift ├── Spotlight Extension.swift ├── ViewModels │ └── Utils │ │ ├── FileTransferPathUtils.swift │ │ ├── HideKeyboard.swift │ │ ├── KeyboardUtils.swift │ │ ├── NavBarModifier.swift │ │ ├── String+DeletingPrefix.swift │ │ ├── UIColor+LightAndDark.swift │ │ ├── View+Extensions.swift │ │ └── View+If.swift └── Views │ ├── .DS_Store │ ├── Bluetooth Pairing View │ ├── BLEPairingView.swift │ ├── BTConnectionView.swift │ ├── BTConnectionViewModel.swift │ ├── BluetoothStatusView.swift │ ├── CreditView.swift │ ├── PairingTutorialView.swift │ └── TroubleshootView.swift │ ├── FillerView.swift │ ├── Onboarding Views │ ├── OnboardingBackgroundView.swift │ ├── OnboardingDataModel.swift │ ├── OnboardingStepView.swift │ └── OnboardingViewPure.swift │ ├── Paired View │ ├── BleContentCommands.swift │ ├── BleContentTransfer.swift │ ├── BleModuleView.swift │ ├── BleModuleViewModel.swift │ └── DownloadState.swift │ ├── Reusable Views │ ├── DemoSubCellView.swift │ ├── DemoViewCell.swift │ ├── ScrollRefreshableView.swift │ └── SubCellViewModel.swift │ ├── Root View │ ├── Config.swift │ ├── RootView.swift │ └── RootViewModel.swift │ ├── Startup View │ ├── StartupView.swift │ └── StartupViewModel.swift │ ├── UI Components │ ├── Blinka Animation.swift │ ├── Buttons.swift │ ├── HeaderView.swift │ ├── OnAnimationComplete.swift │ ├── SearchBarView.swift │ ├── SubHeaderView.swift │ └── WifiHeaderView.swift │ ├── Unpaired View │ ├── MainSelectionView.swift │ ├── MainSelectionViewModel.swift │ ├── SelectionView.swift │ └── WifiSelection.swift │ ├── WebView │ └── WebView Content.swift │ └── Wifi View │ ├── BasicCredentials.swift │ ├── CircuitPythonService.swift │ ├── CircuitPythonType.swift │ ├── LocalNetworkAuth.swift │ ├── NetworkPeripheral.swift │ ├── PyLeap-Bridging-Header.h │ ├── Queue.swift │ ├── SettingsView │ ├── BLESetttings │ │ ├── BLESettingsView.swift │ │ └── BLESettingsViewModel.swift │ ├── SettingsView.swift │ └── SettingsViewModel.swift │ ├── WifiCell.swift │ ├── WifiCellViewModel.swift │ ├── WifiFileTransfer.swift │ ├── WifiListDetailView.swift │ ├── WifiNetworkService.swift │ ├── WifiPairingView.swift │ ├── WifiServiceManager.swift │ ├── WifiServiceSelectionView.swift │ ├── WifiStatusHeaderBarView.swift │ ├── WifiSubViewCell.swift │ ├── WifiSubViewCellModel.swift │ ├── WifiTransferService.swift │ ├── WifiView.swift │ ├── WifiViewModel.swift │ └── Wifi_ifaddrs.m └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/GithubAction.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | .DS_Store -------------------------------------------------------------------------------- /FileTransferClient/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /FileTransferClient/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FileTransferClient/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "FileTransferClient", 8 | platforms: [ 9 | .macOS(.v10_14), .iOS(.v14), .tvOS(.v14) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "FileTransferClient", 15 | targets: ["FileTransferClient"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "FileTransferClient", 27 | dependencies: []), 28 | .testTarget( 29 | name: "FileTransferClientTests", 30 | dependencies: ["FileTransferClient"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /FileTransferClient/README.md: -------------------------------------------------------------------------------- 1 | # FileTransferClient 2 | 3 | ## Introduction 4 | 5 | Swift API for [Adafruit CircuitPython BLE File Transfer protocol](https://github.com/adafruit/Adafruit_CircuitPython_BLE_File_Transfer) 6 | 7 | This BLE service is geared towards file transfer to and from a device running the service. A core part of the protocol is free space responses so that the server can be a memory limited device. The free space responses allow for small buffer sizes that won't be overwhelmed by the client. 8 | 9 | 10 | ## Installing 11 | 12 | TODO: Add Swift Package Manager Support 13 | 14 | 15 | ## Usage 16 | 17 | 1. Create an FileTransferClient object from a connected CBPeripheral 18 | 19 | FileTransferClient(connectedCBPeripheral: CBPeripheral, services: [BoardService]? = nil, completion: @escaping (Result) -> Void) 20 | 21 | Parameters: 22 | 23 | - connectedCBPeripheral: standard CoreBluetooth peripheral. It should be connected. 24 | - services: a list of services to detect and setup. Currenlty only .fileTransfer is available but more services and sensors could be added. 25 | - completion: a completion handler. It returns .success if the board is ready to accept commands, or an .failure with an error if the board setup failed 26 | 27 | It will automatically check the the peripheral supports a valid version of the FileTransfer protocol and setup the board to accept commands 28 | 29 | 30 | 2. Use any of the available commands: 31 | 32 | - **readFile**: Given a full path, returns the full contents of the file 33 | 34 | func readFile(path: String, progress: ProgressHandler? = nil, completion: ((Result) -> Void)?) 35 | 36 | completion is called with *.success* and the binary *Data* of the file or *.failure* with an *Error* 37 | progress is called with the transmission status *typealias ProgressHandler = ((_ transmittedBytes: Int, _ totalBytes: Int) -> Void)* 38 | 39 | - **writeFile**: Writes the content to the given full path. If the file exists, it will be overwritten. 40 | 41 | func writeFile(path: String, data: Data, progress: ProgressHandler? = nil, completion: ((Result) -> Void)?) 42 | 43 | completion is called with *.success* wjith the modification Date or *.failure* with an *Error* 44 | progress is called with the transmission status *typealias ProgressHandler = ((_ transmittedBytes: Int, _ totalBytes: Int) -> Void)* 45 | 46 | - **deleteFile**: Deletes the file or directory at the given full path. Directories must be empty to be deleted. 47 | 48 | func deleteFile(path: String, completion: ((Result) -> Void)?) 49 | 50 | completion is called with *.success* or *.failure* with an *Error* 51 | 52 | 53 | - **makeDirectory**: Creates a new directory at the given full path. If a parent directory does not exist, then it will also be created. If any name conflicts with an existing file, an error will be returned 54 | 55 | func makeDirectory(path: String, completion: ((Result) -> Void)?) 56 | 57 | completion is called with *.success* with the modification Date or *.failure* with an *Error* 58 | 59 | 60 | - **listDirectory**: Lists all of the contents in a directory given a full path. Returned paths are relative to the given path to reduce duplication 61 | 62 | func listDirectory(path: String, completion: ((Result<[BlePeripheral.DirectoryEntry]?, Error>) -> Void)?) 63 | 64 | 65 | completion is called with *.success* with a list of directory entries or *.failure* with an *Error* 66 | 67 | 68 | A DirectoryEntry is a struct with the name of the file and the type: .file with a size in bytes or .directory 69 | 70 | struct DirectoryEntry { 71 | enum EntryType { 72 | case file(size: Int) 73 | case directory 74 | } 75 | 76 | let type: EntryType 77 | let name: String 78 | } 79 | 80 | 81 | 82 | ## Examples 83 | 84 | TODO: Create example 85 | 86 | -------------------------------------------------------------------------------- /FileTransferClient/Sources/FileTransferClient/AdafruitKit/Ble/AdafruitSensors/BleDataProcessingQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BleDataProcessingQueue.swift 3 | // Glider 4 | // 5 | // Created by Antonio García on 4/6/21. 6 | // 7 | 8 | import Foundation 9 | 10 | class DataProcessingQueue { 11 | private var data = Data() 12 | private var dataSemaphore = DispatchSemaphore(value: 1) 13 | 14 | private var uuid: UUID 15 | 16 | init(uuid: UUID) { 17 | self.uuid = uuid 18 | } 19 | 20 | // MARK: - BLE Notifications 21 | private weak var didConnectToPeripheralObserver: NSObjectProtocol? 22 | private weak var didDisconnectFromPeripheralObserver: NSObjectProtocol? 23 | 24 | private func registerNotifications(enabled: Bool) { 25 | let notificationCenter = NotificationCenter.default 26 | if enabled { 27 | didConnectToPeripheralObserver = notificationCenter.addObserver(forName: .didConnectToPeripheral, object: nil, queue: .main, using: {[weak self] notification in self?.didConnectToPeripheral(notification: notification)}) 28 | didDisconnectFromPeripheralObserver = notificationCenter.addObserver(forName: .didDisconnectFromPeripheral, object: nil, queue: .main, using: {[weak self] notification in self?.didDisconnectFromPeripheral(notification: notification)}) 29 | 30 | } else { 31 | if let didConnectToPeripheralObserver = didConnectToPeripheralObserver {notificationCenter.removeObserver(didConnectToPeripheralObserver)} 32 | if let didDisconnectFromPeripheralObserver = didDisconnectFromPeripheralObserver {notificationCenter.removeObserver(didDisconnectFromPeripheralObserver)} 33 | } 34 | } 35 | 36 | private func didConnectToPeripheral(notification: Notification) { 37 | guard let identifier = notification.userInfo?[BleManager.NotificationUserInfoKey.uuid.rawValue] as? UUID else { return } 38 | guard identifier == uuid else { return } 39 | 40 | // Clear cached data 41 | data.removeAll() 42 | } 43 | 44 | private func didDisconnectFromPeripheral(notification: Notification) { 45 | guard let identifier = notification.userInfo?[BleManager.NotificationUserInfoKey.uuid.rawValue] as? UUID else { return } 46 | guard identifier == uuid else { return } 47 | 48 | // Clean data on disconnect 49 | data.removeAll() 50 | 51 | dataSemaphore.signal() // Force signal if it was waiting 52 | } 53 | 54 | func processQueue(receivedData: Data?, processingHandler: ((Data)->Int)) { 55 | // Don't append more data, till the delegate has finished processing it 56 | dataSemaphore.wait() 57 | 58 | // Append received data 59 | if let receivedData = receivedData { 60 | data.append(receivedData) 61 | //DLog("Data received. Queue size: \(data.count) bytes") 62 | } 63 | 64 | // Process chunks 65 | processQueuedChunks(processingHandler: processingHandler) 66 | 67 | // Ready to receive more data 68 | dataSemaphore.signal() 69 | } 70 | 71 | // Important: this method changes "data", so it should be used only when the semaphore is blocking concurrent access 72 | private func processQueuedChunks(processingHandler: ((Data)->Int)) { 73 | // Process chunk 74 | let processedDataCount = processingHandler(data) 75 | 76 | // Remove processed bytes 77 | if processedDataCount > 0 { 78 | data = Data(data.dropFirst(processedDataCount)) 79 | } 80 | else { 81 | //DLog("Queue size: \(data.count) bytes. Waiting for more data to process packet...") 82 | } 83 | 84 | // If there is still unprocessed chunks in the queue, process the next one 85 | let isStillUnprocessedDataInQueue = processedDataCount > 0 && data.count > 0 86 | if isStillUnprocessedDataInQueue { 87 | //DLog("Unprocessed data still in queue (\(data.count) bytes). Try to process next packet") 88 | processQueuedChunks(processingHandler: processingHandler) 89 | } 90 | else if data.isEmpty { 91 | //DLog("Data queue empty") 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /FileTransferClient/Sources/FileTransferClient/AdafruitKit/Ble/BleCentralMode/BlePeripheral+Battery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlePeripheral+Battery.swift 3 | // Bluefruit 4 | // 5 | // Created by Antonio García on 22/06/2017. 6 | // Copyright © 2017 Adafruit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import CoreBluetooth 12 | 13 | extension BlePeripheral { 14 | // Costants 15 | static let kBatteryServiceUUID = CBUUID(string: "180F") 16 | static let kBatteryCharacteristicUUID = CBUUID(string: "2A19") 17 | 18 | // MARK: - Actions 19 | func readBatteryLevel(handler: @escaping ((Int, Error?) -> Void)) { 20 | self.characteristic(uuid: BlePeripheral.kBatteryCharacteristicUUID, serviceUuid: BlePeripheral.kBatteryServiceUUID) { (characteristic, error) in 21 | guard error == nil, let characteristic = characteristic else { DLog("Error reading battery characteristic: \(error?.localizedDescription ?? "")"); return } 22 | 23 | self.readCharacteristic(characteristic) { (result, error) in 24 | guard error == nil, let data = result as? Data, data.count >= 1 else { 25 | DLog("Error reading battery level: \(error?.localizedDescription ?? "")") 26 | handler(-1, error) 27 | return 28 | } 29 | 30 | let level = Int(data[0]) // from 0 to 100 31 | handler(level, nil) 32 | } 33 | } 34 | } 35 | 36 | func startReadingBatteryLevel(handler: @escaping ((Int) -> Void)) { 37 | 38 | self.characteristic(uuid: BlePeripheral.kBatteryCharacteristicUUID, serviceUuid: BlePeripheral.kBatteryServiceUUID) { (characteristic, error) in 39 | guard error == nil, let characteristic = characteristic else { DLog("Error starting read for battery characteristic: \(error?.localizedDescription ?? "")"); return } 40 | 41 | // Read current value 42 | self.readCharacteristic(characteristic) { (result, error) in 43 | guard error == nil, let data = result as? Data, data.count >= 1 else { DLog("Error reading battery level: \(error?.localizedDescription ?? "")"); return } 44 | 45 | let level = Int(data[0]) // from 0 to 100 46 | handler(level) 47 | } 48 | 49 | // Enable notifications to receive value changes 50 | self.enableNotify(for: characteristic, handler: { error in 51 | guard error == nil else { DLog("Error receiving notify for battery level"); return } 52 | guard let data = characteristic.value, data.count >= 1 else { DLog("Invalid data receiving notify for battery level"); return } 53 | 54 | let level = Int(data[0]) // from 0 to 100 55 | handler(level) 56 | }) 57 | } 58 | } 59 | 60 | func stopReadingBatteryLevel() { 61 | self.characteristic(uuid: BlePeripheral.kBatteryCharacteristicUUID, serviceUuid: BlePeripheral.kBatteryServiceUUID) { (characteristic, error) in 62 | guard error == nil, let characteristic = characteristic else { DLog("Error stopping read for battery characteristic: \(error?.localizedDescription ?? "")"); return } 63 | 64 | self.disableNotify(for: characteristic) 65 | } 66 | } 67 | 68 | // MARK: - Utils 69 | func isBatteryAdvertised() -> Bool { 70 | return advertisement.services?.contains(BlePeripheral.kBatteryServiceUUID) ?? false 71 | } 72 | 73 | func hasBattery() -> Bool { 74 | return peripheral.services?.first(where: {$0.uuid == BlePeripheral.kBatteryServiceUUID}) != nil 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /FileTransferClient/Sources/FileTransferClient/AdafruitKit/Utils/AppEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppEnvironment.swift 3 | // Glider 4 | // 5 | // Created by Antonio García on 14/5/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AppEnvironment { 11 | 12 | // MARK: - Run mode 13 | public static var isDebug: Bool { 14 | return _isDebugAssertConfiguration() 15 | } 16 | 17 | public static var isRunningTests: Bool { 18 | return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil 19 | } 20 | 21 | public static var inSimulator: Bool { 22 | #if targetEnvironment(simulator) 23 | return true 24 | #else 25 | return false 26 | #endif 27 | } 28 | 29 | public static var inXcodePreviewMode: Bool { 30 | return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" 31 | } 32 | 33 | // MARK: - App info 34 | public static var appVersion: String? { 35 | return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String 36 | } 37 | 38 | public static var buildNumber: String? { 39 | return Bundle.main.infoDictionary?["CFBundleVersion"] as? String 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /FileTransferClient/Sources/FileTransferClient/AdafruitKit/Utils/CommandQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ElementQueue.swift 3 | // Bluefruit 4 | // 5 | // Created by Antonio García on 17/10/2016. 6 | // Copyright © 2016 Adafruit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Command array, executed sequencially 12 | class CommandQueue { 13 | var executeHandler: ((_ command: Element) -> Void)? 14 | 15 | private var queueLock = NSLock() 16 | 17 | /* 18 | private var queue = [Element]() { 19 | didSet { 20 | queueLock.lock() 21 | var shouldExecute = false 22 | // Start executing the first command (if it was not already executing) 23 | let nextElement = queue.first 24 | if oldValue.isEmpty, nextElement != nil { 25 | shouldExecute = true 26 | } 27 | DLog("queue size: \(queue.count)") 28 | queueLock.unlock() 29 | 30 | if shouldExecute { 31 | self.executeHandler?(nextElement!) 32 | } 33 | } 34 | } 35 | 36 | func first() -> Element? { 37 | queueLock.lock(); defer { queueLock.unlock() } 38 | return queue.first 39 | } 40 | 41 | func append(_ element: Element) { 42 | queue.append(element) 43 | } 44 | 45 | func next() { 46 | guard !queue.isEmpty else { return } 47 | 48 | // Delete finished command and trigger next execution if needed 49 | queue.removeFirst() 50 | 51 | if let nextElement = queue.first { 52 | executeHandler?(nextElement) 53 | } 54 | } 55 | 56 | func removeAll() { 57 | DLog("queue removeAll") 58 | queue.removeAll() 59 | } 60 | */ 61 | 62 | private var queue = [Element]() 63 | 64 | func first() -> Element? { 65 | queueLock.lock(); defer { queueLock.unlock() } 66 | //DLog("queue: \(queue) first: \(queue.first)") 67 | return queue.first 68 | } 69 | 70 | func executeNext() { 71 | queueLock.lock() 72 | guard !queue.isEmpty else { queueLock.unlock(); return } 73 | 74 | //DLog("queue remove finished: \(queue.first)") 75 | // Delete finished command and trigger next execution if needed 76 | queue.removeFirst() 77 | let nextElement = queue.first 78 | queueLock.unlock() 79 | 80 | if let nextElement = nextElement { 81 | //DLog("execute next") 82 | executeHandler?(nextElement) 83 | } 84 | } 85 | 86 | func append(_ element: Element) { 87 | queueLock.lock() 88 | let shouldExecute = queue.isEmpty 89 | queue.append(element) 90 | queueLock.unlock() 91 | //DLog("queue: \(queue) append: \(element). total: \(queue.count)") 92 | 93 | if shouldExecute { 94 | executeHandler?(element) 95 | } 96 | } 97 | 98 | func removeAll() { 99 | // DLog("queue removeAll: \(queue.count)") 100 | queue.removeAll() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /FileTransferClient/Sources/FileTransferClient/AdafruitKit/Utils/Data+LittleEndianTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+LittleEndianTypes.swift 3 | // BluefruitPlayground 4 | // 5 | // Created by Antonio García on 13/11/2019. 6 | // Copyright © 2019 Adafruit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Data { 12 | func toFloatFrom32Bits() -> Float { 13 | return Float(bitPattern: UInt32(littleEndian: self.withUnsafeBytes { $0.load(as: UInt32.self) })) 14 | } 15 | 16 | func toIntFrom32Bits() -> Int { 17 | return Int(Int32(bitPattern: UInt32(littleEndian: self.withUnsafeBytes { $0.load(as: UInt32.self) }))) 18 | } 19 | 20 | func toInt32From32Bits() -> Int32 { 21 | return Int32(bitPattern: UInt32(littleEndian: self.withUnsafeBytes { $0.load(as: UInt32.self) })) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /FileTransferClient/Sources/FileTransferClient/AdafruitKit/Utils/Data+ScanValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+ScanValues.swift 3 | // Bluefruit 4 | // 5 | // Created by Antonio García on 17/11/2016. 6 | // Copyright © 2016 Adafruit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Data Scan 12 | extension Data { 13 | func scanValue(start: Int, length: Int) -> T { 14 | let subdata = self.subdata(in: (self.startIndex + start)..<(self.startIndex + start + length)) 15 | return subdata.withUnsafeBytes { $0.load(as: T.self) } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /FileTransferClient/Sources/FileTransferClient/AdafruitKit/Utils/HexUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HexUtils.swift 3 | // Bluefruit 4 | // 5 | // Created by Antonio García on 15/10/16. 6 | // Copyright © 2015 Adafruit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct HexUtils { 12 | static func hexDescription(data: Data, prefix: String = "", postfix: String = " ") -> String { 13 | return data.reduce("") {$0 + String(format: "%@%02X%@", prefix, $1, postfix)} 14 | } 15 | 16 | static func hexDescription(bytes: [UInt8], prefix: String = "", postfix: String = " ") -> String { 17 | return bytes.reduce("") {$0 + String(format: "%@%02X%@", prefix, $1, postfix)} 18 | } 19 | 20 | static func decimalDescription(data: Data, prefix: String = "", postfix: String = " ") -> String { 21 | return data.reduce("") {$0 + String(format: "%@%ld%@", prefix, $1, postfix)} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /FileTransferClient/Sources/FileTransferClient/AdafruitKit/Utils/Int+Bytes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int+ToByteArray.swift 3 | // Bluefruit 4 | // 5 | // Created by Antonio García on 11/06/2019. 6 | // Copyright © 2019 Adafruit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // From https://stackoverflow.com/questions/29970204/split-uint32-into-uint8-in-swift 12 | 13 | protocol UIntToBytesConvertable { 14 | var toBytes: [UInt8] { get } 15 | } 16 | 17 | extension UIntToBytesConvertable { 18 | fileprivate func toByteArr(endian: T, count: Int) -> [UInt8] { 19 | var _endian = endian 20 | let bytePtr = withUnsafePointer(to: &_endian) { 21 | $0.withMemoryRebound(to: UInt8.self, capacity: count) { 22 | UnsafeBufferPointer(start: $0, count: count) 23 | } 24 | } 25 | return [UInt8](bytePtr) 26 | } 27 | } 28 | 29 | extension UInt16: UIntToBytesConvertable { 30 | var toBytes: [UInt8] { 31 | return toByteArr(endian: self.littleEndian, count: MemoryLayout.size) 32 | } 33 | } 34 | 35 | extension UInt32: UIntToBytesConvertable { 36 | var toBytes: [UInt8] { 37 | return toByteArr(endian: self.littleEndian, count: MemoryLayout.size) 38 | } 39 | } 40 | 41 | extension UInt64: UIntToBytesConvertable { 42 | var toBytes: [UInt8] { 43 | return toByteArr(endian: self.littleEndian, count: MemoryLayout.size) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /FileTransferClient/Sources/FileTransferClient/AdafruitKit/Utils/LogHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogHelper.swift 3 | // BluefruitPlayground 4 | // 5 | // Created by Antonio García on 10/10/2019. 6 | // Copyright © 2019 Adafruit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Note: check that Build Settings -> Project -> Active Compilation Conditions -> Debug, has DEBUG 12 | 13 | public func DLog(_ message: String, function: String = #function) { 14 | if _isDebugAssertConfiguration() { 15 | // NSLog("%@, %@", function, message) 16 | } 17 | 18 | // Send notification in case we are using LogManager 19 | NotificationCenter.default.post(name: .didLogDebugMessage, object: nil, userInfo: ["message" : message]) 20 | } 21 | 22 | 23 | 24 | // MARK: - Custom Notifications 25 | extension Notification.Name { 26 | private static let kPrefix = Bundle.main.bundleIdentifier! 27 | public static let didLogDebugMessage = Notification.Name(kPrefix+".didLogDebugMessage") 28 | 29 | } 30 | -------------------------------------------------------------------------------- /FileTransferClient/Sources/FileTransferClient/AdafruitKit/Utils/String+DeletingPrefix.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+DeletingPrefix.swift 3 | // GliderFileProvider 4 | // 5 | // Created by Antonio García on 27/6/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | public func deletingPrefix(_ prefix: String) -> String { 12 | guard self.hasPrefix(prefix) else { return self } 13 | return String(self.dropFirst(prefix.count)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /FileTransferClient/Sources/FileTransferClient/AdafruitKit/Utils/Types+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Types+Data.swift 3 | // BluefruitPlayground 4 | // 5 | // Created by Antonio García on 13/11/2019. 6 | // Copyright © 2019 Adafruit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // from: https://stackoverflow.com/questions/38023838/round-trip-swift-number-types-to-from-data 12 | 13 | public protocol DataConvertible { 14 | init?(data: Data) 15 | var data: Data { get } 16 | } 17 | 18 | extension DataConvertible where Self: ExpressibleByIntegerLiteral { 19 | 20 | public init?(data: Data) { 21 | var value: Self = 0 22 | guard data.count == MemoryLayout.size(ofValue: value) else { return nil } 23 | _ = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0)}) 24 | self = value 25 | } 26 | 27 | public var data: Data { 28 | return withUnsafeBytes(of: self) { Data($0) } 29 | } 30 | } 31 | 32 | // Declare conformance to all types which can safely be converted to Data and back 33 | extension Int: DataConvertible { } 34 | extension UInt8: DataConvertible { } 35 | extension Int16: DataConvertible { } 36 | extension UInt16: DataConvertible { } 37 | extension Int32: DataConvertible { } 38 | extension UInt32: DataConvertible { } 39 | extension Int64: DataConvertible { } 40 | extension UInt64: DataConvertible { } 41 | extension Float: DataConvertible { } 42 | extension Double: DataConvertible { } 43 | 44 | // Convert from [UInt8] to Data and from Data to [UInt8] 45 | // from: https://stackoverflow.com/questions/31821709/nsdata-to-uint8-in-swift/31821838 46 | extension Data { 47 | var bytes: [UInt8] { 48 | return [UInt8](self) 49 | } 50 | } 51 | 52 | extension Array where Element == UInt8 { 53 | var data: Data { 54 | return Data(self) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /FileTransferClient/Sources/FileTransferClient/FileTransferPathUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileTransferPathUtils.swift 3 | // Glider 4 | // 5 | // Created by Antonio García on 24/5/21. 6 | // 7 | 8 | import Foundation 9 | 10 | // TODO: FileProvider should not use this utils because even if the separators for FileProvider and URLs are the same, it could change in the future 11 | public struct FileTransferPathUtils { 12 | static let pathSeparatorCharacter: Character = "/" 13 | public static let pathSeparator = String(pathSeparatorCharacter) 14 | 15 | // MARK: - Path management 16 | public static func pathRemovingFilename(path: String) -> String { 17 | guard let filenameIndex = path.lastIndex(of: Self.pathSeparatorCharacter) else { 18 | return path 19 | } 20 | 21 | return String(path[path.startIndex...filenameIndex]) 22 | } 23 | 24 | public static func filenameFromPath(path: String) -> String { 25 | guard let filenameIndex = path.lastIndex(of: Self.pathSeparatorCharacter) else { 26 | return path 27 | } 28 | 29 | return String(String(path[filenameIndex...]).dropFirst()) 30 | } 31 | 32 | public static func upPath(from path: String) -> String { 33 | 34 | // Remove trailing separator if exists 35 | let filenamePath: String 36 | if path.last == Self.pathSeparatorCharacter { 37 | filenamePath = String(path.dropLast()) 38 | } 39 | else { 40 | filenamePath = path 41 | } 42 | 43 | // Remove any filename 44 | let pathWithoutFilename = FileTransferPathUtils.pathRemovingFilename(path: filenamePath) 45 | return pathWithoutFilename 46 | } 47 | 48 | public static func parentPath(from path: String) -> String { 49 | guard !isRootDirectory(path: path) else { return rootDirectory } // Root parent is root 50 | 51 | let parentPath: String 52 | // Remove leading '/' and find the next one. Keep anything after the one found 53 | let pathWithoutLeadingSlash = path.deletingPrefix(rootDirectory) 54 | if let indexOfLastSlash = pathWithoutLeadingSlash.lastIndex(of: pathSeparatorCharacter) { 55 | let parentPathWithoutLeadingSlash = String(pathWithoutLeadingSlash.prefix(upTo: indexOfLastSlash)) 56 | parentPath = rootDirectory + parentPathWithoutLeadingSlash 57 | } 58 | else { // Is root (only the leading '/' found) 59 | parentPath = rootDirectory 60 | } 61 | 62 | return parentPath 63 | } 64 | 65 | public static func pathWithTrailingSeparator(path: String) -> String { 66 | return path.hasSuffix(Self.pathSeparator) ? path : path.appending(Self.pathSeparator) // Force a trailing separator 67 | } 68 | 69 | // MARK: - Root Directory 70 | public static var rootDirectory: String { 71 | return Self.pathSeparator 72 | } 73 | 74 | public static func isRootDirectory(path: String) -> Bool { 75 | return path == rootDirectory 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /FileTransferClient/Tests/FileTransferClientTests/GliderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Glider 3 | 4 | final class GliderTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | //XCTAssertEqual(Glider().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | 4 | Copyright (c) 2023 Adafruit Industries 5 | 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. -------------------------------------------------------------------------------- /PyLeap.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PyLeap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PyLeap.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PyLeap.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Zip", 6 | "repositoryURL": "https://github.com/marmelroy/Zip", 7 | "state": { 8 | "branch": null, 9 | "revision": "67fa55813b9e7b3b9acee9c0ae501def28746d76", 10 | "version": "2.1.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /PyLeap.xcodeproj/project.xcworkspace/xcuserdata/trevorbeaton.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap.xcodeproj/project.xcworkspace/xcuserdata/trevorbeaton.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /PyLeap.xcodeproj/xcshareddata/xcschemes/PyLeap.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /PyLeap/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Glider 4 | // 5 | // Created by Antonio García on 14/5/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class AppDelegate: NSObject, UIApplicationDelegate { 11 | 12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 13 | 14 | // UI 15 | setupAppearances() 16 | 17 | return true 18 | } 19 | 20 | private func setupAppearances() { 21 | // Alerts 22 | UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = .blue 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PyLeap/AppEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppEnvironment.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 6/30/21. 6 | // 7 | import Foundation 8 | 9 | public struct AppEnvironment { 10 | 11 | static var isDebug: Bool { 12 | return _isDebugAssertConfiguration() 13 | } 14 | 15 | static var isRunningTests: Bool { 16 | return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil 17 | } 18 | 19 | static var inSimulator: Bool { 20 | #if targetEnvironment(simulator) 21 | return true 22 | #else 23 | return false 24 | #endif 25 | } 26 | 27 | static var inXcodePreviewMode: Bool { 28 | return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" 29 | } 30 | 31 | static var appVersion: String? { 32 | return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/.DS_Store -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/120-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/AppIcon.appiconset/120-1.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/167-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/AppIcon.appiconset/167-1.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/167-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/AppIcon.appiconset/167-2.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "58.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "80.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "120-1.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "idiom" : "ipad", 53 | "scale" : "1x", 54 | "size" : "20x20" 55 | }, 56 | { 57 | "idiom" : "ipad", 58 | "scale" : "2x", 59 | "size" : "20x20" 60 | }, 61 | { 62 | "idiom" : "ipad", 63 | "scale" : "1x", 64 | "size" : "29x29" 65 | }, 66 | { 67 | "idiom" : "ipad", 68 | "scale" : "2x", 69 | "size" : "29x29" 70 | }, 71 | { 72 | "idiom" : "ipad", 73 | "scale" : "1x", 74 | "size" : "40x40" 75 | }, 76 | { 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "167-2.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "76x76" 86 | }, 87 | { 88 | "filename" : "167-1.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "167.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "83.5x83.5" 98 | }, 99 | { 100 | "filename" : "py.jpg", 101 | "idiom" : "ios-marketing", 102 | "scale" : "1x", 103 | "size" : "1024x1024" 104 | } 105 | ], 106 | "info" : { 107 | "author" : "xcode", 108 | "version" : 1 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/AppIcon.appiconset/py.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/AppIcon.appiconset/py.jpg -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/BGColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.941", 9 | "green" : "0.941", 10 | "red" : "0.941" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.941", 27 | "green" : "0.941", 28 | "red" : "0.941" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/BlinkaLoading.imageset/BlinkaLoading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/BlinkaLoading.imageset/BlinkaLoading.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/BlinkaLoading.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BlinkaLoading.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/ClueBoard.imageset/ClueBoard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/ClueBoard.imageset/ClueBoard.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/ClueBoard.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ClueBoard.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Colors/background_default.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "0.675", 10 | "red" : "0.216" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Colors/button_primary_accent.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.656", 9 | "green" : "0.645", 10 | "red" : "0.634" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Colors/button_primary_disabled.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.424", 9 | "green" : "0.422", 10 | "red" : "0.423" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Colors/button_primary_text.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Colors/button_warning_text.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.145", 9 | "green" : "0.098", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Module Overlays/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Module Overlays/blinkCPB.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "blinkCPB.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Module Overlays/blinkCPB.imageset/blinkCPB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/Module Overlays/blinkCPB.imageset/blinkCPB.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Module Overlays/glassesCPB.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "glassesCPB.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Module Overlays/glassesCPB.imageset/glassesCPB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/Module Overlays/glassesCPB.imageset/glassesCPB.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Module Overlays/rainbowsCPB.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "rainbowsCPB.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Module Overlays/rainbowsCPB.imageset/rainbowsCPB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/Module Overlays/rainbowsCPB.imageset/rainbowsCPB.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Onboard1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Untitled_Artwork 6.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Onboard1.imageset/Untitled_Artwork 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/Onboard1.imageset/Untitled_Artwork 6.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Onboard2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Untitled_Artwork 7.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Onboard2.imageset/Untitled_Artwork 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/Onboard2.imageset/Untitled_Artwork 7.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Onboard3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Layer_1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/Onboard3.imageset/Layer_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/Onboard3.imageset/Layer_1.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/adafruitLogoBlack.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "adafruitLogoBlack.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/adafruitLogoBlack.imageset/adafruitLogoBlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/adafruitLogoBlack.imageset/adafruitLogoBlack.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/adafruit_blue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "150", 9 | "green" : "100", 10 | "red" : "74" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "150", 27 | "green" : "100", 28 | "red" : "74" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/adafruit_logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "info_adafruit_logo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "info_adafruit_logo@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/adafruit_logo.imageset/info_adafruit_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/adafruit_logo.imageset/info_adafruit_logo.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/adafruit_logo.imageset/info_adafruit_logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/adafruit_logo.imageset/info_adafruit_logo@2x.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/adafruit_logo_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "adafruit_logo_black.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/adafruit_logo_black.imageset/adafruit_logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/adafruit_logo_black.imageset/adafruit_logo_black.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/adafruit_logo_original.imageset/181069.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/adafruit_logo_original.imageset/181069.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/adafruit_logo_original.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "181069.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/adafruit_pyleap_logo_final1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "adafruit_pyleap_logo_final1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/adafruit_pyleap_logo_final1.imageset/adafruit_pyleap_logo_final1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/adafruit_pyleap_logo_final1.imageset/adafruit_pyleap_logo_final1.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/alt-gray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "97", 9 | "green" : "97", 10 | "red" : "97" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "97", 27 | "green" : "97", 28 | "red" : "97" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/bg.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/bluetooth.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "outline_bluetooth_black_24pt_3x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/bluetooth.imageset/outline_bluetooth_black_24pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/bluetooth.imageset/outline_bluetooth_black_24pt_3x.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/bluetoothLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bluetoothLogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/bluetoothLogo.imageset/bluetoothLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/bluetoothLogo.imageset/bluetoothLogo.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/bluetooth_button_color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "196", 9 | "green" : "143", 10 | "red" : "97" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "255", 27 | "green" : "255", 28 | "red" : "255" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/check.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "check.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/check.imageset/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/check.imageset/check.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/clue.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "board_clue_front.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "board_clue_front@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "board_clue_front@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/clue.imageset/board_clue_front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/clue.imageset/board_clue_front.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/clue.imageset/board_clue_front@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/clue.imageset/board_clue_front@2x.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/clue.imageset/board_clue_front@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/clue.imageset/board_clue_front@3x.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/connectionRequest.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "connectionRequest.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/connectionRequest.imageset/connectionRequest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/connectionRequest.imageset/connectionRequest.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/cpb.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "board_cpb.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "board_cpb@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "board_cpb@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "preserves-vector-representation" : true, 25 | "template-rendering-intent" : "original" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/cpb.imageset/board_cpb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/cpb.imageset/board_cpb.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/cpb.imageset/board_cpb@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/cpb.imageset/board_cpb@2x.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/cpb.imageset/board_cpb@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/cpb.imageset/board_cpb@3x.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/led-glasses.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "led-glasses.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/led-glasses.imageset/led-glasses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/led-glasses.imageset/led-glasses.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/logo.imageset/181069.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/logo.imageset/181069.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "181069.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "original" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/pyleapLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "adafruit_pyleap_logo_final.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/pyleapLogo.imageset/adafruit_pyleap_logo_final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/pyleapLogo.imageset/adafruit_pyleap_logo_final.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/pyleap_blue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "255", 9 | "green" : "190", 10 | "red" : "90" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "255", 27 | "green" : "190", 28 | "red" : "91" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/pyleap_burg.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "84", 9 | "green" : "84", 10 | "red" : "162" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "extended-srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "84", 27 | "green" : "84", 28 | "red" : "164" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/pyleap_gray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "51", 9 | "green" : "51", 10 | "red" : "51" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "51", 27 | "green" : "51", 28 | "red" : "51" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/pyleap_green.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "84", 9 | "green" : "162", 10 | "red" : "114" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "84", 27 | "green" : "164", 28 | "red" : "114" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/pyleap_logo_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pyleap_logo_white.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/pyleap_logo_white.imageset/pyleap_logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/pyleap_logo_white.imageset/pyleap_logo_white.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/pyleap_pink.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "154", 9 | "green" : "97", 10 | "red" : "196" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "154", 27 | "green" : "97", 28 | "red" : "196" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/pyleap_purple.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "162", 9 | "green" : "84", 10 | "red" : "124" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "162", 27 | "green" : "84", 28 | "red" : "124" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/pyleap_spotlight.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "20", 9 | "green" : "20", 10 | "red" : "20" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "0.895", 26 | "blue" : "50", 27 | "green" : "50", 28 | "red" : "50" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/pyleap_yellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "96", 9 | "green" : "228", 10 | "red" : "247" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/rainbow-cpb.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "rainbow-cpb.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/rainbow-cpb.imageset/rainbow-cpb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/rainbow-cpb.imageset/rainbow-cpb.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/rainbow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "rainbow.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/rainbow.imageset/rainbow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/rainbow.imageset/rainbow.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/slide1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "slide1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/slide1.imageset/slide1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/slide1.imageset/slide1.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/slide2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "slide2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/slide2.imageset/slide2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/slide2.imageset/slide2.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/slide3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "slide3.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/slide3.imageset/slide3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/slide3.imageset/slide3.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/slide4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "slide4.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/slide4.imageset/slide4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/slide4.imageset/slide4.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/warning_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "warning_icon.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/warning_icon.imageset/warning_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/warning_icon.imageset/warning_icon.png -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/x-mark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "x-mark.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Assets.xcassets/x-mark.imageset/x-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Assets.xcassets/x-mark.imageset/x-mark.png -------------------------------------------------------------------------------- /PyLeap/ExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 4/16/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ExampleView: View { 11 | @Binding var shouldShowOnboarding: Bool 12 | 13 | var body: some View { 14 | 15 | TabView { 16 | // PageView(title: "Welcome", subtitle: "PyLeap allows you to send complete projects from the Adafruit Learn System to your PyLeap compatible device.", imageName: "Onboard2", showDismissButton: false, shouldShowOnboarding: $shouldShowOnboarding) 17 | // 18 | // PageView(title: "Connect", subtitle: "Pair to your PyLeap enabled device.", imageName: "Onboard2", showDismissButton: false, shouldShowOnboarding: $shouldShowOnboarding) 19 | // 20 | // PageView(title: "Choose your Adventure!", subtitle: "Choose a project you would like to send over to your PyLeap compatible device.", imageName: "slide3", showDismissButton: false, shouldShowOnboarding: $shouldShowOnboarding) 21 | // 22 | PageView(title: "Send projects directly from the Adafruit Learning System to your Adafruit Device...", subtitle: "...without opening a code editor or connecting to a computer.", imageName: "slide4", showDismissButton: true, shouldShowOnboarding: $shouldShowOnboarding) 23 | } 24 | .tabViewStyle(PageTabViewStyle()) 25 | } 26 | } 27 | 28 | struct PageView: View { 29 | let title: String 30 | let subtitle: String 31 | let imageName: String 32 | let showDismissButton: Bool 33 | @Binding var shouldShowOnboarding: Bool 34 | 35 | 36 | var body: some View { 37 | VStack { 38 | Image("pyleapLogo") 39 | .resizable() 40 | .aspectRatio(contentMode: .fit) 41 | .padding(.horizontal, 60) 42 | 43 | Text(title) 44 | .font(Font.custom( "ReadexPro-Regular", size: 24)) 45 | .foregroundColor(Color("pyleap_gray")) 46 | .padding(.horizontal, 30) 47 | .minimumScaleFactor(0.1) 48 | 49 | Image("cpb") 50 | .resizable() 51 | .frame(width: 300, height: 300, alignment: .center) 52 | .aspectRatio(contentMode: .fit) 53 | .padding(.horizontal, 95) 54 | 55 | Text(subtitle) 56 | .font(Font.custom( "ReadexPro-Regular", size: 24)) 57 | .foregroundColor(Color("pyleap_gray")) 58 | .padding(.horizontal, 30) 59 | .minimumScaleFactor(0.1) 60 | if showDismissButton { 61 | Button { 62 | shouldShowOnboarding.toggle() 63 | } label: { 64 | ZStack { 65 | Rectangle() 66 | .frame(width: 270, height: 50, alignment: .center) 67 | .cornerRadius(25) 68 | .foregroundColor(Color("pyleap_pink")) 69 | 70 | Text("Get Started") 71 | .minimumScaleFactor(0.1) 72 | .font(Font.custom("ReadexPro-Regular", size: 25)) 73 | .foregroundColor(Color.white) 74 | .frame(height: 50) 75 | 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | struct ExampleView_Previews: PreviewProvider { 84 | static var previews: some View { 85 | ExampleView(shouldShowOnboarding: .constant(true)) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /PyLeap/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | PyLeap 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSApplicationCategoryType 24 | 25 | LSRequiresIPhoneOS 26 | 27 | NSBluetoothAlwaysUsageDescription 28 | PyLeap requires access to Bluetooth to connect to Adafruit devices 29 | NSBluetoothPeripheralUsageDescription 30 | PyLeap requires access to Bluetooth to connect to Adafruit devices 31 | NSBonjourServices 32 | 33 | _circuitpython._tcp 34 | _bonjour._tcp 35 | 36 | NSLocalNetworkUsageDescription 37 | PyLeap uses the local network to communicate with your Adafruit device 38 | UIAppFonts 39 | 40 | ReadexPro-Medium.ttf 41 | ReadexPro-Regular.ttf 42 | ReadexPro-Light.ttf 43 | ReadexPro-Bold.ttf 44 | ReadexPro-SemiBold.ttf 45 | 46 | UIApplicationSceneManifest 47 | 48 | UIApplicationSupportsMultipleScenes 49 | 50 | 51 | UIApplicationSupportsIndirectInputEvents 52 | 53 | UILaunchScreen 54 | 55 | UIImageName 56 | 57 | 58 | UIRequiredDeviceCapabilities 59 | 60 | armv7 61 | 62 | UIRequiresFullScreen 63 | 64 | UIStatusBarHidden 65 | 66 | UISupportedInterfaceOrientations 67 | 68 | UIInterfaceOrientationPortrait 69 | UIInterfaceOrientationPortraitUpsideDown 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /PyLeap/Model/ProjectsModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectsModel.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 3/24/22. 6 | // 7 | 8 | 9 | import Foundation 10 | 11 | struct RootResults: Decodable { 12 | let projects: [ResultItem] 13 | } 14 | 15 | struct ResultItem: Codable, Identifiable, Equatable { 16 | enum CodingKeys: CodingKey { 17 | case projectName 18 | case projectImage 19 | case description 20 | case bundleLink 21 | case learnGuideLink 22 | case compatibility 23 | } 24 | 25 | var id = UUID() 26 | let projectName: String 27 | let projectImage: String 28 | let description: String 29 | let bundleLink: String 30 | let learnGuideLink: String 31 | let compatibility: [String] 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /PyLeap/Model/WebDirectoryModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebDirectoryModel.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 8/23/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct WebDirectoryModel: Codable, Identifiable, Equatable { 11 | 12 | enum CodingKeys: CodingKey { 13 | case name 14 | case directory 15 | case modified_ns 16 | case file_size 17 | } 18 | 19 | var id = UUID() 20 | let uniqueID = String() 21 | let name: String 22 | let directory: Bool 23 | let modified_ns:Int 24 | let file_size: Int 25 | } 26 | 27 | class WebDirectoryModelx: Codable, Identifiable { 28 | 29 | enum CodingKeys: CodingKey { 30 | case name 31 | case directory 32 | case modified_ns 33 | case file_size 34 | } 35 | 36 | var id = UUID() 37 | let uniqueID = String() 38 | let name: String 39 | let directory: Bool 40 | let modified_ns:Int 41 | let file_size: Int 42 | } 43 | -------------------------------------------------------------------------------- /PyLeap/Models/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Models/.DS_Store -------------------------------------------------------------------------------- /PyLeap/Models/NumbersOnly.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumbersOnly.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 8/3/21. 6 | // 7 | import SwiftUI 8 | 9 | class NumbersOnly: ObservableObject { 10 | @Published var value = "" { 11 | didSet { 12 | let filtered = value.filter { $0.isNumber } 13 | 14 | if value != filtered { 15 | value = filtered 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PyLeap/Models/Scanner/PeripheralAutoConnect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PeripheralAutoConnect.swift 3 | // BluefruitPlayground 4 | // 5 | // Created by Antonio García on 18/02/2020. 6 | // Copyright © 2020 Adafruit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FileTransferClient 11 | 12 | /** 13 | Manages the logic for autoconnecting to a peripheral 14 | Usage: 15 | call isAutoconnectAvailable whenever a peripheral is discovered or updated. 16 | 17 | How it works: 18 | It waits at least kMinScanningTimeToAutoconnect since scanning started, and needs that the peripheral RSSI is bigger than kMaxRssiToAutoConnect during kMinTimeDetectingPeripheralForAutoconnect seconds. 19 | If more than one peripheral matches the conditions, it will connect to the one with bigger RSSI (meaning that is closer) 20 | */ 21 | class PeripheralAutoConnect { 22 | // Config 23 | private static let kMinScanningTimeToAutoconnect: TimeInterval = 1.5 // 5 24 | private static let kMinRssiToAutoConnect = -80 // in dBM 25 | private static let kMinTimeDetectingPeripheralForAutoconnect: TimeInterval = 1 26 | 27 | // Data 28 | private(set) var matchingPeripherals: [(blePeripheral: BlePeripheral, discoverTime: TimeInterval)] = [] // List of peripherals that could be selected, and time since they have been matching the requirements 29 | 30 | // MARK: - Utils 31 | func reset() { 32 | matchingPeripherals.removeAll() 33 | } 34 | 35 | /** 36 | Should be called everytime the peripheralList is updated 37 | - Returns: blePeripheral to autoconnect to, or nil if the decision has not been taken 38 | */ 39 | func update(peripheralList: PeripheralList) -> BlePeripheral? { 40 | let bleManager = peripheralList.bleManager 41 | 42 | // Only update autoconnect if we are not already connecting to a peripheral 43 | guard bleManager.connectedOrConnectingPeripherals().isEmpty else { return nil } 44 | 45 | // Get peripherals 46 | let filteredPeripherals = peripheralList.filteredPeripherals(forceUpdate: true) // Refresh the peripherals 47 | 48 | // Filter by RSSI 49 | let nearbyPeripherals = filteredPeripherals.filter({$0.rssi ?? -127 > PeripheralAutoConnect.kMinRssiToAutoConnect}) 50 | 51 | // Update matching peripherals 52 | let nearbyIdentifiers = nearbyPeripherals.map({$0.identifier}) // List of nearby identifiers 53 | matchingPeripherals = matchingPeripherals.filter {nearbyIdentifiers.contains($0.blePeripheral.identifier)} // Remove peripherals that are no longer near 54 | for nearbyPeripheral in nearbyPeripherals { 55 | if matchingPeripherals.first(where: {$0.blePeripheral.identifier == nearbyPeripheral.identifier}) == nil { 56 | // New peripheral found. Add to possible matches 57 | matchingPeripherals.append((blePeripheral: nearbyPeripheral, discoverTime: CFAbsoluteTimeGetCurrent())) 58 | } 59 | } 60 | 61 | if AppEnvironment.isDebug && false { 62 | //DLog("peripherals: \(matchingPeripherals.count)") 63 | let currentTime = CFAbsoluteTimeGetCurrent() 64 | _ = matchingPeripherals.map { DLog("\($0.blePeripheral.identifier) rssi: \($0.blePeripheral.rssi == nil ? -127:$0.blePeripheral.rssi!) - elapsed: \(Int((currentTime - $0.discoverTime)*1000))") } 65 | //DLog("--") 66 | } 67 | 68 | // Wait for the minimum time since scanning started 69 | guard bleManager.scanningElapsedTime ?? 0 > PeripheralAutoConnect.kMinScanningTimeToAutoconnect else { 70 | //DLog("remaining mandatory scan time: \(AutoConnectViewController.kMinScanningTimeToAutoconnect - (bleManager.scanningElapsedTime ?? 0))") 71 | return nil 72 | } 73 | 74 | // Take peripherals that have been matching more than kMinTimeDetectingPeripheralForAutoconnect seconds 75 | let currentTime = CFAbsoluteTimeGetCurrent() 76 | let preselectedPeripherals = matchingPeripherals.filter({currentTime - $0.discoverTime >= PeripheralAutoConnect.kMinTimeDetectingPeripheralForAutoconnect}).map({$0.blePeripheral}) 77 | 78 | // Sort by RSSI 79 | let sortedPeripherals = preselectedPeripherals.sorted { (blePeripheral0, blePeripheral1) -> Bool in 80 | return blePeripheral0.rssi ?? -127 > blePeripheral1.rssi ?? -127 81 | } 82 | 83 | // Connect to closest CPB 84 | guard let peripheral = sortedPeripherals.first else { return nil } 85 | return peripheral 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /PyLeap/Models/Scanner/PeripheralList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PeripheralList.swift 3 | // BluefruitPlayground 4 | // 5 | // Created by Antonio García on 11/10/2019. 6 | // Copyright © 2019 Adafruit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FileTransferClient 11 | 12 | class PeripheralList { 13 | // Data 14 | private(set) var bleManager: BleManager 15 | private var peripherals = [BlePeripheral]() 16 | private var cachedFilteredPeripherals: [BlePeripheral] = [] 17 | 18 | // MARK: - Lifecycle 19 | init(bleManager: BleManager) { 20 | self.bleManager = bleManager 21 | } 22 | 23 | // MARK: - Actions 24 | func filteredPeripherals(forceUpdate: Bool) -> [BlePeripheral] { 25 | if forceUpdate { 26 | cachedFilteredPeripherals = calculateFilteredPeripherals() 27 | } 28 | return cachedFilteredPeripherals 29 | } 30 | 31 | func clear() { 32 | peripherals.removeAll() 33 | } 34 | 35 | private func calculateFilteredPeripherals() -> [BlePeripheral] { 36 | let peripherals = bleManager 37 | .peripheralsSortedByFirstDiscovery() 38 | .filter({$0.isManufacturerAdafruit() && $0.advertisement.services?.contains(BlePeripheral.kFileTransferServiceUUID) ?? false}) 39 | return peripherals 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /PyLeap/Networking/Image Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image Extension.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 4/22/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | extension Image { 12 | 13 | func data(url:URL) -> Self { 14 | 15 | if let data = try? Data(contentsOf: url) { 16 | return Image(uiImage: UIImage(data: data)!) 17 | .resizable() 18 | } 19 | return self 20 | .resizable() 21 | } 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /PyLeap/Networking/ImageCaching.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCaching.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 5/2/22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct ImageWithURL: View { 12 | 13 | @ObservedObject var imageLoader: ImageLoaderAndCache 14 | 15 | init(_ url: String) { 16 | imageLoader = ImageLoaderAndCache(imageURL: url) 17 | } 18 | 19 | var body: some View { 20 | Image(uiImage: UIImage(data: self.imageLoader.imageData) ?? UIImage(named: "adafruit_logo_original")!) 21 | .resizable() 22 | .clipped() 23 | } 24 | } 25 | 26 | class ImageLoaderAndCache: ObservableObject { 27 | 28 | @Published var imageData = Data() 29 | 30 | init(imageURL: String) { 31 | let cache = URLCache.shared 32 | let request = URLRequest(url: URL(string: imageURL)!, cachePolicy: URLRequest.CachePolicy.returnCacheDataElseLoad, timeoutInterval: 60.0) 33 | if let data = cache.cachedResponse(for: request)?.data { 34 | print("Image gathered from cache.") 35 | self.imageData = data 36 | } else { 37 | URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in 38 | if let data = data, let response = response { 39 | let cachedData = CachedURLResponse(response: response, data: data) 40 | cache.storeCachedResponse(cachedData, for: request) 41 | DispatchQueue.main.async { 42 | print("Image downloaded from the Internet.") 43 | self.imageData = data 44 | } 45 | } 46 | }).resume() 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PyLeap/Networking/JSONDecoderHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONDecoderHelper.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 1/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class JSONDecoderHelper { 11 | static func decode(data: Data) -> T? { 12 | let decoder = JSONDecoder() 13 | return try? decoder.decode(T.self, from: data) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PyLeap/Networking/NetworkError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkError.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 3/10/22. 6 | // 7 | 8 | 9 | import Foundation 10 | 11 | enum NetworkError: String, Error { 12 | 13 | case noInternetConnection = "There's a problem with your internet connection. Try again later." 14 | case invalidURL = "Invalid URL used. Please update URL and try again." 15 | case unableToCompleteRequest = "Unable to complete request. Check your internet connection and try again." 16 | case invalidResponse = "Invalid response from the server.Please try again." 17 | case invalidData = "Invalid data was received. Please try again." 18 | case noDataAvailable = "Data not available." 19 | } 20 | -------------------------------------------------------------------------------- /PyLeap/Networking/NetworkMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkMonitor.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 11/29/21. 6 | // 7 | 8 | import Foundation 9 | import Network 10 | 11 | class NetworkMonitor { 12 | 13 | let queue = DispatchQueue(label: "Monitor") 14 | let monitor: NWPathMonitor 15 | 16 | public private(set) var isConnected: Bool = false 17 | 18 | public private(set) var connectionType: ConnectionType = .unknown 19 | 20 | enum ConnectionType{ 21 | case wifi 22 | case cellular 23 | case unknown 24 | } 25 | 26 | init() { 27 | monitor = NWPathMonitor() 28 | startMonitoring() 29 | } 30 | 31 | public func startMonitoring(){ 32 | print("Start Internet monitoring") 33 | monitor.start(queue: queue) 34 | } 35 | 36 | public func stopMonitoring(){ 37 | monitor.cancel() 38 | } 39 | 40 | 41 | public func getConnectionType(_ path: NWPath){ 42 | if path.usesInterfaceType(.wifi) { 43 | connectionType = .wifi 44 | print("Wifi") 45 | } 46 | else if path.usesInterfaceType(.cellular){ 47 | connectionType = .cellular 48 | print("cellular") 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /PyLeap/Networking/Networking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Networking.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 11/23/22. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | enum HTTPMethod: String { 12 | case delete = "DELETE" 13 | case get = "GET" 14 | case patch = "PATCH" 15 | case post = "POST" 16 | case put = "PUT" 17 | case options = "OPTIONS" 18 | } 19 | 20 | enum HTTPScheme: String { 21 | case http 22 | case https 23 | } 24 | 25 | /// The API protocol allows us to separate the task of constructing a URL, 26 | /// its parameters, and HTTP method from the act of executing the URL request 27 | /// and parsing the response. 28 | /// 29 | 30 | protocol API { 31 | 32 | var scheme: HTTPScheme { get } 33 | 34 | var baseURL: String { get } 35 | 36 | var path: String { get } 37 | // [URLQueryItem(name: "api_key", value: API_KEY)] 38 | var parameters: [URLQueryItem] { get } 39 | 40 | var method: HTTPMethod { get } 41 | } 42 | -------------------------------------------------------------------------------- /PyLeap/Outline.md: -------------------------------------------------------------------------------- 1 | # Outline 2 | 3 | ## Overview 4 | 5 | Facade 6 | The Wifi View's interaction with the View Model should be simple. 7 | Functions that should be accessible: 8 | ViewModel should have these functions: 9 | 10 | Should be able to choose the device its connects to. 11 | - Should I use IP? Host name? 12 | 13 | Are we connected to Wi-Fi? 14 | - If not, show Wi-fi prompt 15 | - If yes, continue. 16 | 17 | Check if this project exists. 18 | - If it doesn't exist, download named project from JSON URL 19 | - If download fails or not able to download the project, show message "Try Again Later" 20 | 21 | Transfer this project. 22 | Can we connect to another remote client(Adafruit device)? 23 | 24 | *Notes* 25 | The User should be able to switch to Bluetooth Mode while in Wi-Fi mode. 26 | 27 | Entities: 28 | [RootResult] -> ProjectDemos 29 | These are references that need to be validated to make sure they exist in File Manager, then transferred. 30 | 31 | For File Manager URL validation functions: 32 | 33 | ###1. File Manager URL validation 34 | Project Validation - Within this function parameter, add in the string of the project's name and check if the project folder with that name exists. 35 | If it doesn't exist, show error saying it doesn't or download file then resume file transfer process. 36 | 37 | 38 | ###2. Filter Files 39 | File Filter - To determine if unwanted files are being sorted out, we create a sort or remove files and directories that has a path like this: 40 | "adafruit-circuitpython-bundle-7.x-mpy" 41 | 42 | ###3. Find out the number of files and directories in selected project. 43 | Project Enumeration - We need to figure out how many files and directories are in the project folder. 44 | 45 | ###4. Sort Files/Directories 46 | Project Sort - Sort files and directories into designated arrays. Might do this individually. 47 | 48 | ###5. Directory Validation 49 | Check if each directory aleardy exists on client(disc). 50 | 51 | ###6. Directory Creation 52 | Using the "PUT" REST command create each directory in the array of directories until count equals zero. 53 | - If yes, remove queued directory from array and recurse. 54 | - If no, create new directory on disc. 55 | 56 | 57 | *Look into creating a queue for this* 58 | 59 | Prepare to start file transfer. 60 | 61 | File Transfer notes will continue... 62 | 63 | New topic, States for Wifi View 64 | 65 | When the user uses wifi view, these are the things that should happen: 66 | Check connection status. 67 | 68 | Check stored IP address. 69 | 70 | - If IP Address was not stored, show alert prompt that allows 71 | - If IP stored, 72 | 73 | 74 | ## Topics 75 | 76 | ### Group 77 | 78 | - ``Symbol`` 79 | -------------------------------------------------------------------------------- /PyLeap/Preview Content/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Preview Content/.DS_Store -------------------------------------------------------------------------------- /PyLeap/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PyLeap/Preview Content/Resources/DefaultPreferences.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /PyLeap/PyLeap.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.networking.wifi-info 6 | 7 | com.apple.security.app-sandbox 8 | 9 | com.apple.security.application-groups 10 | 11 | com.apple.security.device.bluetooth 12 | 13 | com.apple.security.network.client 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /PyLeap/PyLeapApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PyLeapApp.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 6/10/21. 6 | 7 | import SwiftUI 8 | 9 | @main 10 | struct PyLeapApp: App { 11 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 12 | 13 | var body: some Scene { 14 | WindowGroup { 15 | RootView() 16 | } 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PyLeap/Resource/AddedFont/ReadexPro/ReadexPro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Resource/AddedFont/ReadexPro/ReadexPro-Bold.ttf -------------------------------------------------------------------------------- /PyLeap/Resource/AddedFont/ReadexPro/ReadexPro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Resource/AddedFont/ReadexPro/ReadexPro-Light.ttf -------------------------------------------------------------------------------- /PyLeap/Resource/AddedFont/ReadexPro/ReadexPro-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Resource/AddedFont/ReadexPro/ReadexPro-Medium.ttf -------------------------------------------------------------------------------- /PyLeap/Resource/AddedFont/ReadexPro/ReadexPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Resource/AddedFont/ReadexPro/ReadexPro-Regular.ttf -------------------------------------------------------------------------------- /PyLeap/Resource/AddedFont/ReadexPro/ReadexPro-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Resource/AddedFont/ReadexPro/ReadexPro-SemiBold.ttf -------------------------------------------------------------------------------- /PyLeap/Resource/GifImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GifImage.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 3/14/22. 6 | // 7 | 8 | import SwiftUI 9 | import WebKit 10 | 11 | struct GifImage: UIViewRepresentable { 12 | private let name: String 13 | 14 | init(_ name: String) { 15 | self.name = name 16 | } 17 | 18 | func makeUIView(context: Context) -> WKWebView { 19 | let webView = WKWebView() 20 | let url = Bundle.main.url(forResource: name, withExtension: "gif")! 21 | let data = try! Data(contentsOf: url) 22 | webView.load( 23 | data, 24 | mimeType: "image/gif", 25 | characterEncodingName: "UTF-8", 26 | baseURL: url.deletingLastPathComponent() 27 | ) 28 | webView.scrollView.isScrollEnabled = false 29 | 30 | return webView 31 | } 32 | 33 | func updateUIView(_ uiView: WKWebView, context: Context) { 34 | uiView.reload() 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /PyLeap/Resource/ViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModifier.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 11/30/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | /// Hide or show the view based on a boolean value. 12 | /// 13 | /// Example for visibility: 14 | /// 15 | /// Text("Label") 16 | /// .isHidden(true) 17 | /// 18 | /// Example for complete removal: 19 | /// 20 | /// Text("Label") 21 | /// .isHidden(true, remove: true) 22 | /// 23 | /// - Parameters: 24 | /// - hidden: Set to `false` to show the view. Set to `true` to hide the view. 25 | /// - remove: Boolean value indicating whether or not to remove the view. 26 | @ViewBuilder func isHidden(_ hidden: Bool, remove: Bool = false) -> some View { 27 | if hidden { 28 | if !remove { 29 | self.hidden() 30 | } 31 | } else { 32 | self 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /PyLeap/ViewModels/Utils/FileTransferPathUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileTransferPathUtils.swift 3 | // Glider 4 | // 5 | // Created by Antonio García on 24/5/21. 6 | // 7 | 8 | import Foundation 9 | 10 | // TODO: FileProvider should not use this utils because even if the separators for FileProvider and URLs are the same, it could change in the future 11 | struct FileTransferPathUtils { 12 | static let pathSeparatorCharacter: Character = "/" 13 | static let pathSeparator = String(pathSeparatorCharacter) 14 | 15 | // MARK: - Path management 16 | static func pathRemovingFilename(path: String) -> String { 17 | guard let filenameIndex = path.lastIndex(of: Self.pathSeparatorCharacter) else { 18 | return path 19 | } 20 | 21 | return String(path[path.startIndex...filenameIndex]) 22 | } 23 | 24 | static func upPath(from path: String) -> String { 25 | 26 | // Remove trailing separator if exists 27 | let filenamePath: String 28 | if path.last == Self.pathSeparatorCharacter { 29 | filenamePath = String(path.dropLast()) 30 | } 31 | else { 32 | filenamePath = path 33 | } 34 | 35 | // Remove any filename 36 | let pathWithoutFilename = FileTransferPathUtils.pathRemovingFilename(path: filenamePath) 37 | return pathWithoutFilename 38 | } 39 | 40 | static func parentPath(from path: String) -> String { 41 | guard !isRootDirectory(path: path) else { return rootDirectory } // Root parent is root 42 | 43 | let parentPath: String 44 | // Remove leading '/' and find the next one. Keep anything after the one found 45 | let pathWithoutLeadingSlash = path.deletingPrefix(rootDirectory) 46 | if let indexOfLastSlash = pathWithoutLeadingSlash.lastIndex(of: "/") {//(pathWithoutLeadingSlash.range(of: "/")?.lowerBound) { 47 | let parentPathWithoutLeadingSlash = String(pathWithoutLeadingSlash.prefix(upTo: indexOfLastSlash)) 48 | parentPath = rootDirectory + parentPathWithoutLeadingSlash 49 | } 50 | else { // Is root (only the leading '/' found) 51 | parentPath = rootDirectory 52 | } 53 | 54 | return parentPath 55 | } 56 | 57 | static func pathWithTrailingSeparator(path: String) -> String { 58 | return path.hasSuffix("/") ? path : path.appending("/") // Force a trailing separator 59 | } 60 | 61 | // MARK: - Root Directory 62 | static var rootDirectory: String { 63 | return Self.pathSeparator 64 | } 65 | 66 | static func isRootDirectory(path: String) -> Bool { 67 | return path == rootDirectory 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /PyLeap/ViewModels/Utils/HideKeyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HideKeyboard.swift 3 | // 4 | // 5 | // Created by Antonio García on 6/5/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // from: https://www.hackingwithswift.com/quick-start/swiftui/how-to-dismiss-the-keyboard-for-a-textfield 11 | #if canImport(UIKit) 12 | extension View { 13 | func hideKeyboard() { 14 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) 15 | } 16 | } 17 | 18 | extension ViewModifier { 19 | func hideKeyboard() { 20 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) 21 | } 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /PyLeap/ViewModels/Utils/KeyboardUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardUtils.swift 3 | // Glider 4 | // 5 | // Created by Antonio García on 26/5/21. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | /* 12 | class KeyboardHandler { 13 | @State private var keyboardIsShown = false 14 | @State private var keyboardHideMonitor: AnyCancellable? = nil 15 | @State private var keyboardShownMonitor: AnyCancellable? = nil 16 | 17 | func setupKeyboardMonitors() { 18 | keyboardShownMonitor = NotificationCenter.default 19 | .publisher(for: UIWindow.keyboardWillShowNotification) 20 | .sink { _ in if !self.keyboardIsShown { keyboardIsShown = true } } 21 | 22 | keyboardHideMonitor = NotificationCenter.default 23 | .publisher(for: UIWindow.keyboardWillHideNotification) 24 | .sink { _ in if self.keyboardIsShown { keyboardIsShown = false } } 25 | } 26 | 27 | func dismantleKeyboarMonitors() { 28 | keyboardHideMonitor?.cancel() 29 | keyboardShownMonitor?.cancel() 30 | } 31 | 32 | } 33 | */ 34 | -------------------------------------------------------------------------------- /PyLeap/ViewModels/Utils/NavBarModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavBarModifier.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 3/14/22. 6 | // 7 | import SwiftUI 8 | import Foundation 9 | 10 | struct NavigationBarModifier: ViewModifier { 11 | 12 | var backgroundColor: UIColor? 13 | 14 | init( backgroundColor: UIColor?) { 15 | self.backgroundColor = backgroundColor 16 | let coloredAppearance = UINavigationBarAppearance() 17 | coloredAppearance.configureWithTransparentBackground() 18 | coloredAppearance.backgroundColor = .clear 19 | coloredAppearance.titleTextAttributes = [.foregroundColor: UIColor.black] 20 | coloredAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.black] 21 | 22 | UINavigationBar.appearance().standardAppearance = coloredAppearance 23 | UINavigationBar.appearance().compactAppearance = coloredAppearance 24 | UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance 25 | UINavigationBar.appearance().tintColor = .white 26 | 27 | } 28 | 29 | func body(content: Content) -> some View { 30 | ZStack{ 31 | content 32 | VStack { 33 | GeometryReader { geometry in 34 | Color(self.backgroundColor ?? .clear) 35 | .frame(height: geometry.safeAreaInsets.top) 36 | .edgesIgnoringSafeArea(.top) 37 | Spacer() 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /PyLeap/ViewModels/Utils/String+DeletingPrefix.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+DeletingPrefix.swift 3 | // GliderFileProvider 4 | // 5 | // Created by Antonio García on 27/6/21. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | extension String { 12 | func deletingPrefix(_ prefix: String) -> String { 13 | guard self.hasPrefix(prefix) else { return self } 14 | return String(self.dropFirst(prefix.count)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PyLeap/ViewModels/Utils/UIColor+LightAndDark.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+LightAndDark.swift 3 | // 4 | // Created by Antonio García on 08/12/15. 5 | // 6 | 7 | // http://stackoverflow.com/questions/11598043/get-slightly-lighter-and-darker-color-from-uicolor 8 | 9 | #if os(OSX) 10 | 11 | import Cocoa 12 | public typealias PXColor = NSColor 13 | 14 | #else 15 | 16 | import UIKit 17 | public typealias PXColor = UIColor 18 | 19 | #endif 20 | import SwiftUI 21 | 22 | extension PXColor { 23 | 24 | func lighter(by amount: CGFloat = 0.25) -> PXColor { 25 | return hueColorWithBrightnessAmount(1 + amount) 26 | } 27 | 28 | func darker(by amount: CGFloat = 0.25) -> PXColor { 29 | return hueColorWithBrightnessAmount(1 - amount) 30 | } 31 | 32 | fileprivate func hueColorWithBrightnessAmount(_ amount: CGFloat) -> PXColor { 33 | var hue : CGFloat = 0 34 | var saturation : CGFloat = 0 35 | var brightness : CGFloat = 0 36 | var alpha : CGFloat = 0 37 | 38 | #if os(iOS) 39 | 40 | if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) { 41 | return PXColor( hue: hue, 42 | saturation: saturation, 43 | brightness: brightness * amount, 44 | alpha: alpha ) 45 | } else { 46 | return self 47 | } 48 | 49 | #else 50 | 51 | getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) 52 | return PXColor( hue: hue, 53 | saturation: saturation, 54 | brightness: brightness * amount, 55 | alpha: alpha ) 56 | 57 | #endif 58 | 59 | } 60 | 61 | } 62 | 63 | // from: https://stackoverflow.com/questions/38435308/get-lighter-and-darker-color-variations-for-a-given-uicolor 64 | extension Color { 65 | public func lighter(by amount: CGFloat = 0.2) -> Self { Self(UIColor(self).lighter(by: amount)) } 66 | public func darker(by amount: CGFloat = 0.2) -> Self { Self(UIColor(self).darker(by: amount)) } 67 | } 68 | -------------------------------------------------------------------------------- /PyLeap/ViewModels/Utils/View+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View + Extensions.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 3/14/22. 6 | // 7 | import SwiftUI 8 | import Foundation 9 | 10 | extension View { 11 | 12 | func navigationBarColor(_ backgroundColor: UIColor?) -> some View { 13 | self.modifier(NavigationBarModifier(backgroundColor: backgroundColor)) 14 | } 15 | } 16 | 17 | extension View { 18 | /// Applies the given transform if the given condition evaluates to `true`. 19 | /// - Parameters: 20 | /// - condition: The condition to evaluate. 21 | /// - transform: The transform to apply to the source `View`. 22 | /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. 23 | @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { 24 | if condition { 25 | transform(self) 26 | } else { 27 | self 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /PyLeap/ViewModels/Utils/View+If.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+If.swift 3 | // 4 | // 5 | // Created by Antonio García on 5/5/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // from: https://blog.kaltoun.cz/conditionally-applying-view-modifiers-in-swiftui/ 11 | // from: https://www.avanderlee.com/swiftui/conditional-view-modifier/#:~:text=Conditional%20View%20Modifier%20creation%20in,different%20configurations%20to%20your%20views. 12 | extension View { 13 | @ViewBuilder 14 | func `if`(_ condition: Bool, content: (Self) -> Content) -> some View { 15 | if condition { 16 | content(self) 17 | } 18 | else { 19 | self 20 | } 21 | } 22 | 23 | 24 | @ViewBuilder 25 | func ifelse(_ condition: Bool, ifContent: (Self) -> Content, elseContent: (Self) -> Content) -> some View { 26 | if condition { 27 | ifContent(self) 28 | } 29 | else { 30 | elseContent(self) 31 | } 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /PyLeap/Views/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/PyLeap-iOS/02a0f52c0c08ab892098ee356abffe74a87904ff/PyLeap/Views/.DS_Store -------------------------------------------------------------------------------- /PyLeap/Views/Bluetooth Pairing View/BluetoothStatusView.swift: -------------------------------------------------------------------------------- 1 | 2 | // BluetoothStatusView.swift 3 | // Glider 4 | // 5 | // Created by Antonio García on 7/9/21. 6 | // 7 | 8 | import SwiftUI 9 | import FileTransferClient 10 | 11 | struct BluetoothStatusView: View { 12 | @State private var messageTitle: String 13 | @State private var message: String 14 | @State private var isActionHidden: Bool 15 | @StateObject private var model = BTConnectionViewModel() 16 | @EnvironmentObject var rootViewModel: RootViewModel 17 | 18 | init() { 19 | 20 | let bluetoothState = BleManager.shared.state 21 | 22 | var isActionHidden: Bool 23 | 24 | switch bluetoothState { 25 | 26 | case .unauthorized: 27 | messageTitle = "This app is not authorized to use Bluetooth Low Energy" 28 | message = "Bluetooth permission should be granted for the app to connect to a Bluetooth device" 29 | isActionHidden = false 30 | case .unsupported: 31 | messageTitle = "This device doesn't support Bluetooth" 32 | message = "Bluetooth support and specifically Bluetooth Low Energy support is needed to communicate with a Bluefruit Device" 33 | isActionHidden = true 34 | case .poweredOff: 35 | messageTitle = "Bluetooth is currently powered off" 36 | message = "Bluetooth should be enabled on your device for the app to connect to a Bluetooth device" 37 | isActionHidden = true 38 | default: 39 | DLog("Error: BluetoothStatusView in wrong state: \(bluetoothState)") 40 | messageTitle = "Bluetooth is not available" 41 | message = "Bluetooth should be enabled on your device for the app to connect to a Bluetooth device" 42 | isActionHidden = true 43 | break 44 | } 45 | 46 | let settingsUrl = URL(string: UIApplication.openSettingsURLString) 47 | self.isActionHidden = isActionHidden || settingsUrl == nil || !UIApplication.shared.canOpenURL(settingsUrl!) 48 | } 49 | 50 | var body: some View { 51 | 52 | VStack(spacing: 30) { 53 | Image("bluetooth") 54 | 55 | VStack(spacing: 16) { 56 | Text(messageTitle).bold() 57 | Text(message) 58 | } 59 | .font(Font.custom("ReadexPro-Regular", size: 20)) 60 | 61 | Button("Bluetooth permissions") { 62 | if let settingsUrl = URL(string: UIApplication.openSettingsURLString) { 63 | UIApplication.shared.open(settingsUrl) 64 | } 65 | } 66 | // .buttonStyle(MainButtonStyle()) 67 | .opacity(isActionHidden ? 0 : 1) 68 | } 69 | 70 | .onChange(of: model.destination) { destination in 71 | if destination == .fileTransfer { 72 | self.rootViewModel.goToFileTransfer() 73 | } 74 | } 75 | .padding(.horizontal) 76 | .multilineTextAlignment(.center) 77 | .foregroundColor(.gray) 78 | .edgesIgnoringSafeArea(.all) 79 | 80 | } 81 | 82 | } 83 | 84 | struct BluetoothStatusView_Previews: PreviewProvider { 85 | static var previews: some View { 86 | BluetoothStatusView() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /PyLeap/Views/Bluetooth Pairing View/CreditView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 5/16/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CreditView: View { 11 | 12 | @State private var alertVisible: Bool = false 13 | @Binding var isPresented: Bool 14 | 15 | 16 | var body: some View { 17 | ScrollView { 18 | HStack { 19 | Spacer() 20 | Button { 21 | isPresented = false 22 | print("Dismiss") 23 | } label: { 24 | Image(systemName: "xmark") 25 | .resizable() 26 | .foregroundColor(.black) 27 | 28 | .frame(width: 24, height: 24, alignment: .center) 29 | .padding(.top, 30) 30 | .padding(.trailing, 30) 31 | } 32 | 33 | } 34 | 35 | VStack { 36 | 37 | Image("adafruitLogoBlack") 38 | .resizable() 39 | .aspectRatio(contentMode: .fit) 40 | .frame(width: 201, height: 67, alignment: .center) 41 | .padding(.top, 70) 42 | .padding(.bottom, 40) 43 | 44 | VStack (alignment: .leading, spacing: 20) { 45 | 46 | Text(""" 47 | PyLeap is designed for use with specific devices using the CircuitPython BLE FileTransfer service. 48 | 49 | Follow the links below to purchase a compatible device from the Adafruit shop: 50 | """) 51 | 52 | Text(""" 53 | [• Circuit Playground Bluefruit](https://www.adafruit.com/product/4333) 54 | [• Adafruit CLUE](https://www.adafruit.com/product/4500) 55 | """) 56 | .underline() 57 | 58 | Text(""" 59 | Before you can use this app, you'll need to update your uf2 firmware file onto your device. Learn more in the [PyLeap Learn Guide](https://learn.adafruit.com/pyleap-app). 60 | 61 | **Acknowledgements** 62 | 63 | Portions of this Software may utilize the following copyrighted material, the use of which is hereby acknowledged. 64 | 65 | Zip 66 | Copyright(c) 2019 Nathan Moinvaziri 67 | """) 68 | Text("https://github.com/nmoinvaz/minizip") 69 | .underline() 70 | .padding(.top, -10) 71 | } 72 | 73 | // Text("") 74 | // Text(""" 75 | // 76 | // 77 | //Follow the links below to purchase a compatible device from the Adafruit shop: 78 | // 79 | //• [Circuit Playground Bluefruit](https://www.adafruit.com/product/4333) 80 | //• [Adafruit CLUE](https://www.adafruit.com/product/4500) 81 | // 82 | //Before you can use this app, you'll need to update your uf2 firmware file onto your device. Learn more in the [PyLeap Learn Guide](https://learn.adafruit.com/pyleap-app). 83 | // 84 | //**Acknowledgements** 85 | // 86 | //Portions of this Software may utilize the following copyrighted material, the use of which is hereby acknowledged. 87 | // 88 | //Zip 89 | //Copyright(c) 2019 Nathan Moinvaziri 90 | //https://github.com/nmoinvaz/minizip 91 | //""") 92 | 93 | } 94 | .font(Font.custom("ReadexPro-Regular", size: 16)) 95 | .padding(.horizontal, 30) 96 | .preferredColorScheme(.light) 97 | } 98 | } 99 | } 100 | struct CreditView_Previews: PreviewProvider { 101 | static var previews: some View { 102 | CreditView(isPresented: .constant(true)) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /PyLeap/Views/Bluetooth Pairing View/TroubleshootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TroubleshootView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 5/16/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TroubleshootView: View { 11 | var body: some View { 12 | VStack { 13 | Spacer() 14 | ZStack { 15 | Circle() 16 | .frame(width: 200, height: 200, alignment: .center) 17 | .foregroundColor(Color("pyleap_purple")) 18 | 19 | Image("warning_icon") 20 | .resizable() 21 | .frame(width: 75, height: 75, alignment: .center) 22 | } 23 | 24 | Spacer() 25 | 26 | VStack (alignment: .leading, spacing: 12) { 27 | Text("Hmm...") 28 | .bold() 29 | 30 | Text(""" 31 | We’re having trouble connecting to your device. 32 | """) 33 | 34 | .minimumScaleFactor(0.8) 35 | 36 | HStack { 37 | ZStack { 38 | 39 | Circle() 40 | .foregroundColor(Color("pyleap_purple")) 41 | .frame(width: 30, height: 30, alignment: .center) 42 | 43 | Text("1") 44 | .foregroundColor(.white) 45 | } 46 | 47 | Text("Go to your iOS device's Bluetooth settings.") 48 | .minimumScaleFactor(0.8) 49 | } 50 | 51 | HStack { 52 | ZStack { 53 | 54 | Circle() 55 | .foregroundColor(Color("pyleap_purple")) 56 | .frame(width: 30, height: 30, alignment: .center) 57 | 58 | Text("2") 59 | .foregroundColor(.white) 60 | } 61 | 62 | Text(#"Tap the information icon for your CIRCUITPY device and select "Forget This Device"."#) 63 | 64 | .minimumScaleFactor(0.8) 65 | } 66 | 67 | HStack { 68 | ZStack { 69 | 70 | Circle() 71 | .foregroundColor(Color("pyleap_purple")) 72 | .frame(width: 30, height: 30, alignment: .center) 73 | 74 | Text("3") 75 | .foregroundColor(.white) 76 | } 77 | 78 | Text("Return to PyLeap to continue.") 79 | 80 | .minimumScaleFactor(0.8) 81 | } 82 | 83 | } 84 | 85 | 86 | 87 | Button { 88 | 89 | UIApplication.shared.open(URL(string: "App-Prefs:root=Bluetooth")!) 90 | } label: { 91 | 92 | ZStack { 93 | Rectangle() 94 | .frame(width: 270, height: 50, alignment: .center) 95 | .cornerRadius(25) 96 | .foregroundColor(Color("pyleap_pink")) 97 | 98 | Text("Go To Settings") 99 | .font(Font.custom("ReadexPro-Regular", size: 25)) 100 | .foregroundColor(Color.white) 101 | .frame(height: 50) 102 | 103 | } 104 | } 105 | .padding(.top, 30) 106 | Spacer() 107 | .frame(height: 40) 108 | 109 | } 110 | .font(Font.custom("ReadexPro-Regular", size: 16)) 111 | .padding(.horizontal, 30) 112 | .preferredColorScheme(.light) 113 | } 114 | 115 | } 116 | 117 | 118 | struct TroubleshootView_Previews: PreviewProvider { 119 | static var previews: some View { 120 | TroubleshootView() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /PyLeap/Views/FillerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FillerView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 8/12/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FillerView: View { 11 | 12 | @StateObject private var model = StartupViewModel() 13 | @EnvironmentObject var rootViewModel: RootViewModel 14 | 15 | var body: some View { 16 | VStack { 17 | 18 | Image("pyleapLogo") 19 | .resizable() 20 | .aspectRatio(contentMode: .fit) 21 | .offset(y: -30) 22 | 23 | ProgressView() 24 | 25 | } 26 | .preferredColorScheme(.light) 27 | .padding(.horizontal, 30) 28 | .modifier(Alerts(activeAlert: $model.activeAlert, model: model)) 29 | .onAppear { 30 | model.setupBluetooth() 31 | } 32 | .onChange(of: model.isStartupFinished) { isStartupFinished in 33 | if isStartupFinished { 34 | rootViewModel.goToMain() 35 | } 36 | } 37 | } 38 | } 39 | 40 | 41 | private struct Alerts: ViewModifier { 42 | @Binding var activeAlert: StartupViewModel.ActiveAlert? 43 | @ObservedObject var model: StartupViewModel 44 | 45 | func body(content: Content) -> some View { 46 | content 47 | .alert(item: $activeAlert, content: { alert in 48 | switch alert { 49 | case .bluetoothUnsupported: 50 | return Alert( 51 | title: Text("Error"), 52 | message: Text("This device doesn't support Bluetooth Low Energy which is needed to connect to Bluefruit devices"), 53 | dismissButton: .cancel(Text("Ok")) { 54 | model.setupBluetooth() 55 | }) 56 | 57 | case .fileTransferErrorOnReconnect: 58 | return Alert( 59 | title: Text("Error"), 60 | message: Text("Error initializing FileTransfer service"), 61 | dismissButton: .cancel(Text("Ok")) { 62 | model.setupBluetooth() 63 | }) 64 | } 65 | }) 66 | } 67 | } 68 | 69 | 70 | struct FillerView_Previews: PreviewProvider { 71 | static var previews: some View { 72 | FillerView() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /PyLeap/Views/Onboarding Views/OnboardingBackgroundView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingBackgroundView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 7/6/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OnboardingBackgroundView: View { 11 | 12 | @State var scale: CGFloat = 1 13 | 14 | var body: some View { 15 | ZStack { 16 | ForEach(0..<20) { _ in 17 | Circle() 18 | .strokeBorder(Color(red: .random(in: 0...1), 19 | green: .random(in: 0...1), 20 | blue: .random(in: 0...1)), 21 | lineWidth: .random(in: 1...20)) 22 | .blendMode(.colorDodge) 23 | .animation(Animation.easeInOut(duration: 0.05) 24 | .repeatForever() 25 | .speed(.random(in: 0.05...0.9)) 26 | .delay(.random(in: 0...2)) 27 | ) 28 | .scaleEffect(self.scale * .random(in: 0.1...3)) 29 | .frame(width: .random(in: 20...100), 30 | height: CGFloat.random(in: 20...100), 31 | alignment: .center) 32 | .position(CGPoint(x: .random(in: 0...1112), 33 | y: .random(in: 0...834))) 34 | 35 | } 36 | }.onAppear { 37 | self.scale = 1.2 38 | } 39 | } 40 | } 41 | 42 | struct OnboardingBackgroundView_Previews: PreviewProvider { 43 | static var previews: some View { 44 | OnboardingBackgroundView() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /PyLeap/Views/Onboarding Views/OnboardingDataModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingDataModel.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 6/30/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct OnboardingDataModel { 11 | var image: String 12 | var heading: String 13 | var text: String 14 | } 15 | 16 | extension OnboardingDataModel { 17 | 18 | static var data: [OnboardingDataModel] = [ 19 | 20 | OnboardingDataModel(image: "Onboard1", heading: "Welcome", text: "PyLeap is an easy way to get projects from the Adafruit Learn Systems from your mobile device directly to your Adafruit device."), 21 | OnboardingDataModel(image: "Onboard2", heading: "Round Em'Up!", text: "Gathering Circuit Python project files just got a whole lot easier! We use Bundle Fly to gather Circuit Python files that you'll need for your project."), 22 | OnboardingDataModel(image: "Onboard3", heading: "Send Em Off!", text: "Effortlessly send over your Circuit Python files to your BLE hardware!") 23 | 24 | ] 25 | 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /PyLeap/Views/Onboarding Views/OnboardingStepView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingStepView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 6/30/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OnboardingStepView: View { 11 | var data: OnboardingDataModel 12 | @State private var isAnimating: Bool = false 13 | var body: some View { 14 | 15 | 16 | VStack { 17 | Image(data.image) 18 | .resizable() 19 | .scaledToFit() 20 | // .offset(x: 0, y: 100) 21 | .scaleEffect(isAnimating ? 1 : 0.6) 22 | .onAppear(perform: { 23 | isAnimating = false 24 | withAnimation(.easeOut(duration: 0.5)) { 25 | self.isAnimating = true 26 | 27 | } 28 | }) 29 | 30 | Text(data.heading) 31 | .font(.largeTitle) 32 | .bold() 33 | .foregroundColor(Color(#colorLiteral(red: 0.5275210142, green: 0.4204645753, blue: 0.6963143945, alpha: 1))) 34 | .font(.custom("SF Pro", size: 15)) 35 | 36 | Text(data.text) 37 | .font(.headline) 38 | .multilineTextAlignment(.center) 39 | .frame(maxWidth: 350) 40 | .foregroundColor(Color(#colorLiteral(red: 0.5275210142, green: 0.4204645753, blue: 0.6963143945, alpha: 1))) 41 | .opacity(/*@START_MENU_TOKEN@*/0.8/*@END_MENU_TOKEN@*/) 42 | } 43 | // Always on Light Mode. 44 | // .preferredColorScheme(.light) 45 | .padding() 46 | } 47 | } 48 | 49 | struct OnboardingStepView_Previews: PreviewProvider { 50 | static var data = OnboardingDataModel.data[0] 51 | static var previews: some View { 52 | OnboardingStepView(data: data) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /PyLeap/Views/Paired View/DownloadState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadState.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 4/28/22. 6 | // 7 | 8 | import Foundation 9 | 10 | enum DownloadState { 11 | case idle 12 | case downloading 13 | case transferring 14 | case complete 15 | case failed 16 | 17 | } 18 | 19 | enum InternetState { 20 | case noInternetConnection 21 | case connectedToInternet 22 | } 23 | 24 | enum ErrorConnecting { 25 | case noError 26 | case peerInformationError 27 | } 28 | -------------------------------------------------------------------------------- /PyLeap/Views/Reusable Views/DemoViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListCell.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 3/14/22. 6 | // 7 | import SwiftUI 8 | import Foundation 9 | 10 | struct DemoViewCell: View { 11 | 12 | @EnvironmentObject var expandedState : ExpandedBLECellState 13 | 14 | let result : ResultItem 15 | 16 | @State var isExpanded: Bool = false { 17 | didSet { 18 | onViewGeometryChanged() 19 | } 20 | } 21 | 22 | @Binding var isConnected: Bool 23 | @Binding var deviceInfo: String 24 | 25 | let onViewGeometryChanged: ()->Void 26 | 27 | var body: some View { 28 | content 29 | .frame(maxWidth: .infinity) 30 | } 31 | 32 | private var content: some View { 33 | VStack(alignment: .leading, spacing: 8) { 34 | header 35 | 36 | if isExpanded { 37 | 38 | Group { 39 | DemoSubview(bindingString: $deviceInfo, result: result, isConnected: $isConnected) 40 | } 41 | 42 | } 43 | } 44 | } 45 | 46 | private var header: some View { 47 | 48 | HStack { 49 | Text(result.projectName) 50 | .font(Font.custom("ReadexPro-Regular", size: 24)) 51 | .padding(8) 52 | .foregroundColor(.white) 53 | 54 | Spacer() 55 | 56 | Image(systemName: "chevron.down") 57 | .resizable() 58 | .frame(width: 30, height: 15, alignment: .center) 59 | .foregroundColor(.white) 60 | .padding(.trailing, 30) 61 | } 62 | 63 | .padding(.vertical, 5) 64 | .padding(.leading) 65 | .frame(maxWidth: .infinity) 66 | .background(Color("pyleap_purple")) 67 | .onTapGesture { 68 | expandedState.currentCell = result.bundleLink 69 | 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /PyLeap/Views/Reusable Views/ScrollRefreshableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollRefreshableView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 9/26/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ScrollRefreshableView: View { 11 | 12 | var content: Content 13 | var onRefresh: ()->() 14 | 15 | 16 | init(title: String, tintColor: Color, @ViewBuilder content: @escaping () -> Content, onRefresh: @escaping () -> ()) { 17 | self.content = content() 18 | self.onRefresh = onRefresh 19 | 20 | UIRefreshControl.appearance().attributedTitle = NSAttributedString(string: title) 21 | UIRefreshControl.appearance().tintColor = UIColor(tintColor) 22 | } 23 | 24 | var body: some View { 25 | List { 26 | 27 | 28 | content 29 | .listRowSeparator(.hidden) 30 | .listRowBackground(Color.clear) 31 | .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) 32 | 33 | 34 | 35 | } 36 | 37 | .listStyle(.plain) 38 | .refreshable { 39 | onRefresh() 40 | } 41 | } 42 | 43 | } 44 | 45 | struct ScrollRefreshableView_Previews: PreviewProvider { 46 | static var previews: some View { 47 | MainHeaderView() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PyLeap/Views/Reusable Views/SubCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubCellViewModel.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 4/1/22. 6 | // 7 | 8 | import Foundation 9 | 10 | class SubCellViewModel: ObservableObject { 11 | 12 | @Published var projectDownloaded = false 13 | @Published var failedProjectLaunch = false 14 | @Published var isConnectedToInternet = false 15 | 16 | let directoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] 17 | var manager = FileManager.default 18 | 19 | var networkMonitor = NetworkMonitor() 20 | 21 | init() { 22 | internetMonitoring() 23 | } 24 | 25 | func deleteStoredFilesInFM () { 26 | print("\(#function) @Line: \(#line)") 27 | do { 28 | try manager.removeItem(at: directoryPath) 29 | 30 | } catch { 31 | print(error) 32 | } 33 | } 34 | 35 | func find(projectWith title: String) { 36 | 37 | let nestedFolderURL = directoryPath.appendingPathComponent(title) 38 | 39 | if manager.fileExists(atPath: nestedFolderURL.relativePath) { 40 | print("\(title) - Exists") 41 | projectDownloaded = true 42 | } else { 43 | print("\(title) - Does not exist.") 44 | projectDownloaded = false 45 | } 46 | } 47 | 48 | func internetMonitoring() { 49 | 50 | networkMonitor.startMonitoring() 51 | networkMonitor.monitor.pathUpdateHandler = { path in 52 | if path.status == .satisfied { 53 | print("Connected to internet.") 54 | 55 | DispatchQueue.main.async { 56 | // self.showAlert = false 57 | self.isConnectedToInternet = true 58 | } 59 | } else { 60 | print("No connection.") 61 | DispatchQueue.main.async { 62 | // self.showAlert = true 63 | self.isConnectedToInternet = false 64 | } 65 | } 66 | print("isExpensive: \(path.isExpensive)") 67 | } 68 | } 69 | 70 | func searchPathForProject(nameOf project: String) { 71 | var manager = FileManager.default 72 | 73 | let nestedFolderURL = directoryPath.appendingPathComponent(project) 74 | 75 | if manager.fileExists(atPath: nestedFolderURL.relativePath) { 76 | print("\(project) - Exist") 77 | projectDownloaded = true 78 | } else { 79 | print("Does not exist - \(project)") 80 | projectDownloaded = false 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /PyLeap/Views/Root View/Config.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Config.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 4/25/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Config { 11 | // MARK: - Screenshot Mode 12 | public static var isSimulatingBluetooth: Bool { 13 | #if SIMULATEBLUETOOTH 14 | return true 15 | #else 16 | return false 17 | #endif 18 | } 19 | 20 | public static let areFastlaneSnapshotsRunning = UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") 21 | } 22 | -------------------------------------------------------------------------------- /PyLeap/Views/Root View/RootViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootViewModel.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 6/30/21. 6 | // 7 | 8 | import Foundation 9 | import FileTransferClient 10 | 11 | public class RootViewModel: ObservableObject { 12 | 13 | // public var shared = RootViewModel() 14 | 15 | enum Destination { 16 | //case splash 17 | case main 18 | case startup 19 | case onboard 20 | case bluetoothPairing 21 | case bluetoothStatus 22 | case fileTransfer 23 | case wifi 24 | case settings 25 | case bleSettings 26 | case mainSelection 27 | case wifiSelection 28 | case wifiPairingTutorial 29 | case wifiServiceSelection 30 | case selection 31 | 32 | } 33 | 34 | @Published var destination: Destination = AppEnvironment.isRunningTests ? .mainSelection : .startup 35 | 36 | 37 | func goToTest(){ 38 | //destination = .test 39 | } 40 | 41 | func goToWiFiServiceSelection() { 42 | destination = .wifiServiceSelection 43 | } 44 | 45 | func goToWifiPairingTutorial() { 46 | destination = .wifiPairingTutorial 47 | } 48 | 49 | func goToWiFiSelection() { 50 | destination = .wifiSelection 51 | } 52 | 53 | func goToWifiView() { 54 | destination = .wifi 55 | } 56 | 57 | func goTobluetoothPairing() { 58 | destination = .bluetoothPairing 59 | } 60 | 61 | func goToSelection(){ 62 | destination = .selection 63 | } 64 | 65 | func goToMainSelection(){ 66 | destination = .mainSelection 67 | } 68 | 69 | func goToMain(){ 70 | 71 | if FileTransferConnectionManager.shared.selectedClient != nil { 72 | destination = .fileTransfer 73 | } 74 | else { 75 | destination = .main 76 | } 77 | } 78 | 79 | func backToMain() { 80 | destination = .main 81 | } 82 | 83 | func goToStartup(){ 84 | destination = .startup 85 | } 86 | 87 | func goToOnboarding() { 88 | destination = .onboard 89 | } 90 | 91 | func goToFileTransfer() { 92 | destination = .fileTransfer 93 | } 94 | 95 | func goToSettings(content: SettingState){ 96 | destination = .settings 97 | } 98 | 99 | func goToBLESettings(){ 100 | destination = .bleSettings 101 | } 102 | 103 | func showWarningIfBluetoothStateIsNotReady() { 104 | let bluetoothState = BleManager.shared.state 105 | let shouldShowBluetoothDialog = bluetoothState == .poweredOff || bluetoothState == .unsupported || bluetoothState == .unauthorized 106 | 107 | if shouldShowBluetoothDialog { 108 | destination = .bluetoothStatus 109 | } 110 | else if destination == .bluetoothStatus { 111 | goToStartup() 112 | } 113 | } 114 | 115 | 116 | 117 | } 118 | 119 | -------------------------------------------------------------------------------- /PyLeap/Views/Startup View/StartupView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartupView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 7/25/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StartupView: View { 11 | 12 | @StateObject private var model = StartupViewModel() 13 | @EnvironmentObject var rootViewModel: RootViewModel 14 | @AppStorage("onboarding") var onboardingSeen = false 15 | 16 | var body: some View { 17 | VStack { 18 | Text("Restoring Connection...") 19 | .bold() 20 | .foregroundColor(Color.purple) 21 | 22 | ProgressView() 23 | .scaleEffect(1.5) 24 | .progressViewStyle(CircularProgressViewStyle(tint: Color.white)) 25 | } 26 | .modifier(Alerts(activeAlert: $model.activeAlert, model: model)) 27 | .onAppear { 28 | print("Startup View Appeared.") 29 | model.setupBluetooth() 30 | } 31 | .onChange(of: model.isStartupFinished) { isStartupFinished in 32 | if isStartupFinished { 33 | rootViewModel.goToMain() 34 | } 35 | } 36 | } 37 | 38 | 39 | private struct Alerts: ViewModifier { 40 | @Binding var activeAlert: StartupViewModel.ActiveAlert? 41 | @ObservedObject var model: StartupViewModel 42 | 43 | func body(content: Content) -> some View { 44 | content 45 | .alert(item: $activeAlert, content: { alert in 46 | switch alert { 47 | case .bluetoothUnsupported: 48 | return Alert( 49 | title: Text("Error"), 50 | message: Text("This device doesn't support Bluetooth Low Energy which is needed to connect to Bluefruit devices"), 51 | dismissButton: .cancel(Text("Ok")) { 52 | model.setupBluetooth() 53 | }) 54 | 55 | case .fileTransferErrorOnReconnect: 56 | return Alert( 57 | title: Text("Error"), 58 | message: Text("Error initializing FileTransfer service"), 59 | dismissButton: .cancel(Text("Ok")) { 60 | model.setupBluetooth() 61 | }) 62 | } 63 | }) 64 | } 65 | } 66 | 67 | } 68 | 69 | struct StartupView_Previews: PreviewProvider { 70 | static var previews: some View { 71 | StartupView() 72 | .environmentObject(RootViewModel()) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /PyLeap/Views/UI Components/Blinka Animation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScanningView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 7/6/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BlinkaAnimationView: View { 11 | 12 | @State private var isAnimating = false 13 | @State private var showProgress = false 14 | var foreverAnimation: Animation { 15 | Animation.linear(duration: 4.0) 16 | .repeatForever(autoreverses: false) 17 | } 18 | 19 | var height: CGFloat 20 | var width: CGFloat 21 | 22 | var body: some View { 23 | 24 | ZStack { 25 | Image("BlinkaLoading") 26 | .resizable(resizingMode: .stretch) 27 | .aspectRatio(contentMode: .fit) 28 | .frame(width: width, height: height) 29 | .rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0)) 30 | .animation(self.isAnimating ? foreverAnimation : .default) 31 | 32 | .onAppear { self.isAnimating = true } 33 | 34 | } 35 | } 36 | } 37 | 38 | struct ScanningView_Previews: PreviewProvider { 39 | static var previews: some View { 40 | BlinkaAnimationView(height: 300, width: 300) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /PyLeap/Views/UI Components/Buttons.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Buttons.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 4/29/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RunItButton: View { 11 | 12 | var body: some View { 13 | 14 | ZStack { 15 | Rectangle() 16 | .frame(width: 270, height: 50, alignment: .center) 17 | .cornerRadius(25) 18 | .foregroundColor(Color("pyleap_pink")) 19 | 20 | Text("Run") 21 | .font(Font.custom("ReadexPro-Regular", size: 25)) 22 | .foregroundColor(Color.white) 23 | .frame(height: 50) 24 | 25 | } 26 | 27 | } 28 | } 29 | 30 | struct PairingTutorialButton: View { 31 | 32 | var body: some View { 33 | 34 | ZStack { 35 | Rectangle() 36 | .frame(width: 270, height: 50, alignment: .center) 37 | .cornerRadius(25) 38 | .foregroundColor(Color("bluetooth_button_color")) 39 | 40 | Text("Pairing Tutorial") 41 | .font(Font.custom("ReadexPro-Regular", size: 25)) 42 | .foregroundColor(Color.white) 43 | .frame(height: 50) 44 | 45 | } 46 | 47 | } 48 | } 49 | 50 | struct LearnGuideButton: View { 51 | var body: some View { 52 | 53 | ZStack { 54 | RoundedRectangle(cornerRadius: 25) 55 | .stroke((Color("pyleap_purple")), lineWidth: 3.5) 56 | .frame(width: 270, height: 50, alignment: .center) 57 | 58 | Text("Learn Guide") 59 | .font(.custom("ReadexPro-Regular", size: 25)) 60 | .foregroundColor(Color("pyleap_purple")) 61 | } 62 | } 63 | } 64 | 65 | 66 | struct ConnectButton: View { 67 | var body: some View { 68 | 69 | ZStack { 70 | ZStack { 71 | Rectangle() 72 | .frame(width: 270, height: 50, alignment: .center) 73 | .cornerRadius(25) 74 | .foregroundColor(Color("adafruit_blue")) 75 | 76 | Text("Connect") 77 | .font(Font.custom("ReadexPro-Regular", size: 25)) 78 | .foregroundColor(Color.white) 79 | .frame(height: 50) 80 | 81 | } 82 | } 83 | 84 | } 85 | } 86 | 87 | struct DownloadingButton: View { 88 | var body: some View { 89 | 90 | ZStack { 91 | ZStack { 92 | Rectangle() 93 | .frame(width: 270, height: 50, alignment: .center) 94 | .cornerRadius(25) 95 | .foregroundColor(Color(.gray)) 96 | 97 | Text("Downloading...") 98 | .font(Font.custom("ReadexPro-Regular", size: 25)) 99 | .foregroundColor(Color.white) 100 | .frame(height: 50) 101 | 102 | } 103 | } 104 | 105 | } 106 | } 107 | 108 | struct CompleteButton: View { 109 | var body: some View { 110 | 111 | ZStack { 112 | 113 | Rectangle() 114 | .frame(width: 270, height: 50, alignment: .center) 115 | .cornerRadius(25) 116 | .foregroundColor(Color("pyleap_green")) 117 | 118 | Image("check") 119 | .resizable() 120 | .scaledToFit() 121 | .frame(width: 30, height: 30) 122 | } 123 | 124 | 125 | } 126 | } 127 | 128 | struct FailedButton: View { 129 | var body: some View { 130 | 131 | ZStack { 132 | Rectangle() 133 | .frame(width: 270, height: 50, alignment: .center) 134 | .cornerRadius(25) 135 | .foregroundColor(Color("pyleap_burg")) 136 | 137 | Image(systemName: "xmark") 138 | .resizable() 139 | .foregroundColor(.white) 140 | .scaledToFit() 141 | .frame(width: 30, height: 30) 142 | } 143 | } 144 | } 145 | 146 | struct Buttons_Previews: PreviewProvider { 147 | static var previews: some View { 148 | ConnectButton() 149 | RunItButton() 150 | DownloadingButton() 151 | CompleteButton() 152 | FailedButton() 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /PyLeap/Views/UI Components/HeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 4/25/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HeaderView: View { 11 | @State var showSheetView = false 12 | @EnvironmentObject var rootViewModel: RootViewModel 13 | 14 | 15 | 16 | var body: some View { 17 | 18 | 19 | HStack (alignment: .center, spacing: 0) { 20 | 21 | Image(systemName: "gearshape") 22 | .resizable() 23 | .frame(width: 25, height: 25, alignment: .center) 24 | .offset(y: 15) 25 | .padding(.leading, CGFloat(20)) 26 | .foregroundColor(.clear) 27 | 28 | Spacer() 29 | 30 | Image("pyleap_logo_white") 31 | .resizable() 32 | .scaledToFit() 33 | .frame(width: 125, height: 125) 34 | .offset(y: 12) 35 | 36 | Spacer() 37 | 38 | Button { 39 | rootViewModel.goToBLESettings() 40 | } label: { 41 | Image(systemName: "plus") 42 | .resizable() 43 | .frame(width: 25, height: 25, alignment: .center) 44 | .offset(y: 15) 45 | .padding(.trailing, CGFloat(20)) 46 | .foregroundColor(.white) 47 | } 48 | 49 | } 50 | 51 | .frame(maxWidth: .infinity) 52 | .frame(maxHeight: 120) 53 | .background(Color("pyleap_gray")) 54 | 55 | 56 | } 57 | } 58 | 59 | struct MainHeaderView: View { 60 | @State var showSheetView = false 61 | @EnvironmentObject var rootViewModel: RootViewModel 62 | var body: some View { 63 | 64 | VStack { 65 | HStack { 66 | 67 | Spacer() 68 | Image("pyleap_logo_white") 69 | .resizable() 70 | .scaledToFit() 71 | .frame(width: 125, height: 125) 72 | .offset(y: 12) 73 | .padding(.leading, 60) 74 | 75 | Spacer() 76 | 77 | Button { 78 | self.showSheetView.toggle() 79 | } label: { 80 | Image(systemName: "info.circle") 81 | .resizable() 82 | .frame(width: 30, height: 30, alignment: .center) 83 | .offset(y: 15) 84 | .foregroundColor(.white) 85 | }.sheet(isPresented: $showSheetView) { 86 | CreditView(isPresented: $showSheetView) 87 | } 88 | 89 | .padding() 90 | } 91 | Spacer() 92 | } 93 | .frame(maxWidth: .infinity) 94 | .frame(maxHeight: 120) 95 | .background(Color("pyleap_gray")) 96 | 97 | 98 | } 99 | } 100 | 101 | 102 | 103 | struct HeaderView_Previews: PreviewProvider { 104 | 105 | static var previews: some View { 106 | HeaderView() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /PyLeap/Views/UI Components/OnAnimationComplete.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnAnimationComplete.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 5/2/22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | /// An animatable modifier that is used for observing animations for a given animatable value. 11 | struct AnimationCompletionObserverModifier: AnimatableModifier where Value: VectorArithmetic { 12 | 13 | /// While animating, SwiftUI changes the old input value to the new target value using this property. This value is set to the old value until the animation completes. 14 | var animatableData: Value { 15 | didSet { 16 | notifyCompletionIfFinished() 17 | } 18 | } 19 | 20 | /// The target value for which we're observing. This value is directly set once the animation starts. During animation, `animatableData` will hold the oldValue and is only updated to the target value once the animation completes. 21 | private var targetValue: Value 22 | 23 | /// The completion callback which is called once the animation completes. 24 | private var completion: () -> Void 25 | 26 | init(observedValue: Value, completion: @escaping () -> Void) { 27 | self.completion = completion 28 | self.animatableData = observedValue 29 | targetValue = observedValue 30 | } 31 | 32 | /// Verifies whether the current animation is finished and calls the completion callback if true. 33 | private func notifyCompletionIfFinished() { 34 | guard animatableData == targetValue else { return } 35 | 36 | /// Dispatching is needed to take the next runloop for the completion callback. 37 | /// This prevents errors like "Modifying state during view update, this will cause undefined behavior." 38 | DispatchQueue.main.async { 39 | self.completion() 40 | } 41 | } 42 | 43 | func body(content: Content) -> some View { 44 | /// We're not really modifying the view so we can directly return the original input value. 45 | return content 46 | } 47 | } 48 | 49 | extension View { 50 | 51 | /// Calls the completion handler whenever an animation on the given value completes. 52 | /// - Parameters: 53 | /// - value: The value to observe for animations. 54 | /// - completion: The completion callback to call once the animation completes. 55 | /// - Returns: A modified `View` instance with the observer attached. 56 | func onAnimationCompleted(for value: Value, completion: @escaping () -> Void) -> ModifiedContent> { 57 | return modifier(AnimationCompletionObserverModifier(observedValue: value, completion: completion)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /PyLeap/Views/UI Components/SearchBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBarView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 8/12/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SearchBarView: View { 11 | 12 | @State private var text: String = "" 13 | @State private var isEditing = false 14 | 15 | var body: some View { 16 | //MARK:- Search Bar UI 17 | 18 | HStack { 19 | 20 | Spacer() 21 | 22 | Button(action: { 23 | print("Do something") 24 | }, label: { 25 | Image(systemName:"plus") 26 | .font(.title) 27 | }) 28 | 29 | } 30 | 31 | HStack { 32 | 33 | TextField("Search ...", text: $text) 34 | .padding(7) 35 | .padding(.horizontal, 25) 36 | .background(Color(.systemGray6)) 37 | .cornerRadius(8) 38 | .padding(.horizontal, 10) 39 | .onTapGesture { 40 | self.isEditing = true 41 | } 42 | 43 | if isEditing { 44 | Button(action: { 45 | self.isEditing = false 46 | self.text = "" 47 | 48 | }) { 49 | Text("Cancel") 50 | } 51 | .padding(.trailing, 10) 52 | .transition(.move(edge: .trailing)) 53 | .animation(.default) 54 | } 55 | } 56 | .overlay( 57 | HStack { 58 | Image(systemName: "magnifyingglass") 59 | .foregroundColor(.gray) 60 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) 61 | .padding(.leading, 15) 62 | 63 | if isEditing { 64 | Button(action: { 65 | self.text = "" 66 | }) { 67 | Image(systemName: "multiply.circle.fill") 68 | .foregroundColor(.gray) 69 | .padding(.trailing, 8) 70 | } 71 | } 72 | } 73 | ) 74 | 75 | } 76 | } 77 | 78 | 79 | struct SearchBarView_Previews: PreviewProvider { 80 | static var previews: some View { 81 | SearchBarView() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /PyLeap/Views/UI Components/SubHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubHeaderView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 4/25/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SubHeaderView: View { 11 | var body: some View { 12 | HStack { 13 | 14 | Text("Browse available WiFi PyLeap Projects") 15 | .fixedSize(horizontal: false, vertical: true) 16 | .multilineTextAlignment(/*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) 17 | .font(Font.custom("ReadexPro-Regular", size: 25)) 18 | 19 | } 20 | .padding(.vertical,30) 21 | } 22 | } 23 | 24 | struct MainSubHeaderView: View { 25 | let device: String 26 | 27 | var body: some View { 28 | 29 | 30 | HStack { 31 | 32 | Text("Pick a project to run on your \(device)") 33 | .fixedSize(horizontal: false, vertical: true) 34 | .multilineTextAlignment(/*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) 35 | .font(Font.custom("ReadexPro-Regular", size: 25)) 36 | 37 | } 38 | .padding(.vertical,30) 39 | } 40 | } 41 | 42 | 43 | struct SubHeaderView_Previews: PreviewProvider { 44 | static var previews: some View { 45 | SubHeaderView() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /PyLeap/Views/UI Components/WifiHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WifiHeaderView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 9/7/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WifiHeaderView: View { 11 | @Environment(\.presentationMode) var presentationMode 12 | @State var showSheetView = false 13 | @EnvironmentObject var rootViewModel: RootViewModel 14 | 15 | var body: some View { 16 | 17 | VStack { 18 | 19 | 20 | 21 | HStack (alignment: .center, spacing: 0) { 22 | 23 | Image(systemName: "gearshape") 24 | .resizable() 25 | .frame(width: 25, height: 25, alignment: .center) 26 | .offset(y: 15) 27 | .padding(.leading, CGFloat(20)) 28 | .foregroundColor(.clear) 29 | 30 | Spacer() 31 | 32 | Image("pyleap_logo_white") 33 | .resizable() 34 | .scaledToFit() 35 | .frame(width: 125, height: 125) 36 | .offset(y: 12) 37 | 38 | Spacer() 39 | 40 | Button { 41 | rootViewModel.goToSettings(content: .wifi) 42 | } label: { 43 | Image(systemName: "plus") 44 | .resizable() 45 | .frame(width: 25, height: 25, alignment: .center) 46 | .offset(y: 15) 47 | .padding(.trailing, CGFloat(20)) 48 | .foregroundColor(.white) 49 | } 50 | 51 | } 52 | Spacer() 53 | } 54 | .frame(maxWidth: .infinity) 55 | .frame(maxHeight: 120) 56 | .background(Color("pyleap_gray")) 57 | 58 | 59 | } 60 | } 61 | 62 | struct WifiHeaderView_Previews: PreviewProvider { 63 | static var previews: some View { 64 | WifiHeaderView() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /PyLeap/Views/Unpaired View/MainSelectionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainSelectionViewModel.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 9/29/22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import Network 11 | import Combine 12 | 13 | class InternetConnectionManager: ObservableObject { 14 | 15 | private let monitor = NWPathMonitor() 16 | private let queue = DispatchQueue(label: "InternetConnectionMonitor") 17 | @Published var isConnected = false 18 | 19 | init() { 20 | 21 | startMonitoring(completion: { 22 | monitor.pathUpdateHandler = { path in 23 | 24 | DispatchQueue.main.async { 25 | let newIsConnected = path.status == .satisfied 26 | if self.isConnected != newIsConnected { 27 | self.isConnected = newIsConnected 28 | print("net: \(path.status) \(self.isConnected)") 29 | } 30 | } 31 | } 32 | }) 33 | } 34 | 35 | func startMonitoring(completion:()->Void) { 36 | print("Start Monitoring Network") 37 | monitor.start(queue: queue) 38 | completion() 39 | 40 | } 41 | 42 | deinit { 43 | print("Network Deinit") 44 | monitor.cancel() 45 | } 46 | } 47 | 48 | class MainSelectionViewModel: ObservableObject { 49 | 50 | @ObservedObject var networkModel = NetworkService() 51 | @ObservedObject var networkMonitor = InternetConnectionManager() 52 | 53 | 54 | let fileManager = FileManager.default 55 | 56 | let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! 57 | 58 | let dataStore = DataStore() 59 | 60 | @Published var pdemos : [ResultItem] = [] 61 | var networkMonitorCancellable: AnyCancellable? 62 | 63 | init() { 64 | let fileURL = documentsDirectory.appendingPathComponent("StandardPyLeapProjects.json") 65 | 66 | 67 | networkMonitorCancellable = networkMonitor.$isConnected.sink { isConnected in 68 | if isConnected { 69 | print("The device is currently connected to the internet.") 70 | // Perform some action when the device is connected to the internet. 71 | self.networkModel.fetch { 72 | self.pdemos = self.dataStore.loadDefaultList() 73 | } 74 | 75 | } else { 76 | print("The device is not currently connected to the internet.") 77 | // Perform some action when the device is not connected to the internet. 78 | print("Loading cached remote data.") 79 | self.pdemos = self.dataStore.loadDefaultList() 80 | 81 | 82 | } 83 | } 84 | 85 | } 86 | 87 | 88 | } 89 | -------------------------------------------------------------------------------- /PyLeap/Views/Unpaired View/SelectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectionView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 10/24/22. 6 | // 7 | 8 | import SwiftUI 9 | import FileTransferClient 10 | 11 | struct SelectionView: View { 12 | @EnvironmentObject var rootViewModel: RootViewModel 13 | @ObservedObject var connectionManager = FileTransferConnectionManager.shared 14 | 15 | var body: some View { 16 | 17 | VStack { 18 | 19 | HStack { 20 | Button { 21 | // rootViewModel.goToSelection() 22 | 23 | } label: { 24 | Image(systemName: "arrow.backward") 25 | .resizable() 26 | .frame(width: 25, height: 25, alignment: .center) 27 | .offset(y: 15) 28 | .foregroundColor(.clear) 29 | } 30 | .padding() 31 | 32 | Spacer() 33 | } 34 | .padding(.top, 15) 35 | 36 | 37 | Image("pyleapLogo") 38 | .resizable() 39 | .aspectRatio(contentMode: .fit) 40 | .padding(.top, 100) 41 | .padding(.horizontal, 60) 42 | 43 | Text("What type of device do you want to connect to?") 44 | .font(Font.custom("ReadexPro-Regular", size: 36)) 45 | .minimumScaleFactor(0.01) 46 | .multilineTextAlignment(.center) 47 | .padding(.top, 100) 48 | .padding(.horizontal, 30) 49 | 50 | Spacer() 51 | 52 | VStack { 53 | 54 | Button { 55 | rootViewModel.goToWiFiSelection() 56 | } label: { 57 | Text("Wifi") 58 | .font(Font.custom("ReadexPro-Regular", size: 25)) 59 | .foregroundColor(Color.white) 60 | .frame(width: 270, height: 50, alignment: .center) 61 | .background(Color("pyleap_pink")) 62 | .clipShape(Capsule()) 63 | .padding(5) 64 | } 65 | 66 | 67 | Button { 68 | rootViewModel.goTobluetoothPairing() 69 | } label: { 70 | Text("Bluetooth") 71 | .font(Font.custom("ReadexPro-Regular", size: 25)) 72 | .foregroundColor(Color.white) 73 | .frame(width: 270, height: 50, alignment: .center) 74 | .background(Color("bluetooth_button_color")) 75 | .clipShape(Capsule()) 76 | .padding(5) 77 | } 78 | 79 | 80 | 81 | Button { 82 | rootViewModel.backToMain() 83 | } label: { 84 | Text("I Don't Know") 85 | .font(Font.custom("ReadexPro-Regular", size: 25)) 86 | .foregroundColor(Color.white) 87 | .padding(.horizontal, 60) 88 | .minimumScaleFactor(0.1) 89 | .frame(width: 270, height: 50, alignment: .center) 90 | .background(Color.gray) 91 | .clipShape(Capsule()) 92 | .padding(5) 93 | } 94 | 95 | 96 | 97 | Button { 98 | rootViewModel.goTobluetoothPairing() 99 | } label: { 100 | Text("Reconnect to a device") 101 | .font(Font.custom("ReadexPro-Regular", size: 25)) 102 | .frame(width: 270, height: 50, alignment: .center) 103 | .foregroundColor(.blue) 104 | } 105 | 106 | 107 | 108 | } 109 | 110 | 111 | } 112 | .padding(.bottom, 60) 113 | 114 | } 115 | } 116 | 117 | struct SelectionView_Previews: PreviewProvider { 118 | static var previews: some View { 119 | SelectionView() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /PyLeap/Views/WebView/WebView Content.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebView Content.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 3/15/22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import WebKit 11 | 12 | struct WebView : UIViewRepresentable { 13 | 14 | private let learnGuideLink: URLRequest 15 | 16 | init(_ name: URLRequest) { 17 | self.learnGuideLink = name 18 | } 19 | 20 | var request: URLRequest { 21 | get { 22 | return URLRequest(url: learnGuideLink.url!) 23 | } 24 | } 25 | 26 | func makeUIView(context: Context) -> WKWebView { 27 | 28 | return WKWebView() 29 | } 30 | 31 | func updateUIView(_ uiView: WKWebView, context: Context) { 32 | uiView.load(request) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/BasicCredentials.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicCredentials.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 8/22/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct BasicCredentials: Hashable, Codable { 11 | public let username: String 12 | public let password: String 13 | 14 | public init(username: String, password: String) { 15 | self.username = "" 16 | self.password = "passw0rd" 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/CircuitPythonService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircuitPythonService.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 8/24/22. 6 | // 7 | 8 | import Foundation 9 | 10 | class CircuitPythonService:Equatable{ 11 | 12 | var netService:NetService 13 | var ipAddress:String 14 | 15 | init(netService:NetService, ipAddress:String) { 16 | self.netService = netService 17 | self.ipAddress = ipAddress 18 | } 19 | 20 | func getAddress() -> String { 21 | return ipAddress 22 | } 23 | 24 | func getPort() -> Int{ 25 | return netService.port 26 | } 27 | 28 | func getName() -> String{ 29 | return netService.name 30 | } 31 | 32 | } 33 | 34 | func == (lhs: CircuitPythonService, rhs: CircuitPythonService) -> Bool { 35 | return (lhs.netService == rhs.netService) && (lhs.ipAddress == rhs.ipAddress) 36 | } 37 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/CircuitPythonType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircuitPythonType.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 9/8/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CircuitPythonType{ 11 | static let serviceType = "_circuitpython._tcp." 12 | static let servicePort = 80 13 | static let serviceDomain = "local" 14 | } 15 | 16 | struct AdafruitInfo { 17 | static let baseURL = "https://adafruit.github.io/pyleap.github.io/pyleapProjects.json" 18 | } 19 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/LocalNetworkAuth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalNetworkAuth.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 9/30/22. 6 | // 7 | 8 | import Foundation 9 | import Network 10 | 11 | public class LocalNetworkAuthorization: NSObject { 12 | private var browser: NWBrowser? 13 | private var netService: NetService? 14 | private var completion: ((Bool) -> Void)? 15 | 16 | public func requestAuthorization(completion: @escaping (Bool) -> Void) { 17 | self.completion = completion 18 | 19 | // Create parameters, and allow browsing over peer-to-peer link. 20 | let parameters = NWParameters() 21 | parameters.includePeerToPeer = true 22 | 23 | // Browse for a custom service type. 24 | let browser = NWBrowser(for: .bonjour(type: "_bonjour._tcp", domain: nil), using: parameters) 25 | self.browser = browser 26 | browser.stateUpdateHandler = { newState in 27 | switch newState { 28 | case .failed(let error): 29 | print(error.localizedDescription) 30 | case .ready, .cancelled: 31 | break 32 | case let .waiting(error): 33 | print("Local network permission has been denied: \(error)") 34 | self.reset() 35 | self.completion?(false) 36 | default: 37 | break 38 | } 39 | } 40 | 41 | self.netService = NetService(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100) 42 | self.netService?.delegate = self 43 | 44 | self.browser?.start(queue: .main) 45 | self.netService?.publish() 46 | } 47 | 48 | private func reset() { 49 | self.browser?.cancel() 50 | self.browser = nil 51 | self.netService?.stop() 52 | self.netService = nil 53 | } 54 | } 55 | 56 | @available(iOS 14.0, *) 57 | extension LocalNetworkAuthorization : NetServiceDelegate { 58 | public func netServiceDidPublish(_ sender: NetService) { 59 | self.reset() 60 | print("Local network permission has been granted") 61 | completion?(true) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/NetworkPeripheral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkPeripheral.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 8/24/22. 6 | // 7 | 8 | import Foundation 9 | 10 | open class NetworkPeripheral: NSObject { 11 | 12 | var peripheral: NetService 13 | 14 | public init(peripheral: NetService) { 15 | self.peripheral = peripheral 16 | 17 | super.init() 18 | self.peripheral.delegate = self 19 | } 20 | 21 | open var name: String { 22 | return peripheral.name 23 | } 24 | 25 | open var hostName: String? { 26 | return peripheral.hostName 27 | } 28 | 29 | 30 | 31 | 32 | 33 | } 34 | extension NetworkPeripheral: NetServiceDelegate { 35 | 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/PyLeap-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // PyLeap-Bridging-Header.h 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 9/7/22. 6 | // 7 | 8 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/Queue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Queue.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 8/25/22. 6 | // 7 | 8 | import Foundation 9 | 10 | class Queue { 11 | var array = [T]() 12 | 13 | func enQueue(value: T) { 14 | array.append(value) 15 | } 16 | 17 | func deQueue() -> T? { 18 | if array.isEmpty { 19 | return nil 20 | } else { 21 | return array.remove(at:0) 22 | } 23 | 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/SettingsView/BLESetttings/BLESettingsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BLESettingsViewModel.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 12/15/22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | class BLESettingsViewModel: ObservableObject { 12 | 13 | private let kPrefix = Bundle.main.bundleIdentifier! 14 | let userDefaults = UserDefaults.standard 15 | @Published var hostName = "" 16 | @Published var device = "" 17 | @Published var ipAddress = "" 18 | var connectedToDevice = false 19 | @Published var invalidURL = false 20 | @Published var confirmDownload = false 21 | 22 | 23 | init() { 24 | check() 25 | registerNotifications(enabled: true) 26 | } 27 | 28 | private weak var errorObserver: NSObjectProtocol? 29 | private weak var confirmDownloadObserver: NSObjectProtocol? 30 | private weak var invalidIPObserver: NSObjectProtocol? 31 | 32 | 33 | private func registerNotifications(enabled: Bool) { 34 | let notificationCenter = NotificationCenter.default 35 | 36 | if enabled { 37 | errorObserver = notificationCenter.addObserver(forName: .invalidCustomNetworkRequest, object: nil, queue: .main, using: {[weak self] _ in self?.showError()}) 38 | 39 | 40 | confirmDownloadObserver = notificationCenter.addObserver(forName: .didCollectCustomProject, object: nil, queue: .main, using: {[weak self] _ in self?.showConfirmationAlert()}) 41 | 42 | 43 | } else { 44 | if let testObserver = errorObserver {notificationCenter.removeObserver(testObserver)} 45 | 46 | } 47 | } 48 | 49 | 50 | func showError() { 51 | invalidURL = true 52 | } 53 | 54 | func showConfirmationAlert() { 55 | confirmDownload = true 56 | } 57 | 58 | func check() { 59 | print(#function) 60 | if userDefaults.object(forKey: kPrefix+".storeResolvedAddress.ipAddress") == nil { 61 | connectedToDevice = false 62 | } else { 63 | 64 | connectedToDevice = true 65 | 66 | ipAddress = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.ipAddress") as! String 67 | hostName = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") as! String 68 | device = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.device") as! String 69 | } 70 | } 71 | 72 | func clearKnownIPAddress() { 73 | userDefaults.set(nil, forKey: kPrefix+".storedIP") 74 | userDefaults.set(nil, forKey: kPrefix+".storeResolvedAddress.ipAddress" ) 75 | userDefaults.set(nil, forKey: kPrefix+".storeResolvedAddress.hostName" ) 76 | userDefaults.set(nil, forKey: kPrefix+".storeResolvedAddress.device" ) 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/SettingsView/SettingsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewModel.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 9/14/22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | class SettingsViewModel: ObservableObject { 12 | 13 | private let kPrefix = Bundle.main.bundleIdentifier! 14 | let userDefaults = UserDefaults.standard 15 | @Published var hostName = "" 16 | @Published var device = "" 17 | @Published var ipAddress = "" 18 | var connectedToDevice = false 19 | @Published var invalidURL = false 20 | @Published var confirmDownload = false 21 | 22 | 23 | init() { 24 | check() 25 | registerNotifications(enabled: true) 26 | } 27 | 28 | private weak var errorObserver: NSObjectProtocol? 29 | private weak var confirmDownloadObserver: NSObjectProtocol? 30 | private weak var invalidIPObserver: NSObjectProtocol? 31 | 32 | 33 | private func registerNotifications(enabled: Bool) { 34 | let notificationCenter = NotificationCenter.default 35 | 36 | if enabled { 37 | errorObserver = notificationCenter.addObserver(forName: .invalidCustomNetworkRequest, object: nil, queue: .main, using: {[weak self] _ in self?.showError()}) 38 | 39 | 40 | confirmDownloadObserver = notificationCenter.addObserver(forName: .didCollectCustomProject, object: nil, queue: .main, using: {[weak self] _ in self?.showConfirmationAlert()}) 41 | 42 | 43 | } else { 44 | if let testObserver = errorObserver {notificationCenter.removeObserver(testObserver)} 45 | 46 | } 47 | } 48 | 49 | 50 | func showError() { 51 | invalidURL = true 52 | } 53 | 54 | func showConfirmationAlert() { 55 | confirmDownload = true 56 | } 57 | 58 | func check() { 59 | print(#function) 60 | if userDefaults.object(forKey: kPrefix+".storeResolvedAddress.ipAddress") == nil { 61 | connectedToDevice = false 62 | } else { 63 | 64 | connectedToDevice = true 65 | 66 | ipAddress = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.ipAddress") as! String 67 | hostName = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") as! String 68 | device = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.device") as! String 69 | } 70 | } 71 | 72 | func clearKnownIPAddress() { 73 | userDefaults.set(nil, forKey: kPrefix+".storedIP") 74 | userDefaults.set(nil, forKey: kPrefix+".storeResolvedAddress.ipAddress" ) 75 | userDefaults.set(nil, forKey: kPrefix+".storeResolvedAddress.hostName" ) 76 | userDefaults.set(nil, forKey: kPrefix+".storeResolvedAddress.device" ) 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/WifiCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WifiCell.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 9/1/22. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | 11 | 12 | class ExpandedState: ObservableObject { 13 | @Published var currentCell = "" 14 | } 15 | 16 | 17 | 18 | struct WifiCell: View { 19 | 20 | @EnvironmentObject var expandedState : ExpandedState 21 | 22 | let result : ResultItem 23 | 24 | @State var isExpanded: Bool = false { 25 | didSet { 26 | onViewGeometryChanged() 27 | } 28 | } 29 | 30 | @State var isExpandedTest: String = "" 31 | 32 | 33 | @ObservedObject var viewModel = WifiCellViewModel() 34 | @Binding var isConnected: Bool 35 | @Binding var bootOne: String 36 | @Binding var stateBinder: DownloadState 37 | 38 | var showRunItButton = false 39 | 40 | var projectName = String() 41 | 42 | let onViewGeometryChanged: ()->Void 43 | 44 | var body: some View { 45 | content 46 | .frame(maxWidth: .infinity) 47 | } 48 | 49 | var header: some View { 50 | 51 | HStack { 52 | Text(result.projectName) 53 | .font(Font.custom("ReadexPro-Regular", size: 24)) 54 | .padding(8) 55 | .foregroundColor(.white) 56 | 57 | Spacer() 58 | 59 | Image(systemName: "chevron.down") 60 | .resizable() 61 | .scaledToFit() 62 | .frame(width: 30, height: 15, alignment: .center) 63 | .foregroundColor(.white) 64 | .padding(.trailing, 30) 65 | } 66 | 67 | 68 | .padding(.vertical, 5) 69 | .padding(.leading) 70 | .frame(maxWidth: .infinity) 71 | .background(Color("pyleap_purple")) 72 | .onTapGesture { 73 | expandedState.currentCell = result.bundleLink 74 | } 75 | 76 | 77 | 78 | } 79 | 80 | 81 | var content: some View { 82 | VStack(alignment: .leading, spacing: 8) { 83 | header 84 | 85 | if isExpanded { 86 | Group { 87 | WifiSubViewCell(result: result, bindingString: $bootOne, downloadStateBinder: $stateBinder,isConnected: $isConnected) 88 | 89 | } 90 | } 91 | } 92 | } 93 | 94 | 95 | 96 | 97 | 98 | } 99 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/WifiCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WifiCellViewModel.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 12/2/22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | class WifiCellViewModel: ObservableObject { 12 | 13 | @Published var isExpanded : Bool = false 14 | 15 | init() { 16 | print("Initialized") 17 | print("\(#function) @Line: \(#line)") 18 | } 19 | 20 | deinit { 21 | print("Deinitialized") 22 | print("\(#function) @Line: \(#line)") 23 | } 24 | 25 | @Published var projectBundle = "" 26 | 27 | } 28 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/WifiListDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WifiListDetailView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 8/23/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WifiListDetailView: View { 11 | 12 | let text: String 13 | 14 | var body: some View { 15 | VStack { 16 | Text(text) 17 | .lineLimit(nil) 18 | .multilineTextAlignment(.leading) 19 | 20 | } 21 | } 22 | } 23 | struct WifiListDetailView_Previews: PreviewProvider { 24 | static var previews: some View { 25 | WifiListDetailView(text: "") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/WifiNetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WifiNetworkService.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 8/9/22. 6 | // 7 | 8 | import Foundation 9 | 10 | enum NetworkerError: Error { 11 | case badResponse 12 | case badStatusCode(Int) 13 | case badData 14 | } 15 | 16 | //enum 17 | 18 | class WifiNetworkService { 19 | 20 | private let session: URLSession 21 | 22 | init() { 23 | let config = URLSessionConfiguration.default 24 | session = URLSession(configuration: config) 25 | // getIPAddress() 26 | } 27 | 28 | func getIFAddresses() -> [String] { 29 | var addresses = [String]() 30 | 31 | // Get list of all interfaces on the local machine: 32 | var ifaddr : UnsafeMutablePointer? 33 | guard getifaddrs(&ifaddr) == 0 else { return [] } 34 | guard let firstAddr = ifaddr else { return [] } 35 | 36 | // For each interface ... 37 | for ptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) { 38 | let flags = Int32(ptr.pointee.ifa_flags) 39 | let addr = ptr.pointee.ifa_addr.pointee 40 | 41 | // Check for running IPv4, IPv6 interfaces. Skip the loopback interface. 42 | if (flags & (IFF_UP|IFF_RUNNING|IFF_LOOPBACK)) == (IFF_UP|IFF_RUNNING) { 43 | if addr.sa_family == UInt8(AF_INET) || addr.sa_family == UInt8(AF_INET6) { 44 | 45 | // Convert interface address to a human readable string: 46 | var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) 47 | if (getnameinfo(ptr.pointee.ifa_addr, socklen_t(addr.sa_len), &hostname, socklen_t(hostname.count), 48 | nil, socklen_t(0), NI_NUMERICHOST) == 0) { 49 | let address = String(cString: hostname) 50 | addresses.append(address) 51 | } 52 | } 53 | } 54 | } 55 | 56 | freeifaddrs(ifaddr) 57 | return addresses 58 | } 59 | 60 | func getIPAddress() -> String { 61 | var address: String? 62 | var ifaddr: UnsafeMutablePointer? = nil 63 | if getifaddrs(&ifaddr) == 0 { 64 | var ptr = ifaddr 65 | while ptr != nil { 66 | defer { ptr = ptr?.pointee.ifa_next } 67 | 68 | guard let interface = ptr?.pointee else { return "" } 69 | let addrFamily = interface.ifa_addr.pointee.sa_family 70 | if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) { 71 | 72 | // wifi = ["en0"] 73 | // wired = ["en2", "en3", "en4"] 74 | // cellular = ["pdp_ip0","pdp_ip1","pdp_ip2","pdp_ip3"] 75 | 76 | let name: String = String(cString: (interface.ifa_name)) 77 | if name == "en0" { 78 | var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) 79 | getnameinfo(interface.ifa_addr, socklen_t((interface.ifa_addr.pointee.sa_len)), &hostname, socklen_t(hostname.count), nil, socklen_t(0), NI_NUMERICHOST) 80 | address = String(cString: hostname) 81 | } 82 | } 83 | } 84 | freeifaddrs(ifaddr) 85 | } 86 | return address ?? "" 87 | } 88 | 89 | 90 | 91 | 92 | // public static func request(_ url: URLConvertible, 93 | // method: HTTPMethod = .get, 94 | // parameters: Parameters? = nil, 95 | // encoding: ParameterEncoding = URLEncoding.default, 96 | // headers: HTTPHeaders? = nil, 97 | // interceptor: RequestInterceptor? = nil) -> DataRequest { 98 | // return Session.default.request(url, 99 | // method: method, 100 | // parameters: parameters, 101 | // encoding: encoding, 102 | // headers: headers, 103 | // interceptor: interceptor) 104 | // } 105 | } 106 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/WifiStatusHeaderBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WifiStatusHeaderBarView.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 9/8/22. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | 11 | struct WifiStatusConnectedView: View { 12 | 13 | let userDefaults = UserDefaults.standard 14 | private let kPrefix = Bundle.main.bundleIdentifier! 15 | @EnvironmentObject var rootViewModel: RootViewModel 16 | 17 | @Binding var hostName: String 18 | 19 | func showConfirmationPrompt() { 20 | comfirmationAlertMessage(title: "Are you sure you want to disconnect?", exitTitle: "Cancel", primaryTitle: "Disconnect") { 21 | rootViewModel.goToSelection() 22 | } cancel: { 23 | 24 | } 25 | } 26 | 27 | var body: some View { 28 | HStack(alignment: .center, spacing: 0, content: { 29 | 30 | Image(systemName: "wifi") 31 | .resizable() 32 | .scaledToFit() 33 | .frame(width: 20, height: 20) 34 | .padding(5) 35 | 36 | Text("Connected to \(hostName). ") 37 | .font(Font.custom("ReadexPro-Regular", size: 14)) 38 | 39 | Button { 40 | showConfirmationPrompt() 41 | } label: { 42 | Text("Disconnect") 43 | .font(Font.custom("ReadexPro-Bold", size: 14)) 44 | .underline() 45 | .minimumScaleFactor(0.1) 46 | } 47 | 48 | }) 49 | .padding(.all, 0.0) 50 | .frame(maxWidth: .infinity) 51 | .frame(maxHeight: 40) 52 | .background(Color("pyleap_green")) 53 | .foregroundColor(.white) 54 | } 55 | } 56 | 57 | struct WifiStatusNoConnectionView: View { 58 | var body: some View { 59 | 60 | HStack(alignment: .center, spacing: 8, content: { 61 | Text("No Device Detected") 62 | .font(Font.custom("ReadexPro-SemiBold", size: 14)) 63 | 64 | }) 65 | .padding(.all, 0.0) 66 | .frame(maxWidth: .infinity) 67 | .frame(maxHeight: 40) 68 | .background(Color("pyleap_burg")) 69 | .foregroundColor(.white) 70 | 71 | } 72 | } 73 | 74 | struct WifiStatusConnectingView: View { 75 | var body: some View { 76 | HStack(alignment: .center, spacing: 8, content: { 77 | Text("Searching for Adafruit Devices...") 78 | .font(Font.custom("ReadexPro-Regular", size: 14)) 79 | }) 80 | .padding(.all, 0.0) 81 | .frame(maxWidth: .infinity) 82 | .frame(maxHeight: 40) 83 | .background(Color("pyleap_yellow")) 84 | .foregroundColor(.white) 85 | 86 | } 87 | 88 | } 89 | 90 | struct NetworkConnectionBanner: View { 91 | @State var spin = false 92 | 93 | var body: some View { 94 | HStack(alignment: .center, spacing: 8, content: { 95 | Text("Searching local network...") 96 | .font(Font.custom("ReadexPro-Regular", size: 14)) 97 | 98 | // ProgressView() 99 | //.resizable() 100 | // .frame(width: 40, height: 40, alignment: .center) 101 | // .rotationEffect(.degrees(spin ? 360: 0)) 102 | // .animation(Animation.linear(duration: 0.8, curve:.linear).repeatForever(autoreverses: false)) 103 | // .animation(Animation.linear.repeatForever(autoreverses: false)) 104 | // .onAppear(){ 105 | // spin = true 106 | // } 107 | 108 | }) 109 | .padding(.all, 0.0) 110 | .frame(maxWidth: .infinity) 111 | .frame(maxHeight: 40) 112 | .background(Color("pyleap_yellow")) 113 | .foregroundColor(.white) 114 | 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/WifiSubViewCellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WifiSubViewCellModel.swift 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 9/1/22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | class WifiSubViewCellModel: ObservableObject { 12 | 13 | @ObservedObject var wifiTransferService = WifiTransferService() 14 | 15 | @ObservedObject var wifiFileTransfer = WifiFileTransfer() 16 | 17 | @Published var downloadState: DownloadState = .idle 18 | 19 | @Published var projectDownloaded = false 20 | @Published var failedProjectLaunch = false 21 | 22 | @Published var usbInUse = false 23 | @Published var showUsbInUseError = false 24 | 25 | 26 | init() { 27 | registerForUSBInUseErrorNotification(enabled: true) 28 | } 29 | 30 | private weak var usbInUseErrorNotification: NSObjectProtocol? 31 | 32 | private func registerForUSBInUseErrorNotification(enabled: Bool) { 33 | print("\(#function) @Line: \(#line)") 34 | 35 | let notificationCenter = NotificationCenter.default 36 | 37 | if enabled { 38 | 39 | // NotificationCenter.default.addObserver(self, selector: #selector(zipSuccess(_:)), name: .usbInUseErrorNotification,object: nil) 40 | 41 | usbInUseErrorNotification = notificationCenter.addObserver(forName: .usbInUseErrorNotification, object: nil, queue: .main, using: {[weak self] _ in self?.zipSuccess()}) 42 | 43 | } else { 44 | 45 | } 46 | } 47 | 48 | 49 | 50 | 51 | func zipSuccess() { 52 | showUsbInUseError = true 53 | } 54 | 55 | func checkIfUSBInUse() { 56 | 57 | wifiTransferService.optionRequest(handler: { result in 58 | switch result { 59 | 60 | case .success: 61 | print("Success") 62 | 63 | self.wifiTransferService.getRequest(read: "boot_out.txt") { result in 64 | print(result) 65 | } 66 | 67 | 68 | case .failure: 69 | print("Failure") 70 | } 71 | 72 | 73 | 74 | 75 | // if success.contains("GET, OPTIONS, PUT, DELETE, MOVE") { 76 | // 77 | // print("USB not in use.") 78 | // DispatchQueue.main.async { 79 | // self.usbInUse = false 80 | // } 81 | // 82 | // // DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 83 | // // if wifiFileTransfer.projectDownloaded { 84 | // // 85 | // // wifiFileTransfer.testFileExistance(for: result.projectName, bundleLink: result.bundleLink) 86 | // // 87 | // // } else { 88 | // // downloadModel.trueDownload(useProject: result.bundleLink, projectName: result.projectName) 89 | // // } 90 | // // } 91 | // 92 | // } else { 93 | // DispatchQueue.main.async { 94 | // self.usbInUse = true 95 | // } 96 | // print("USB in use - files cannot be tranferred or moved while USB is in use. Show Error") 97 | // } 98 | 99 | }) 100 | 101 | } 102 | 103 | 104 | 105 | let directoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] 106 | 107 | func searchPathForProject(nameOf project: String) { 108 | var manager = FileManager.default 109 | 110 | let nestedFolderURL = directoryPath.appendingPathComponent(project) 111 | 112 | if manager.fileExists(atPath: nestedFolderURL.relativePath) { 113 | print("\(project) - Exist") 114 | projectDownloaded = true 115 | } else { 116 | print("Does not exist - \(project)") 117 | projectDownloaded = false 118 | } 119 | } 120 | 121 | 122 | 123 | 124 | } 125 | -------------------------------------------------------------------------------- /PyLeap/Views/Wifi View/Wifi_ifaddrs.m: -------------------------------------------------------------------------------- 1 | // 2 | // Wifi_ifaddrs.m 3 | // PyLeap 4 | // 5 | // Created by Trevor Beaton on 8/9/22. 6 | // 7 | 8 | #import 9 | #include 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyLeap-iOS 2 | A Bluetooth Low Energy File Transfer native iOS app written in Swift for Adafruit Industries. This app will make it accessible to send files via BLE (Bluetooth LE) to your Adafruit hardware on the go! 3 | 4 | Work in progress. 5 | 6 | ### [Introduction to iOS Development](https://learn.adafruit.com/introduction-to-ios-development) 7 | If you'd like to get started with iOS development, I'll written a guide on how you can get started right way. 8 | 9 | 10 | ### [Basic Chat](https://github.com/adafruit/Basic-Chat) 11 | You can also check out the Basic Chat project this basic iOS project focused on Bluetooth Low Energy communication. 12 | 13 | Basic Chat is a Bluetooth Low Energy native iOS app using Swift 5. Basic Chat communicates with bluetooth enabled devices that implements the Nordic UART Service. 14 | 15 | This sample project is the source for [this tutorial](https://learn.adafruit.com/build-a-bluetooth-app-using-swift-5/overview). It is not meant to cover product ready iOS development. It is only an introduction to BLE and iOS. 16 | 17 | # Hardware 18 | Currently testing with: 19 | 20 | • [Adafruit Circuit Playground Bluefruit with nRF52840](https://www.adafruit.com/product/4333) 21 | 22 | • [Adafruit CLUE nRF52840 Express with nRF52840](https://www.adafruit.com/product/4500) 23 | 24 | 25 | # Installation and usage: 26 | 27 | • Prepare your boards. If you have not setup either of the boards mentioned above, be sure to follow these guides. 28 | 29 | [Introducing Adafruit CLUE](https://learn.adafruit.com/adafruit-clue) 30 | 31 | [Adafruit Circuit Playground Bluefruit](https://learn.adafruit.com/adafruit-circuit-playground-bluefruit) 32 | 33 | • Launch Xcode and run build the project on your target iOS Device. 34 | 35 | (Note: Bluetooth Low Engery is not available in the iOS simulator, so the iOS device is a requirement to test the application) 36 | 37 | ### This project is not available to Mac Catalyst yet. 38 | 39 | 40 | --------------------------------------------------------------------------------