├── .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 |
--------------------------------------------------------------------------------